1. Giới thiệu
Cú pháp positional cho record-class thực sự tiện cho mấy trường hợp đơn giản:
public record User(string Name, int Age);
Nhưng đôi khi bạn lại cực kỳ muốn thêm method cho record, property lạ, đổi access modifier, thêm logic vào constructor (kiểu validation hoặc tự động chuyển đổi dữ liệu). Tiếc là với positional record thì không có chỗ để nhét mấy cái đó! Đến lúc chuyển sang cú pháp body rõ ràng nếu:
- Bạn cần mở rộng record bằng method, property hoặc logic thêm.
- Cần kiểm soát hành vi property (setter, getter, init, validation).
- Muốn tạo nhiều constructor khác nhau cho các kiểu khởi tạo khác nhau.
- Cần thêm interface hoặc implement method đặc biệt.
Xây record có body
Cú pháp khá giống class. Bạn chắc quen kiểu này rồi:
public class User
{
/* ... */
}
Giờ thì là record nhé:
public record User
{
// Property định nghĩa rõ ràng
public string Name { get; init; }
public int Age { get; init; }
// Logic thêm
public string GetGreeting()
{
return $"Chào, mình tên là {Name} và mình {Age} tuổi!";
}
// Constructor custom
public User(string name, int age)
{
Name = name;
if (age < 0)
throw new ArgumentException("Age không thể là số âm!");
Age = age;
}
}
Fun fact: Nếu bạn tự định nghĩa property, compiler sẽ không tự tạo property từ positional syntax cho bạn đâu. Tất cả đều rõ ràng, kiểm soát hết!
Kiểu mix: cú pháp hybrid
C# cho phép bạn kết hợp cả hai kiểu: bạn có thể khai báo positional record rồi thêm body:
public record User(string Name, int Age)
{
public string GetGreeting()
{
return $"Mình là {Name}, mình {Age} tuổi!";
}
}
Trong trường hợp này, property Name và Age vẫn tự động được tạo nhờ positional syntax, còn method thêm thì nằm gọn trong body.
2. Khác biệt với record thường — các nuance của body
- record rõ ràng cho phép bạn kiểm soát hoàn toàn constructor, property, method.
- Bạn có thể implement interface hoặc thêm logic so sánh riêng nếu có rule nhận diện object phức tạp.
- Khác với record positional, muốn thêm property mới thì chỉ cần khai báo trong body record thôi.
// Lỗi newbie!
public record User(string Name, int Age)
{
public string Name { get; init; } // ← conflict! Khai báo property trùng
}
Lỗi newbie: Một số bạn sinh viên hay vừa viết public record User(string Name, int Age) vừa thêm vào body property public string Name { get; init; }, tưởng là hai biến khác nhau. Không nhé! Sẽ bị conflict (khai báo trùng). Hoặc dùng positional syntax hoàn toàn, hoặc tự định nghĩa property — đừng lẫn lộn.
Ví dụ thực tế
Mình tiếp tục phát triển app console, nơi user tạo đơn hàng. Giả sử có class đơn hàng Order, trước giờ như này:
public record Order(string Product, int Quantity, double Price);
Giờ cần validation cho quantity (quantity không được nhỏ hơn 1) và thêm property tính tổng tiền:
public record Order
{
public string Product { get; init; }
public int Quantity { get; init; }
public double Price { get; init; }
public double TotalCost => Quantity * Price;
public Order(string product, int quantity, double price)
{
Product = product ?? throw new ArgumentNullException(nameof(product));
if (quantity < 1)
throw new ArgumentException("Số lượng phải >= 1!");
Quantity = quantity;
Price = price;
}
public override string ToString()
=> $"Sản phẩm: {Product}, Số lượng: {Quantity}, Tổng: {TotalCost}";
}
Lưu ý là mình đã tự định nghĩa property, dùng init-setter (object bất biến), thêm tự động tính tổng tiền — code linh hoạt hơn hẳn!
Gọi trong chương trình:
var order = new Order("Xe đạp", 2, 15000);
Console.WriteLine(order); // Sản phẩm: Xe đạp, Số lượng: 2, Tổng: 30000
3. record struct
Tiến hóa của struct
Trước khi có record struct, struct trong C# chỉ là "con ngựa thồ đơn giản" — copy nhanh, lưu trên stack, rất hợp cho mấy kiểu dữ liệu nhỏ (như tọa độ hoặc màu sắc). Nhưng nó không có mấy thứ hay ho của record: không có positional syntax, with-expression, so sánh theo value mặc định và các “bánh ngọt” khác.
Giờ thì C# cho phép khai báo struct kiểu record luôn:
public record struct Point(int X, int Y);
Vậy nó cho mình gì?
- Tự động implement Equals, GetHashCode, ToString — struct của bạn so sánh và in ra đẹp luôn!
- Có with-clone: var p2 = p1 with { X = 10 };
- Có thể dùng positional hoặc body rõ ràng.
So sánh: struct cổ điển VS record struct
| struct | record struct | |
|---|---|---|
| Positional syntax | Không | Có |
| with-expression | Không | Có |
| Bất biến | Không (mặc định) | Có (init) |
| So sánh theo value | Không (mặc định) | Có |
| ToString | Mặc định | Đẹp |
Cú pháp body rõ ràng cho record struct
Y chang record thường, chỉ khác là struct:
public record struct Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public int Area => Width * Height;
public Rectangle(int width, int height)
{
Width = width > 0 ? width : throw new ArgumentException("Chiều rộng > 0");
Height = height > 0 ? height : throw new ArgumentException("Chiều cao > 0");
}
public void Print()
{
Console.WriteLine($"Kích thước: {Width} x {Height}, diện tích: {Area}");
}
}
Ví dụ dùng:
var rect = new Rectangle(10, 7);
rect.Print(); // Kích thước: 10 x 7, diện tích: 70
// Clone và đổi chiều rộng, không ảnh hưởng bản gốc
var wideRect = rect with { Width = 20 };
wideRect.Print(); // Kích thước: 20 x 7, diện tích: 140
Đặc điểm record struct
- Vẫn là struct — value type. Copy khi gán nhé!
- Đủ ưu điểm của record: so sánh theo value, with-clone, ToString đẹp.
- Nên dùng cho bộ dữ liệu nhỏ, gọn, bất biến, khi muốn tránh cấp phát heap.
- Có thể khai báo positional param hoặc body rõ ràng.
4. Lỗi phổ biến và bẫy
Quá mutable: record struct không tự làm field bất biến nếu bạn khai báo kiểu thường (ví dụ public int Value;). Hãy dùng init-setter cho truly immutable struct nhé!
So sánh: Nếu bạn thêm field mới thủ công (không nằm trong positional syntax), nhớ là: chỉ field trong constructor hoặc có init-setter mới được auto-so sánh theo value.
Copy: Đây là struct, nên... copy hết! Đừng nhầm với record reference type.
Nhầm lẫn with-expression: Nó luôn tạo shallow copy, tức là KHÔNG clone sâu các object lồng bên trong đâu.
GO TO FULL VERSION