1. Tại sao lại cần record
Trong C# (và .NET nói chung) từ lâu các khối xây dựng chính là class (class) và struct (struct). Nhưng mỗi loại đều không hoàn hảo cho một số task. Class là kiểu tham chiếu, có thể thay đổi, và so sánh theo tham chiếu (trừ một số trường hợp hiếm). Struct là kiểu giá trị (copy khi truyền), mặc định so sánh từng byte và thường cũng có thể thay đổi (cho đến khi có readonly struct).
Nhưng nếu bạn muốn một kiểu lưu trữ dữ liệu đơn giản, tự nhiên, dễ so sánh theo giá trị, clone nhanh và không phải lo lắng ai đó ở đâu đó thay đổi object của bạn — bạn phải tự chế hoặc dùng thư viện như ValueTuple hoặc thậm chí System.Tuple. Nhưng mấy cái đó không đẹp và không "ngầu" như mong muốn.
Vì vậy, trong C# 9 đã xuất hiện record — kiểu dữ liệu kết hợp cú pháp ngắn gọn, đảm bảo bất biến và hành vi so sánh theo giá trị.
2. Bốn kiểu trên một trang
| class | struct | record | record struct | |
|---|---|---|---|---|
| Loại | Kiểu tham chiếu | Kiểu giá trị | Kiểu tham chiếu | Kiểu giá trị |
| Có thể thay đổi | Mặc định — có | Mặc định — có | Mặc định — không (init) | Mặc định — không (init) |
| So sánh | Theo tham chiếu (==) | Theo giá trị (field) | Theo giá trị (fields/properties) | Theo giá trị (fields) |
| Clone | Chỉ thủ công | Chỉ thủ công | Có hỗ trợ sẵn (with) | Có hỗ trợ sẵn (with) |
| Kế thừa | Có | Không | Có | Không |
| Bất biến | Phải tự làm | Phải tự làm | Rất dễ làm | Rất dễ làm |
| Cú pháp | Dài nhất | Ngắn | Ngắn nhất (kiểu position) | Khá ngắn |
| Dùng trong collection | Theo tham chiếu | Bản copy | Theo tham chiếu | Bản copy |
Sơ đồ trực quan
+----------------+ +----------------+ +--------------------+
| class | | struct | | record |
+----------------+ +----------------+ +--------------------+
| Reference Type | | Value Type | | Reference Type |
| Mutable | | Mutable | | Immutable (init) |
| == : Reference | | == : By Fields | | == : By Value |
+----------------+ +----------------+ +--------------------+
3. Bản chất khác biệt giữa record, class và struct
Hành vi trong bộ nhớ: tham chiếu hay giá trị?
- class và record — là kiểu tham chiếu. Khi truyền vào hàm, chỉ copy tham chiếu đến object.
- struct và record struct — là kiểu giá trị. Luôn copy từng byte (trừ khi truyền tham chiếu rõ ràng).
class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);
void Demo()
{
var pc = new PointClass { X = 1, Y = 2 };
var ps = new PointStruct { X = 1, Y = 2 };
var pr = new PointRecord(1, 2);
var prs = new PointRecordStruct(1, 2);
ChangeY(pc); // pc.Y sẽ bị đổi!
ChangeY(ps); // ps.Y không đổi — là bản copy!
ChangeY(pr); // pr.Y sẽ bị đổi!
ChangeY(prs); // prs.Y không đổi — là bản copy!
}
void ChangeY(dynamic p) { p.Y = 99; }
Nếu bạn thấy dynamic ở đây như phép thuật — đúng rồi, chỉ để ví dụ thôi, để bạn thấy khi làm việc với struct thì giá trị không đổi, còn với class hoặc record (kiểu tham chiếu) thì sẽ đổi.
So sánh: làm sao biết hai object bằng nhau?
- class: so sánh theo tham chiếu (== — true chỉ khi là cùng một object trong bộ nhớ), trừ khi override Equals.
- struct: so sánh theo giá trị tất cả field (mặc định).
- record: so sánh theo giá trị tất cả field/property được khai báo trong constructor chính.
class Foo { public int A; public int B; }
record Bar(int A, int B);
var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);
Console.WriteLine(foo1 == foo2); // False! Khác object
Console.WriteLine(bar1 == bar2); // True! Giá trị giống nhau
Fun fact:
Với record struct còn hay hơn: so sánh "theo giá trị" như struct thường, nhưng có cú pháp và tính năng của record.
4. Bất biến: ai đảm bảo được gì?
So sánh độ an toàn của object:
- class: Mặc định dễ thay đổi, trừ khi bạn để tất cả field là readonly.
- struct: Tương tự, nhưng có thể khai báo là readonly struct, khi đó không thể đổi field/property.
- record: Thường field khai báo với init, nghĩa là chỉ set được trong constructor hoặc initializer (with). Rất tiện cho truyền dữ liệu an toàn.
- record struct: Tương tự, có thể làm readonly record struct để có bất biến kiểu giá trị với bonus của record.
record Person(string Name, int Age);
var p1 = new Person("Aleksei", 23);
// p1.Age = 24; // Lỗi! Chỉ init
var p2 = p1 with { Age = 24 }; // Ok! Tạo bản copy với dữ liệu mới
Khi bạn có project lớn và hàng chục entity-object, record giúp tránh rất nhiều bug kiểu "ai đó ở đâu đó đổi field — và mọi thứ hỏng".
5. Cú pháp: khai báo thế nào cho khỏi rối
// class
public class Product
{
public int Id { get; init; }
public string Name { get; init; }
}
// struct
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// record
public record Product(int Id, string Name);
// record struct
public record struct Point(int X, int Y);
Như bạn thấy, record-type có cú pháp ngắn nhất. Bạn có constructor, destructor, so sánh bằng, ToString() và nhiều thứ khác "out of the box".
| Cú pháp | Được tặng kèm | Có kế thừa được không |
|---|---|---|
| class | gần như không có gì | Có |
| struct | gần như không có gì | Không |
| record | ToString, Equals, Deconstruct, with | Có |
| record struct | ToString, Equals, Deconstruct, with | Không |
6. Clone và toán tử with
Chỉ record và record struct mới có toán tử with đặc biệt, cho phép copy object và đổi một số property mà không đau đầu.
record User(string Name, int Age);
var user1 = new User("Irina", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Irina", user2.Age == 29
Với class (class) bạn phải tự viết hàm copy, mà quên copy một field là bug ngay.
7. Kế thừa: ai kế thừa ai
- class: Hỗ trợ kế thừa chuẩn (hierarchy class, virtual method, abstraction, v.v.).
- struct: Không hỗ trợ (chỉ implement interface).
- record: Hỗ trợ kế thừa, nhưng có giới hạn (ví dụ chỉ kế thừa giữa record-type, không với class).
- record struct: Không hỗ trợ, giống struct thường.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // Ok!
class Vehicle { }
class Car : Vehicle { } // Ok!
// struct không thể kế thừa struct khác
Chi tiết về kế thừa sẽ có ở level sau :P
8. Ứng dụng thực tế của từng kiểu
- class: Object lớn, nhiều hành vi, vòng đời dài, trạng thái thay đổi, có hierarchy (ví dụ: logic nghiệp vụ, UI component).
- struct: Object nhỏ, giá trị, cần copy nhanh, không có GC, overhead thấp (ví dụ: tọa độ, màu sắc, số lượng — cái gì dễ clone nhanh).
- record: DTO (Data Transfer Object), object tham số, config, trạng thái bất biến, kết quả tính toán, cần so sánh theo nội dung.
- record struct: Kiểu giá trị, cần bất biến và hành vi như record, nhưng không muốn cấp phát heap.
9. Lỗi phổ biến và bẫy thường gặp
Đôi khi dev nghĩ record chỉ là "thay thế class". Không phải đâu! Nếu bạn cần object thay đổi trạng thái trong quá trình sống — dùng class.
Nếu bạn muốn so sánh object theo tham chiếu (ví dụ pattern singleton hoặc khi vòng đời object quan trọng) — dùng class.
Nếu bạn làm object-giá trị, cần hành xử như số hoặc điểm trên lưới tọa độ — struct hoặc record struct.
Nếu bạn làm việc với object bất biến, dễ so sánh, thường truyền giữa các layer, lưu trong collection, log, serialize — chọn record.
Nhớ nhé: nếu bạn khai báo record-class với property chỉ đọc, nhưng quên về object lồng bên trong — field lồng vẫn có thể bị đổi nếu nó tự thay đổi được.
record Student(string Name, List<int> Grades);
var s1 = new Student("Anton", new List<int>() {5,5,5});
var s2 = s1 with { };
s2.Grades.Add(2); // Cả hai object đều "dính" điểm 2! s1.Grades == s2.Grades
GO TO FULL VERSION