Một vấn đề nghiêm trọng với C# records đã xuất hiện và khiến ngay cả những lập trình viên có kinh nghiệm cũng bị bất ngờ. Vấn đề xảy ra khi sử dụng toán tử with
cùng với computed properties, dẫn đến sự không nhất quán trong dữ liệu có thể khó gỡ lỗi.
Vấn đề Cốt Lõi với Nondestructive Mutation
C# records đã giới thiệu toán tử with
cho nondestructive mutation, cho phép các lập trình viên tạo ra các instance mới với các giá trị thuộc tính đã được sửa đổi. Tuy nhiên, toán tử này không hoạt động như nhiều lập trình viên mong đợi. Thay vì gọi constructor với các giá trị mới, nó thực hiện sao chép bộ nhớ của record gốc và sau đó trực tiếp thiết lập các trường được chỉ định. Cách tiếp cận này bỏ qua các tính toán thuộc tính tại thời điểm khởi tạo.
Khi một record chứa computed properties mà các giá trị của chúng được tính toán từ các trường khác trong quá trình khởi tạo, việc sử dụng with
sẽ tạo ra các đối tượng không nhất quán. Ví dụ, một record tính toán xem một số là chẵn hay lẻ trong quá trình construction sẽ giữ lại giá trị computed cũ ngay cả khi số cơ bản thay đổi thông qua thao tác with
.
Computed properties: Các thuộc tính tính toán giá trị của chúng dựa trên dữ liệu khác trong quá trình tạo đối tượng, thay vì được thiết lập trực tiếp.
Mã Code Minh Họa Vấn Đề:
public sealed record Number(int Value)
{
public bool Even { get; } = (Value & 1) == 0;
}
var n2 = new Number(2); // Even = True (đúng)
var n3 = n2 with { Value = 3 }; // Even = True (sai, phải là False)
Cuộc Tranh Luận Trong Cộng Đồng Về Hành Vi Mong Đợi
Cộng đồng phát triển vẫn còn chia rẽ về việc liệu đây có phải là một bug hay là hành vi mong đợi. Một số người cho rằng việc triển khai hiện tại tuân theo đặc tả ngôn ngữ và ưu tiên hiệu suất thông qua việc sao chép bộ nhớ. Những người khác lại cho rằng nó vi phạm nguyên tắc ít bất ngờ nhất, nơi các lập trình viên tự nhiên mong đợi các đối tượng mới có computed properties được khởi tạo đúng cách.
Cuộc thảo luận đã tiết lộ rằng vấn đề này ảnh hưởng đến nhiều lập trình viên đã phát hiện ra nó một cách độc lập, cho thấy nó đại diện cho một vấn đề khả năng sử dụng thực sự chứ không phải là một trường hợp đặc biệt.
Các Tùy Chọn Workaround Hạn Chế
Các lập trình viên gặp phải vấn đề này có ít giải pháp thỏa đáng. Cách tiếp cận đơn giản nhất là tránh các thao tác with
trên records chứa computed properties. Các giải pháp thay thế bao gồm viết các Roslyn analyzers tùy chỉnh để phát hiện các mẫu sử dụng có vấn đề hoặc triển khai các scheme lazy initialization phức tạp.
Tuy nhiên, những workaround này thêm vào sự phức tạp đáng kể và chi phí bảo trì. Cách tiếp cận lazy initialization yêu cầu quản lý thủ công các giá trị computed, trong khi các giải pháp dựa trên analyzer chỉ hoạt động trong các môi trường phát triển cụ thể.
Nó thực sự dẫn đến các cấu trúc dữ liệu không nhất quán, đó là nơi các bug tồn tại (và các vấn đề bảo mật tiềm ẩn trong trường hợp tệ nhất).
Các Tùy Chọn Giải Pháp Thay Thế Có Sẵn:
- Tùy chọn 1: Tránh sử dụng toán tử
with
cho các record có thuộc tính được tính toán - Tùy chọn 2: Viết trình phân tích Roslyn để phát hiện các mẫu sử dụng có vấn đề
- Tùy chọn 3: Triển khai khởi tạo trễ với các lớp wrapper
Lazy<T>
- Tùy chọn 4: Yêu cầu thay đổi đặc tả ngôn ngữ (giải pháp dài hạn)
Sự Đánh Đổi Giữa Hiệu Suất và Tính Đúng Đắn
Việc triển khai hiện tại ưu tiên hiệu suất bằng cách sử dụng sao chép bộ nhớ thay vì tái tạo đối tượng hoàn toàn. Lựa chọn thiết kế này phản ánh các paradigm lập trình cũ nơi tối ưu hóa từng microsecond được ưu tiên hơn trải nghiệm lập trình viên và tính nhất quán của dữ liệu.
Thiết kế ngôn ngữ hiện đại thường ưu tiên tính đúng đắn theo mặc định, với các tối ưu hóa hiệu suất có sẵn như các opt-in rõ ràng. Quyết định của nhóm C# ưu tiên tốc độ hơn tính nhất quán đã tạo ra tình huống mà records có thể chứa các kết hợp dữ liệu không thể về mặt logic.
Vấn đề này làm nổi bật những căng thẳng rộng lớn hơn trong thiết kế ngôn ngữ giữa khả năng tương thích ngược, hiệu suất và kỳ vọng của lập trình viên. Mặc dù hành vi này về mặt kỹ thuật tuân theo đặc tả, nó tạo ra một khoảng cách đáng kể giữa cách tính năng này dường như hoạt động và cách nó thực sự hoạt động.
Tình huống này phục vụ như một lời nhắc nhở rằng ngay cả các tính năng ngôn ngữ được thiết lập tốt cũng có thể chứa những gotcha tinh tế ảnh hưởng đến các ứng dụng thực tế. Các lập trình viên làm việc với C# records nên cân nhắc cẩn thận xem các trường hợp sử dụng của họ có liên quan đến computed properties hay không trước khi dựa vào toán tử with
một cách nặng nề.
Tham khảo: UNEXPECTED INCONSISTENCY IN RECORDS