CodeGym /Các khóa học /C# SELF /Hàm thuần và bất biến

Hàm thuần và bất biến

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

1. Giới thiệu

Hàm thuần — là hàm mà với cùng đầu vào luôn trả về cùng một kết quả và không gây bất kỳ hiệu ứng phụ nào. Nói nôm na: hàm thuần không "phá" gì xung quanh nó và không bị "nhiễm" từ bên ngoài.

Một hàm được coi là thuần khi:

  • Nó chỉ tính toán giá trị dựa trên các tham số vào.
  • Không thay đổi biến bên ngoài hoặc trạng thái của chương trình.
  • Không phụ thuộc vào các biến bên ngoài hoặc trạng thái có thể thay đổi.

Hai quy tắc vàng của tính thuần

  1. Tính xác định: cùng một đầu vào — cùng một đầu ra.
  2. Không có hiệu ứng phụ: hàm không thay đổi gì bên ngoài nó: không file, không biến toàn cục, không giao diện người dùng (chào Console.WriteLine).

Không phải tín ngưỡng, mà là lý trí

Có thể tưởng tượng đây là mấy chuyện học thuật, nhưng thực tế thì ngược lại:

  • Hàm thuần dự đoán được. Nó dễ test — bạn gọi với các tham số cụ thể và nhận kết quả cụ thể.
  • Hàm thuần có thể "xê dịch" tự do trong code mà không sợ hỏng chỗ khác.
  • Trong thế giới đa nhân, hàm thuần là đảm bảo an toàn: có thể chạy song song mà không lo race condition.

2. Ví dụ về hàm thuần và không thuần

Hàm thuần: mọi thứ dự đoán được

// Hàm thuần: không chạm gì bên ngoài
int Add(int a, int b)
{
    return a + b;
}

int Square(int x)
{
    return x * x;
}

Gọi Add(2, 3) cả trăm lần — luôn ra 5. Chán nhưng đáng tin cậy.

Hàm không thuần: vi phạm quy tắc

// Vi phạm tính thuần: phụ thuộc vào trạng thái bên ngoài (biến static)
int counter = 0;

int Increase()
{
    counter++;
    return counter;
}

Ở đây gọi Increase() sẽ trả về giá trị mới mỗi lần — tức là không còn tính xác định.

// Vi phạm tính thuần: gây hiệu ứng ngoài (in ra màn hình)
int AddAndPrint(int a, int b)
{
    int sum = a + b;
    Console.WriteLine(sum); // Hiệu ứng phụ!
    return sum;
}

Còn randomness và thời gian thì sao?

Bất kỳ hàm nào dùng DateTime.Now hoặc Random thì không còn thuần nữa:

// Không thuần!
int GetRandomNumber()
{
    return new Random().Next();
}

Bảng so sánh

Đặc tính Hàm thuần Hàm không thuần
Một kết quả giống nhau cho cùng tham số Có Không
Hiệu ứng phụ Không Có
Phụ thuộc vào trạng thái bên ngoài Không Có

3. Bất biến dữ liệu: lý thuyết và thực hành

Bất biến (immutability) — là cách tiếp cận khi đối tượng không thể thay đổi sau khi tạo. Nếu cần giá trị mới — tạo đối tượng mới.

Tại sao quan trọng?

  • Ứng dụng trở nên bền hơn với lỗi vô tình khi thay đổi dữ liệu.
  • Không có "rò rỉ" thay đổi bí mật: nếu bạn có 1 đối tượng, sẽ không ai lén thay đổi trường của nó.
  • Bất biến là nền tảng cho nhiều tối ưu hóa tự động và tính toán song song.

Ví dụ đơn giản trong C#

Kiểu bất biến trong .NET

Chuỗi (string) trong C# là bất biến! Mỗi khi bạn làm string.Concat(s, "world"), một chuỗi mới được tạo ra.

string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"

Mảng và collection: mutable theo mặc định

int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // Mảng bị thay đổi!

Bất biến "dễ hiểu": biên dịch mã

Thay vì thay đổi đối tượng/giá trị hiện có, trả về cái mới:

// Thay vì này:
void AddToList(List<int> list, int value)
{
    list.Add(value); // Đột biến!
}

// Tốt hơn là:
List<int> AddToList(List<int> list, int value)
{
    var newList = new List<int>(list) { value }; // Danh sách mới
    return newList;
}

Minh họa: thay đổi hay không thay đổi

flowchart LR
    A[Đối tượng gốc] --"mutation"--> B[Cùng đối tượng, nhưng khác bên trong]
    A --"immutability"--> C[Đối tượng mới]

4. Tại sao cần trong mã C# thực tế?

  • Trong C# hiện đại các thư viện như LINQ, Entity Framework và ASP.NET Core thiên về hàm thuần và bất biến.
  • Bất biến giảm số "bug kỳ diệu" khi ai đó ở đâu đó ghi đè giá trị quan trọng.
  • Hàm thuần cho phép dễ dùng unit-testing tự động, vì để test chỉ cần kiểm tra input và output, không cần quan tâm thế giới bên ngoài.

Ví dụ: làm việc với string

string s = "Hello";
string newS = s.Replace("H", "J"); // s vẫn là "Hello"; newS — "Jello"

Ví dụ: LINQ và collection

Where, Select và các method khác trả về collection mới, không đụng tới cái cũ.

var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();

// numbers vẫn giữ nguyên!

Ví dụ đời thực: cấu hình qua đối tượng bất biến

Nhiều API .NET hiện dùng đối tượng bất biến cho cấu hình, ví dụ JsonSerializerOptions:

var options = new JsonSerializerOptions
{
    WriteIndented = true
};
// Đối tượng này không thay đổi "trong quá trình", giúp tăng độ tin cậy.

5. Lỗi và bẫy thường gặp

Con đường trơn trượt bắt đầu khi bạn "vô tình" mutate dữ liệu trong mã tự cho là "thuần".

Thường xảy ra với collection: bạn muốn filter list, nhưng trong quá trình lại thay đổi cái gốc.

Hoặc quên rằng method như List<T>.Add thay đổi object tại chỗ.

Ví dụ rắc rối:

List<int> DoubleTheNumbers(List<int> xs)
{
    // Sai! Mutate list gốc, trả về cùng object.
    foreach (var i in xs)
        xs.Add(i * 2);
    return xs;
}

Mã này thậm chí sẽ ném lỗi runtime (InvalidOperationException), vì ta thay đổi collection trong khi đang duyệt — vấn đề cổ điển của mutation.

Đúng cách:

List<int> DoubleTheNumbers(List<int> xs)
{
    var newList = new List<int>(xs.Select(x => x * 2));
    return newList;
}
2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Danh sách số không thay đổi
Danh sách số không thay đổi
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION