CodeGym /Các khóa học /C# SELF /Hàm bậc cao

Hàm bậc cao

C# SELF
Mức độ, Bài học
Có sẵn

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
Where
Func<T, bool>
Lọc phần tử
Select
Func<T, TResult>
Biến đổi phần tử
OrderBy
Func<T, TKey>
Sắp xếp theo khóa
Aggregate
Func<TAcc, T, TAcc>
Tổng hợp (gộp) collection
Any
Func<T, bool>
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;
}
2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Lọc mảng bằng hàm bậc cao
Lọc mảng bằng hàm bậc cao
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION