CodeGym /Các khóa học /C# SELF /Phân biệt record với class và struct

Phân biệt record với class và struct

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

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);
So sánh cú pháp khai báo các kiểu khác nhau

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
2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Tạo record và so sánh các đối tượng
Tạo record và so sánh các đối tượng
2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sử dụng toán tử `with` để ghi dữ liệu
Sử dụng toán tử `with` để ghi dữ liệu
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Vấn đề với DTO
DTO, record và with
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION