Chuyển tới nội dung chính

Structs and Records

Định nghĩa

  • Structkiểu giá trị (Value Type) nhẹ phù hợp cho các cấu trúc dữ liệu nhỏ có ngữ nghĩa giá trị. Chúng được cấp phát trên ngăn xếp (Stack) (trong hầu hết trường hợp) và sao chép theo giá trị.
  • Record (C# 9+) là kiểu tham chiếu hoặc kiểu giá trị cung cấp so sánh dựa trên giá trị (Value-Based Equality), cú pháp thân thiện với tính bất biến (Immutability), và đột biến không phá hủy (Non-Destructive Mutation) thông qua biểu thức with.

Khái niệm cốt lõi

Struct

Struct là kiểu giá trị. Khi bạn gán một struct, toàn bộ nội dung được sao chép.

Các kiểu nguyên thủy đều là Struct

Nhiều kiểu nguyên thủy (Primitive Types) của C# thực chất là struct bên dưới:

Từ khóa C#Kiểu thực tếPhân loại
intSystem.Int32Struct
longSystem.Int64Struct
floatSystem.SingleStruct
doubleSystem.DoubleStruct
decimalSystem.DecimalStruct
boolSystem.BooleanStruct
charSystem.CharStruct
byteSystem.ByteStruct
stringSystem.StringClass
objectSystem.ObjectClass

Đây là lý do tại sao các kiểu này có phương thức và thuộc tính (ví dụ int.MaxValue, "hello".Length).

public struct Point
{
public double X { get; }
public double Y { get; }

public Point(double x, double y) => (X, Y) = (x, y);

public double DistanceTo(Point other) =>
Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}

Readonly Struct (C# 7.2)

Đánh dấu struct là readonly đảm bảo với trình biên dịch rằng không thành viên nào thay đổi trạng thái.

public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }

public Money(decimal amount, string currency) => (Amount, Currency) = (amount, currency);

public Money Add(Money other) =>
Currency == other.Currency
? new Money(Amount + other.Amount, Currency)
: throw new InvalidOperationException("Currency mismatch");
}

Khởi tạo trường trong hàm tạo (Trước C# 11 vs C# 11+)

Trong hàm tạo struct tùy chỉnh (trước C# 11), tất cả các trường phải được gán rõ ràng — bỏ sót dù chỉ một trường sẽ gây lỗi biên dịch:

public struct Time
{
private int _hours, _minutes, _seconds;

// Pre-C# 11: must assign ALL fields, or compile error
public Time(int hours, int minutes, int seconds)
{
_hours = hours;
_minutes = minutes;
_seconds = seconds; // omitting this would cause CS0171
}
}

Từ C# 11, trình biên dịch tự động khởi tạo bất kỳ trường nào không được gán rõ ràng về giá trị mặc định của nó (0, null, v.v.) trong hàm tạo. Điều này loại bỏ nhu cầu khởi tạo 0 thủ công cho mọi trường.

Hàm tạo mặc định

Struct luôn có một hàm tạo không tham số ngầm định (ngay cả khi bạn định nghĩa các hàm tạo khác) khởi tạo 0 cho tất cả các trường. Bạn không thể định nghĩa hàm tạo không tham số riêng trước C# 10 — và ngay cả trong C# 10+, nó phải khởi tạo tất cả các trường.

Hành vi sao chép của kiểu giá trị

Vì struct là kiểu giá trị, truyền struct cho phương thức sẽ sao chép toàn bộ struct. Các thay đổi bên trong phương thức không ảnh hưởng đến bản gốc:

public struct Counter
{
public int Value { get; set; }
public Counter(int value) => Value = value;
}

var c = new Counter(10);
Console.WriteLine(c.Value); // 10

ChangeValue(c);
Console.WriteLine(c.Value); // Still 10 — struct was copied

static void ChangeValue(Counter counter) => counter.Value = 99;

Sử dụng tham số ref hoặc out nếu bạn cần phương thức thay đổi struct gốc.

Record

Record kết hợp những điểm tốt nhất của class và ngữ nghĩa giá trị — chúng sử dụng so sánh dựa trên giá trị (Value-Based Equality) (hai record bằng nhau nếu tất cả dữ liệu của chúng bằng nhau).

Record Class (C# 9+)

// Positional record — concise syntax
public record Person(string FirstName, string LastName, int Age);

// Usage
var p1 = new Person("Alice", "Smith", 30);
var p2 = new Person("Alice", "Smith", 30);

Console.WriteLine(p1 == p2); // True — value-based equality
Console.WriteLine(ReferenceEquals(p1, p2)); // False — different references

Biểu thức With (Đột biến không phá hủy)

var original = new Person("Alice", "Smith", 30);
var updated = original with { Age = 31 }; // creates a new copy with one field changed

Phân rã (Deconstruction)

var person = new Person("Alice", "Smith", 30);
var (first, last, age) = person; // deconstructs positional parameters

Record Struct (C# 10+)

public record struct Point(double X, double Y);

// Readonly record struct
public readonly record struct Money(decimal Amount, string Currency);

Ví dụ mã

Cạm bẫy của Struct có thể thay đổi

public struct BadCounter
{
public int Value;

public void Increment() => Value++; // mutates in-place
}

BadCounter c = new BadCounter();
c.Increment();
Console.WriteLine(c.Value); // 1

List<BadCounter> list = new() { new BadCounter() };
list[0].Increment(); // MODIFIES A COPY — Value stays 0!
Console.WriteLine(list[0].Value); // 0 — bug!
Struct có thể thay đổi (Mutable Structs)

Mutable struct gây ra các lỗi tinh tế khi sử dụng trong bộ sưu tập, sau giao diện, hoặc trong khối using. Luôn thiết kế struct dưới dạng bất biến (Immutable). Sử dụng readonly struct để thực thi điều này tại thời điểm biên dịch.

Record với các thành viên bổ sung

public record Student(string FirstName, string LastName, double Gpa)
{
public string FullName => $"{FirstName} {LastName}";
public virtual bool Equals(Student? other) =>
other is not null && FirstName == other.FirstName && LastName == other.LastName;
}

Hàm tạo chính cho Class (C# 12)

C# 12 giới thiệu hàm tạo chính (Primary Constructor) cho class thông thường. Khác với record, các class này không có so sánh dựa trên giá trị hoặc biểu thức with.

// Class with primary constructor — NOT a record
public class Product(string name, decimal price)
{
public string Name { get; } = name;
public decimal Price { get; } = price;

public override string ToString() => $"{Name}: {price:C}";
}

var p1 = new Product("Widget", 9.99m);
var p2 = new Product("Widget", 9.99m);
Console.WriteLine(p1 == p2); // False — reference equality, not value-based
Hàm tạo chính vs Record

Class có hàm tạo chính vẫn là class thông thường — nó sử dụng so sánh tham chiếu (Reference Equality) và không hỗ trợ biểu thức with. Sử dụng record khi bạn cần ngữ nghĩa dựa trên giá trị.

Bảng so sánh

Tính năngclassstructrecord classrecord struct
KiểuTham chiếuGiá trịTham chiếuGiá trị
So sánhTham chiếuGiá trị (bitwise)Dựa trên giá trịDựa trên giá trị
Kế thừaCó (đơn)KhôngCó (đơn)Không
Có thể thay đổiCó (nhưng nên tránh)Có (nhưng nên tránh)Có (nhưng nên tránh)
Biểu thức withKhôngKhông
Trường hợp sử dụng chínhHành vi phức tạpDữ liệu nhỏ (< 16 byte)DTO, value objectValue object nhỏ

Khi nào sử dụng loại nào

Hướng dẫn quyết định

  1. Cần kế thừa và hành vi phức tạp? Sử dụng class.
  2. Nhỏ (< 16 byte), tồn tại ngắn, không có boxing dự kiến? Sử dụng struct (ưu tiên readonly struct).
  3. Dữ liệu bất biến, so sánh dựa trên giá trị, DTO, hoặc value object DDD? Sử dụng record (record class hoặc record struct).
  4. Value object nhỏ bất biến cần with? Sử dụng readonly record struct.

Khi nào sử dụng Class

  • Kiểu đại diện cho một thực thể có định danh (ví dụ User, Order).
  • Bạn cần hệ thống phân cấp kế thừa.
  • Đối tượng lớn hoặc tồn tại lâu.
  • Ngữ nghĩa tham chiếu là mong muốn.

Khi nào sử dụng Struct

  • Kiểu nhỏ (Microsoft khuyến nghị dưới 16 byte).
  • Đại diện cho một giá trị đơn (ví dụ Point, Color, Money).
  • Tồn tại ngắn và thường được cấp phát trong phạm vi phương thức.
  • Bạn muốn tránh cấp phát heap và áp lực GC.

Khi nào sử dụng Record

  • DTO, mô hình yêu cầu/phản hồi API.
  • Value object trong DDD (Domain-Driven Design).
  • Dữ liệu bất biến cần sao chép với thay đổi nhỏ (with).
  • Bạn muốn so sánh cấu trúc (Structural Equality) sẵn dùng.

Lỗi thường gặp

  • Boxing struct — ép struct sang giao diện hoặc object cấp phát trên heap. Trong các đường dẫn nóng (Hot Paths), điều này gây áp lực GC.
  • Struct có thể thay đổi trong bộ sưu tập — thay đổi struct thông qua bộ chỉ mục bộ sưu tập chỉ thay đổi bản sao, không phải bản gốc. Luôn sử dụng readonly struct.
  • Record không phải lúc nào cũng bất biến — bạn có thể thêm bộ truy cập set hoặc trường có thể thay đổi vào record. Tính bất biến là một lựa chọn thiết kế, không bị thực thi.
  • Struct không thể kế thừa từ struct khác — chúng chỉ có thể triển khai giao diện.
  • Hàm tạo mặc định của struct — luôn khởi tạo tất cả các trường về 0/null. Bạn không thể ngăn điều này.
public struct Temperature
{
public decimal Value { get; } // default(Temperature).Value == 0, even if 0 is invalid
}

Điểm chính cần nhớ

  • Struct là kiểu giá trị — sử dụng cho dữ liệu nhỏ, bất biến khi ngữ nghĩa sao chép là mong muốn.
  • Record cung cấp so sánh dựa trên giá trị và biểu thức with — lý tưởng cho DTO và value object.
  • Ưu tiên readonly struct để đảm bảo tính bất biến của struct tại thời điểm biên dịch.
  • Hàm tạo chính C# 12 cho class không cung cấp tính năng của record (so sánh, with).
  • Giữ struct dưới 16 byte để tránh hình phạt hiệu suất do sao chép.

Câu hỏi phỏng vấn

H: Khi nào bạn nên sử dụng struct thay vì class? Đ: Khi kiểu nhỏ (dưới ~16 byte), đại diện cho một giá trị đơn, tồn tại ngắn, và bạn muốn ngữ nghĩa giá trị (sao chép khi gán). Ví dụ: Point, Color, Guid.

H: Record là gì? Đ: Record là kiểu tham chiếu (hoặc kiểu giá trị với record struct) cung cấp so sánh dựa trên giá trị (Value-Based Equality), đột biến không phá hủy thông qua biểu thức with, và cú pháp vị trí (Positional Syntax) gọn gàng cho mô hình dữ liệu bất biến.

H: So sánh dựa trên giá trị (Value-Based Equality) là gì? Đ: Hai thực thể record được coi là bằng nhau nếu tất cả các trường và thuộc tính của chúng có cùng giá trị, bất kể chúng có phải là cùng một đối tượng trong bộ nhớ hay không. Điều này khác với class, mặc định sử dụng so sánh tham chiếu (Reference Equality).

H: Biểu thức with làm gì? Đ: Nó tạo ra một bản sao nông (Shallow Copy) của record với một hoặc nhiều thuộc tính được thay đổi, để lại bản gốc không thay đổi. Đây là một dạng đột biến không phá hủy (Non-Destructive Mutation).

H: Boxing là gì và nó liên quan đến struct như thế nào? Đ: Boxing là quá trình chuyển đổi kiểu giá trị sang kiểu tham chiếu (ví dụ ép struct sang object hoặc giao diện). Điều này cấp phát một đối tượng mới trên heap và sao chép dữ liệu của struct vào đó. Boxing thường xuyên trong các đường dẫn nóng làm giảm hiệu suất do tăng áp lực GC.

Tài liệu tham khảo