1. Tính bất biến là gì?
Bắt đầu bằng một ví dụ nhé: tưởng tượng bạn là kế toán, mỗi ngày đều lập báo cáo bán hàng. Là dân chuyên nghiệp, bạn không sửa báo cáo tuần trước, mà tạo báo cáo mới dựa trên cái cũ nhưng với dữ liệu cập nhật. Lập trình với object bất biến cũng vậy: sau khi tạo ra, object đó không thay đổi nữa, mọi "thay đổi" nghĩa là tạo ra một bản sao mới.
Tính bất biến (immutability) — là thuộc tính của object không thay đổi sau khi khởi tạo. Tất cả thuộc tính của nó bị "đóng băng": muốn giá trị khác thì tạo object mới thôi.
Tại sao cần cái này?
- Đảm bảo an toàn: nếu không ai có thể vô tình sửa object của bạn, dữ liệu sẽ không bị phá hỏng bất ngờ. Điều này rất hay gặp trong chương trình đa luồng, khi nhiều luồng cùng lúc muốn sửa gì đó.
- Dễ debug: object không đổi thì bạn biết chắc nó ra sao sau khi tạo.
- Tiện cho truyền dữ liệu, nhất là trong hệ thống phân tán, nơi các bản sao có thể khác nhau.
- Cho phép tạo "ảnh chụp trạng thái" (snapshots) — lịch sử thay đổi trở nên rõ ràng.
2. Tính bất biến ở record-type
Khi bạn khai báo một class bình thường, thuộc tính của nó mặc định là có thể thay đổi (mutable). Ví dụ:
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}
var user = new UserProfile { Name = "Ivan", Age = 25 };
user.Age = 26; // Ok luôn — class thường: đổi tuổi "liền tay"
Với record thì khác: nó dùng để lưu dữ liệu bất biến.
public record UserProfile(string Name, int Age);
// Tạo object:
var user = new UserProfile("Ivan", 25);
// Thử đổi tuổi:
user.Age = 26; // Lỗi biên dịch: thuộc tính chỉ đọc thôi!
Thuộc tính của record positional được khai báo chỉ đọc (init-only). Bạn không thể đổi sau khi tạo, nhưng có thể dùng with-biểu thức để tạo bản sao mới với thuộc tính đã đổi.
Class có thể thay đổi vs record bất biến
| Class | Record-positional |
|---|---|
Thuộc tính mặc định |
Bất biến |
| Cách thay đổi Truy cập thuộc tính |
Chỉ tạo bản sao mới |
| So sánh object Theo tham chiếu (ReferenceEquals) |
Theo giá trị (Equals) |
| Tiện truyền dữ liệu Không phải lúc nào cũng |
Có |
3. with-biểu thức
Chắc bạn nghĩ — "record thì xịn đấy, nhưng không đổi được thì sống sao?" Đây là lúc with-biểu thức xuất hiện!
with — cú pháp đặc biệt cho phép tạo bản sao mới của record, chỉ đổi thuộc tính bạn muốn.
Tức là: "Lấy object này, copy nó, nhưng sửa vài thuộc tính chỗ này."
Ví dụ đơn giản
var user1 = new UserProfile("Anna", 30);
// ... nhưng thời gian trôi, Anna đã lớn hơn
var user2 = user1 with { Age = 31 };
// user1 vẫn như cũ, user2 — bản sao, nhưng lớn hơn 1 tuổi
Console.WriteLine(user1); // UserProfile { Name = Anna, Age = 30 }
Console.WriteLine(user2); // UserProfile { Name = Anna, Age = 31 }
Bên trong hoạt động thế nào
Đây không phải là bản sao lỗi, mà là object mới, tạo bằng phương thức tự động Clone(), tạo bản sao và gán giá trị mới.
Nếu with-biểu thức có ngoài đời, bạn sẽ dậy mỗi sáng không phải trong "cơ thể mệt mỏi cũ", mà là bản sao của chính mình với tâm trạng chỉnh lại và cơ bắp to hơn (nếu bạn là record thôi nha).
4. Một chút về lồng nhau và copy
Nếu record chứa record khác — ổn luôn:
public record Address(string City, string Street);
public record Student(string Name, int Age, string Email, Address Home);
var a1 = new Address("Moskva", "Tverskaya");
var s1 = new Student("Lena", 21, "lena@mail.ru", a1);
var s2 = s1 with { Home = a1 with { Street = "Arbat" } };
Ở đây mọi thứ thực sự bất biến, vì Address lồng bên trong cũng là record.
5. Một số lưu ý cuối
Record positional = gọn nhẹ
Có thể khai báo record theo kiểu “ngắn gọn” (positional syntax). Khi đó tất cả thuộc tính tự động là init-only.
public record Course(string Name, int Credits);
var c1 = new Course("C#", 5);
var c2 = c1 with { Credits = 6 };
Tương tự thuộc tính chỉ đọc (init-only)
Trong record, bạn cũng có thể khai báo thuộc tính rõ ràng như này:
public record Student
{
public string Name { get; init; }
public int Age { get; init; }
}
Những thuộc tính này cũng chỉ đổi được khi khởi tạo (hoặc dùng with).
6. Thực hành: demo app
Viết thử "Trường học online" của tụi mình nhé. Giả sử đã có record cho sinh viên:
public record Student(string Name, int Age, string Email);
Kinh điển: ai đó nhập sai email, mà sinh viên đã tạo tài khoản rồi. Làm sao "cập nhật" email? Dùng with thôi!
var student = new Student("Ekaterina", 19, "kate@school.com");
var updatedStudent = student with { Email = "ekaterina@school.com" };
// Kiểm tra object:
Console.WriteLine(student); // Student { Name = Ekaterina, Age = 19, Email = kate@school.com }
Console.WriteLine(updatedStudent); // Student { Name = Ekaterina, Age = 19, Email = ekaterina@school.com }
7. Lỗi phổ biến và bẫy thường gặp
Giờ nói chút về "nỗi đau" — chỗ mà sinh viên hay vấp phải khi nghịch record bất biến.
- Đầu tiên, nhiều bạn nghĩ with sẽ đổi object gốc. Thực ra, object gốc vẫn y nguyên, chỉ có object mới là có trường đã đổi. Đôi khi vì vậy mà bạn bị mất giá trị mới.
- Thứ hai, nhớ nhé: nếu trong record có object lồng bên trong mà có thể thay đổi (ví dụ mảng hoặc List), thì with-biểu thức không tạo bản sao sâu! Collection của bạn sẽ giống nhau ở cả hai bản.
public record Student(string Name, int Age, List<string> Subjects);
var s1 = new Student("Oleg", 22, new List<string> { "Math", "Physics" });
var s2 = s1 with { };
s1.Subjects.Add("C#"); // Ồ, giờ cả s2.Subjects cũng có "C#"
Vì vậy, để state thực sự immutable, chỉ nên dùng kiểu đơn giản hoặc collection vốn đã bất biến (ImmutableList<T> và các loại khác trong System.Collections.Immutable).
Nếu muốn đảm bảo bất biến thật sự, hãy dùng các collection này hoặc tự làm deep copy thủ công.
GO TO FULL VERSION