Structs and Records
Định nghĩa
- Struct là kiể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 |
|---|---|---|
int | System.Int32 | Struct |
long | System.Int64 | Struct |
float | System.Single | Struct |
double | System.Double | Struct |
decimal | System.Decimal | Struct |
bool | System.Boolean | Struct |
char | System.Char | Struct |
byte | System.Byte | Struct |
string | System.String | Class |
object | System.Object | Class |
Đâ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.
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!
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
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ăng | class | struct | record class | record struct |
|---|---|---|---|---|
| Kiểu | Tham chiếu | Giá trị | Tham chiếu | Giá trị |
| So sánh | Tham chiếu | Giá trị (bitwise) | Dựa trên giá trị | Dựa trên giá trị |
| Kế thừa | Có (đơn) | Không | Có (đơn) | Không |
| Có thể thay đổi | Có | Có (nhưng nên tr ánh) | Có (nhưng nên tránh) | Có (nhưng nên tránh) |
Biểu thức with | Không | Không | Có | Có |
| Trường hợp sử dụng chính | Hành vi phức tạp | Dữ liệu nhỏ (< 16 byte) | DTO, value object | Value object nhỏ |
Khi nào sử dụng loại nào
Hướng dẫn quyết định
- Cần kế thừa và hành vi phức tạp? Sử dụng class.
- 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). - 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 classhoặcrecord struct). - Value object nhỏ bất biến cần
with? Sử dụngreadonly 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
objectcấ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
sethoặ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.