1. Giới thiệu
Chắc hồi ở trường bạn từng nghe: "Đó là hàm của hàm". Hàm bậc cao (Higher-Order Functions, HOF) là những hàm mà hoặc nhận hàm khác làm đối số, hoặc trả về hàm như kết quả, hoặc cả hai.
Nói nôm na: nếu method của bạn có thể nhận một hàm khác (ví dụ delegate hoặc lambda) làm tham số, hoặc trả về nó — chúc mừng, bạn có hàm bậc cao!
Hàm bậc cao giống như một chìa khoá đa năng: nó không chỉ mở cửa được, mà còn có thể đưa bạn một chìa khoá khác để bạn mở cửa cần thiết sau này.
Ứng dụng trong thực tế
Nghe thì hay ho và hơi "to tát". Nhưng rốt cuộc mấy trick này có ích gì trong đời sống hàng ngày của dev C#? Câu trả lời ngắn gọn nhất là:
Hàm bậc cao làm code linh hoạt, tái sử dụng được và ngắn gọn hơn.
Đây là nền tảng cho thứ như LINQ, lọc và sắp xếp collection, xây dựng pipeline xử lý dữ liệu, cấu hình callback, event và thậm chí dependency injection.
Một vài kịch bản thực tế:
- Giới hạn hành vi của method bằng cách truyền vào logic (ví dụ: filter, sort, transform).
- Viết một handler dữ liệu generic, mà bạn "vặn" logic cần thiết vào bằng delegate.
- Xây dựng chuỗi xử lý ("pipeline") nơi mỗi hàm chỉnh sửa dữ liệu theo "công thức" riêng.
- Tạo abstraction đa nền tảng: chạy khác trên Windows, khác trên Linux? Chỉ việc truyền hàm phù hợp.
2. Ví dụ đơn giản về hàm bậc cao
Hàm nhận một hàm khác
Ví dụ cổ điển nhất — method nhận delegate hoặc lambda.
// Hàm bậc cao: nhận hàm process làm tham số
void ForEach<T>(IEnumerable<T> collection, Action<T> process)
{
foreach (var item in collection)
{
process(item); // Gọi hàm truyền vào
}
}
// Sử dụng:
var numbers = new List<int> { 1, 2, 3 };
ForEach(numbers, n => Console.WriteLine($"Phần tử: {n}"));
Chuyện gì đang xảy ra ở đây? Method ForEach không biết chính xác phải làm gì với từng phần tử. Tất cả những gì nó làm là gọi processor được truyền vào (process). Processor này có thể làm bất cứ thứ gì — in ra console, lưu vào DB, vẽ lên UI, v.v.
Đúng vậy, đó là cách hoạt động của method ForEach trên collection trong C#, và hầu hết các method của LINQ — đều là hàm bậc cao!
Hàm trả về một hàm
Bây giờ ví dụ nặng hơn — method không chỉ nhận mà còn trả về một hàm.
// Hàm bậc cao: trả về một hàm khác
Func<int, int> CreateMultiplier(int factor)
{
// Trả về lambda sử dụng biến factor
return x => x * factor;
}
// Sử dụng:
var multiplyBy10 = CreateMultiplier(10);
Console.WriteLine(multiplyBy10(7)); // 70
Ở đây CreateMultiplier trả về một hàm nhân tham số của nó với factor đã cho. Đây không chỉ là HOF, mà còn là ví dụ về "function factory".
Hàm nhận và trả về hàm
Func<int, int> Compose(Func<int, int> f, Func<int, int> g)
{
// Trả về hàm áp dụng g trước, rồi f: f(g(x))
return x => f(g(x));
}
// Sử dụng:
Func<int, int> increment = x => x + 1;
Func<int, int> doubleIt = x => x * 2;
var incrementThenDouble = Compose(doubleIt, increment);
Console.WriteLine(incrementThenDouble(5)); // (5 + 1) * 2 = 12
Chính các composition thế này là nền tảng cho xử lý dữ liệu theo chuỗi — ví dụ khi bạn dùng Select, Where, OrderBy, v.v.
3. C# hỗ trợ hàm bậc cao như thế nào
Trong các ngôn ngữ hàm thực thụ (Haskell, F#) mọi hàm mặc định là hàm bậc cao. C# (từ version 2.0) cũng hỗ trợ model này nhờ delegate và lambda.
- Delegates (Func, Action, Predicate) — kiểu của hàm.
- Lambda expressions — cú pháp tạo hàm inline.
- Methods có thể nhận và trả delegate — vậy là hàm bậc cao được "hỗ trợ sẵn".
Sơ đồ trực quan
flowchart LR
A[Dữ liệu] --> B[Hàm 1]
B --> C[Hàm 2]
C --> D[Kết quả]
subgraph "Pipeline xử lý (hàm bậc cao)"
B
C
end
4. Phát triển ứng dụng ví dụ
Tiếp tục phát triển demo step-by-step — giả sử đây là "Mini string processor".
Thêm method bậc cao đơn giản
Giả sử ta có list tên người dùng, và muốn biến đổi tuỳ ý bằng các hàm.
// Method nhận list string và hàm để biến đổi
List<string> TransformNames(List<string> names, Func<string, string> transformer)
{
var result = new List<string>();
foreach (var name in names)
{
result.Add(transformer(name));
}
return result;
}
Dùng method này thế nào?
var names = new List<string> { "Anna", "Boris", "Sergey" };
// Chuyển thành chữ hoa
var upperNames = TransformNames(names, n => n.ToUpper());
// Thêm prefix "Thân mến" vào mỗi tên
var politeNames = TransformNames(names, n => "Thân mến " + n);
foreach (var n in upperNames)
Console.WriteLine(n); // ANNA, BORIS, SERGEY
foreach (var n in politeNames)
Console.WriteLine(n); // Thân mến Anna, ...
Method TransformNames generic: nó uỷ quyền logic biến đổi cho tham số truyền vào (transformer), ví dụ gọi ToUpper hoặc bất kỳ "công thức" nào khác.
Thích nghi với các kiểu
Ví dụ có thể dễ dàng generic hoá cho mọi kiểu dữ liệu.
// Method generic - hàm bậc cao cho mọi T
List<TResult> Map<T, TResult>(List<T> items, Func<T, TResult> transformer)
{
var result = new List<TResult>();
foreach (var item in items)
{
result.Add(transformer(item));
}
return result;
}
Áp dụng:
var numbers = new List<int> { 1, 2, 3 };
var doubled = Map(numbers, x => x * 2); // [2, 4, 6]
var strings = Map(numbers, x => $"Số: {x}"); // ["Số: 1", ...]
5. Lọc và tổng hợp qua hàm bậc cao
Logic lọc và tìm kiếm từ lâu đã được implement bằng hàm bậc cao.
// Lọc: hàm bậc cao
List<T> Filter<T>(List<T> items, Predicate<T> criteria)
{
var result = new List<T>();
foreach (var item in items)
{
if (criteria(item)) // Gọi hàm điều kiện
{
result.Add(item);
}
}
return result;
}
Cách dùng:
var names = new List<string> { "Anna", "Boris", "Andrei" };
var aNames = Filter(names, n => n.StartsWith("A"));
// Kết quả: "Anna", "Andrei"
6. Khái niệm composition của hàm (function composition)
Hàm bậc cao không chỉ cho phép dùng từng hàm rời rạc, mà còn ghép chúng thành chuỗi — gọi là composition. Trong C# ta có thể làm một hàm nhận hai hàm và trả về hàm kết hợp.
// Composer hàm: trả về hàm áp dụng trước g, sau đó f
Func<T, TResult> Compose<T, TIntermediate, TResult>(
Func<TIntermediate, TResult> f,
Func<T, TIntermediate> g)
{
return x => f(g(x));
}
// Ví dụ:
Func<int, int> plusOne = n => n + 1;
Func<int, int> timesTwo = n => n * 2;
var plusOneThenDouble = Compose(timesTwo, plusOne);
Console.WriteLine(plusOneThenDouble(3)); // (3 + 1) * 2 = 8
7. Một vài chú ý hữu ích
Bối cảnh lịch sử: tại sao trước đây mọi thứ phức tạp hơn?
Trước khi có delegate và lambda, dev thường viết nhiều vòng lặp na ná nhau, copy/paste code để "lọc", "biến đổi" hay "gộp" dữ liệu. Khi có hàm bậc cao, ta có thể tách phần thay đổi ra thành các hàm tham số — giảm duplication triệt để và làm code biểu đạt hơn.
Một chút syntactic sugar: hàm như biểu thức
Hàm bậc cao thường được viết dưới dạng expression-bodied methods — ngắn gọn, một dòng:
List<string> FilterNames(Predicate<string> pred) =>
Names.Where(name => pred(name)).ToList();
List<TResult> MapNames<TResult>(Func<string, TResult> transformer) =>
Names.Select(transformer).ToList();
So sánh với cơ chế của LINQ
Nhìn cách LINQ dùng hàm bậc cao:
| Method của LINQ | Nhận delegate kiểu | Mục đích |
|---|---|---|
|
|
Lọc phần tử |
|
|
Biến đổi phần tử |
|
|
Sắp xếp theo khóa |
|
|
Tổng hợp (gộp) collection |
|
|
Kiểm tra sự tồn tại của phần tử theo điều kiện |
Tất cả những method này xoay quanh ý tưởng hàm bậc cao: bạn viết luật xử lý, còn thư viện chuẩn lo phần "cơ sở hạ tầng".
8. Lỗi hay gặp và mấy cái bẫy
Nhầm lẫn kiểu delegate.
Ban đầu có thể khó phân biệt lúc nào dùng Action, khi nào Func, hoặc Predicate.
Mẹo: Nếu hàm trả về bool — có thể là Predicate. Nếu trả về giá trị — dùng Func, nếu không trả về gì — Action.
Closure (zakhvat bien).
Nếu hàm trả về sử dụng biến từ scope bên ngoài, chú ý để giá trị đúng tại thời điểm gọi. Biến không được copy, mà bị "captured".
Debug chuỗi phức tạp.
Khi các hàm được nối thành pipeline dài, đôi khi khó biết lớp nào xử lý sai. Thêm các debug print tạm và comment:
n => {
Console.WriteLine("Trước thao tác:" + n);
var res = n * 2;
Console.WriteLine("Sau:" + res);
return res;
}
GO TO FULL VERSION