- Đây là đề xuất đưa type unions (discriminated unions) vào C#, cho phép biểu diễn rằng một biến hoặc tham số chỉ có thể chứa một trong một số kiểu bị giới hạn; hiện ở trạng thái Proposed, còn prototype, implementation và specification đều ở trạng thái Not Started
- Các cách triển khai hiện có dựa trên hệ phân cấp kế thừa hoặc
object khó đồng thời đáp ứng tập kiểu đóng, tổ hợp các kiểu không liên quan, lưu giá trị không cần wrapper và tránh cấp phát, nên được chia thành bốn nhóm union
- Đề xuất phân biệt union class, union struct, ad hoc union và custom union; mỗi cách có các ràng buộc khác nhau về cách khai báo, việc có cấp phát hay không và khả năng tái sử dụng kiểu hiện có
- Trong
switch và pattern matching, nếu xử lý tất cả kiểu thành viên thì cung cấp tính exhaustiveness không cần default, nhưng với union struct, default có thể không tương ứng với thành viên đã khai báo nên cần cảnh báo và chỉ định rõ thành viên default
- Ad hoc union dùng cú pháp
(A or B or C) để gom các kiểu hiện có, được triển khai bằng erasure và kiểm tra runtime, đồng thời có các hạn chế như boxing kiểu giá trị, không hỗ trợ ref type và không hỗ trợ true runtime overloading
Mục tiêu và động lực của đề xuất
- Type unions for C# là đề xuất đưa type unions, tức discriminated unions, vào C#
- Trạng thái tài liệu là Proposed
- Prototype, Implementation và Specification đều được đánh dấu là Not Started
- Trong phát triển phần mềm, có những tình huống một biến không phải lúc nào cũng chứa cùng một loại, mà cần chứa một trong các kiểu liên quan bị giới hạn
- Ví dụ là trường hợp như
Customer và Supplier, chỉ chia sẻ một số thuộc tính nhưng cần thực hiện các thao tác tương tự tùy theo khác biệt
- Cách phân tán phần triển khai vào từng kiểu bằng abstract method hoặc interface chung phù hợp khi kiểu đó tồn tại vì thao tác ấy, hoặc thao tác ấy là một phần bản chất của kiểu
- Nếu kiểu có mục đích rộng hơn, việc thêm các method như vậy có thể không phù hợp
- Cũng có thể tạo kiểu cơ sở chung như
Contact bằng kế thừa, nhưng sẽ khó hoặc không phù hợp trong các trường hợp sau
- Không sở hữu định nghĩa kiểu
- Có nhiều tình huống tương tự và kế thừa chỉ giải quyết được một tình huống
- Không muốn để yêu cầu của một thao tác cụ thể rò rỉ vào định nghĩa dữ liệu
- Dùng
object có thể hoạt động, nhưng phải quản lý bảo đảm rằng chỉ giá trị hợp lệ được đưa vào bằng tài liệu và chú thích
- Cũng có thể bảo vệ bằng một hệ phân cấp wrapper hoặc kiểu aggregate tùy chỉnh, nhưng nếu có nhiều tập kiểu theo từng ngữ cảnh thì sẽ tốn thời gian và rườm rà
- Mục tiêu là để C# khai báo rằng cùng một vị trí lưu trữ một trong nhiều kiểu bị giới hạn, và để ngôn ngữ đảm nhiệm việc bảo vệ biến
Bốn nhóm union
- Vì khó đáp ứng mọi use case bằng một triển khai duy nhất, đề xuất chia thành bốn nhóm
-
Standard - union classes
- Dùng khi có thể định nghĩa union cùng với các thành viên, và có ý định sử dụng các thành viên như những class độc lập
- Nhắm tới các trường hợp việc cấp phát class không phải là vấn đề
- Ví dụ:
- Protocol, serialization, kiểu truyền dữ liệu
- Mô hình dữ liệu UI (XAML)
- Syntax tree
- Trạng thái state machine không thay đổi thường xuyên
- Các mô hình dữ liệu đa hình khác
- Giá trị được giữ lâu ở dạng union, như field hoặc property
-
Specialized - union structs
- Dùng khi cần tránh cấp phát hoặc cần dùng kiểu đặc biệt, và có thể chấp nhận một số hạn chế để đạt điều đó
- Ví dụ:
- Giá trị được cấp phát trong mảng liên tiếp
- Giá trị được ánh xạ lên block bộ nhớ (interop)
- Trạng thái state machine thay đổi thường xuyên
- Giá trị được giữ ngắn hạn ở dạng union, như đối số hoặc giá trị trả về
- Kiểu thư viện có khả năng dùng cho mục đích đặc biệt
-
Ad Hoc - ad hoc unions
- Dùng khi cần cấu thành union từ các kiểu đã tồn tại và có thể không liên quan đến nhau
- Các union được khai báo với cùng kiểu thành viên phải có thể hoán đổi cho nhau
-
Custom unions
- Cách dành cho các trường hợp không khớp tốt với các nhóm khác
- Ví dụ:
- Kiểu và hệ phân cấp hiện có khó định nghĩa lại dễ dàng
- Bố cục lưu trữ tùy chỉnh
- Hình thái và hành vi API tùy chỉnh
Standard - union classes
- union class là named type union đặt tất cả kiểu thành viên trong một khai báo self-contained
- Khai báo giống enum, nhưng khác ở chỗ mỗi thành viên là một kiểu có thể có trạng thái bằng một hoặc nhiều biến trạng thái
union U
{
A(int x, string y);
B(int z);
C;
}
- Với mỗi thành viên, chỉ có thể chỉ định tên và danh sách biến trạng thái
- Việc tạo được thực hiện bằng cách cấp phát kiểu thành viên
U u = new A(10, "ten");
- Kiểu của thành viên được tạo là
A, và khi được gán vào biến u thì được chuyển đổi thành U
- Việc phân rã được thực hiện bằng type test và pattern matching
if (u is A a) { ... }
if (u is A(var x, var y)) { ... }
if (u is A { y: var y }) { ... }
- union class được xem là exhaustive
- Trong
switch expression hoặc statement, nếu xử lý tất cả kiểu thành viên thì không cần default case
var x = u switch {
A a => a.x,
B b => b.z,
C c => 0
};
- Có thể bao gồm null bằng ký hiệu nullable chuẩn
U? u = null;
- Phần triển khai được biểu diễn bằng abstract record class và nested derived record class
[Closed]
abstract record U
{
public record A(int x, string y) : U;
public record B(int z) : U;
public record C : U { public static C Singleton = new C(); };
}
- Attribute
Closed giúp ngôn ngữ hiểu rằng đây là hệ phân cấp đóng, nơi subtype không được khai báo bên ngoài module của kiểu cơ sở
Specialized - union structs
- union struct cũng là named type union đặt tất cả kiểu thành viên trong một khai báo self-contained
- Cả union và kiểu thành viên đều là struct, có thể dùng mà không cần heap allocation
- Khai báo tương tự union class nhưng thêm từ khóa
struct
union struct U
{
A(int x, string y);
B(int z);
C;
}
- Việc tạo, phân rã, exhaustive switch và ký hiệu nullable tương tự union class
U u = new A(10, "ten");
if (u is A a) { ... }
U? u = null;
- union struct có thể chưa được gán hoặc được gán
default, dẫn đến undefined state
- Trạng thái này không tương ứng với bất kỳ kiểu thành viên đã khai báo nào
- Switch dựa vào exhaustiveness có thể phát sinh ngoại lệ runtime
U u = default;
var x = u switch
{
A a => a.x,
B b => b.z,
C c => 0
}
- Compiler tạo cảnh báo nếu
default được gán cho struct union
// warning: default not a valid state
U u = default;
- Để tránh cảnh báo, có thể khai báo trạng thái default trong union struct và liên kết nó với một kiểu thành viên cụ thể
union struct U
{
A(int x, string y);
B(int z);
C = default;
}
- Phần triển khai được biểu diễn dưới dạng một
struct có các kiểu thành viên là nested record struct, cùng API chuyển đổi giữa kiểu thành viên và aggregate union struct
- Bố cục bên trong do trình biên dịch chọn để lưu trữ hiệu quả dữ liệu của các kiểu thành viên khả dĩ
- Trình biên dịch cũng chọn tradeoff giữa tốc độ và kích thước
[Union]
struct U
{
public record struct A(int x, string y);
public record struct B(int z);
public record struct C { public static C Singleton = default; };
public static implicit operator U(A value) {...};
public static implicit operator U(B value) {...};
public static implicit operator U(C value) {...};
public static explicit operator A(U union) {...};
public static explicit operator B(U union) {...};
public static explicit operator C(U union) {...};
public bool TryGetA(out A value) {...};
public bool TryGetB(out B value) {...};
public bool TryGetC(out C value) {...};
public enum UnionKind { A = 1, B = 2, C = 3 };
public UnionKind Kind => {...};
}
- Attribute
Union xác định kiểu đó là union struct
- Union struct có trạng thái default sẽ khai báo
UnionKind tương ứng là 0
- Toàn bộ API khởi tạo của union struct hiện vẫn chưa được ghi trong tài liệu
Kiểm tra kiểu, boxing, reflection của union struct
- Khi thực hiện kiểm tra kiểu với một union struct đã biết, nó không kiểm tra kiểu của chính struct mà gọi API của union struct
u is A a
- Biểu thức trên được chuyển đổi như sau
u.TryGetA(out var a)
- Switch expression cũng được chuyển đổi sang dạng dùng
Kind và các lệnh gọi TryGetX
u.Kind switch {
U.UnionKind.A when u.TryGetA(out var a) => a.x,
U.UnionKind.B when u.TryGetB(out var b) => b.z,
U.UnionKind.C when u.TryGetC(out var c) => 0,
_ => throw ...;
}
- Boxed union struct không phải là giá trị của kiểu thành viên đã được boxing, mà là chính union struct được boxing
- Trường hợp sử dụng chính của union struct là tránh boxing, nhưng đôi khi vẫn có thể cần boxing
- Vì kiểu union struct và các thành viên được biết là có liên quan với nhau, có thể kiểm tra kiểu của boxed union struct như kiểu thành viên và unbox nó
U u = ...;
object value = u;
if (value is A a) {...}
- Đoạn mã trên được chuyển đổi như sau
if (value is A a || (value is U u && u.TryGetA(out a))) {...}
- Ngược lại, boxed member type cũng có thể được kiểm tra và unbox như union struct
A a = ...;
object value = a;
if (value is U u) {...}
- Nếu không thể biết tĩnh rằng hai phía của phép kiểm tra kiểu có liên quan đến union struct, phép kiểm tra kiểu sẽ thất bại
bool IsType(object value) => value is T;
U u = new A(...);
if (IsType(u)) {...}
- Khi dùng reflection, có thể cần chuyển đổi boxed member type
A thành boxed union struct U
- Tính năng struct union cung cấp các phương thức tiện ích để chuyển đổi giữa boxed union struct và boxed member type ở runtime
public static class TypeUnion
{
public bool TryConvert(Type unionType, object value, out object? boxedUnion);
public bool TryConvert(object value, out TUnion union);
public object? GetValue(object? boxedUnion);
}
- Union class và ad hoc union đã có dạng đúng khi dùng reflection nên không cần chuyển đổi
Ref union structs
- Union struct có modifier
ref có thể chứa refs hoặc ref structs làm biến trạng thái
ref union struct U
{
A(ref int x);
B(ReadOnlySpan y);
C;
}
- Trong trường hợp này, phần triển khai union và các kiểu thành viên có giá trị ref struct được chuyển đổi thành ref struct
ref struct U
{
public ref struct A { public ref int x; public A(ref int x) {...}; }
public ref struct B { public ReadOnlySpan y; public B(ReadOnlySpan y) {...} }
public record struct C { public static C Singleton = default; }
...
}
- Nếu C# bổ sung ref record struct type, các kiểu thành viên bị ảnh hưởng có thể tiếp tục được giữ dưới dạng record struct
Ad Hoc - ad hoc unions
- Ad hoc union là anonymous union được tạo từ các kiểu đã khai báo ở nơi khác
- Cú pháp dùng dấu ngoặc đơn và pattern syntax
or
(A or B or C)
- Để tham chiếu bằng một tên chung, dùng file hoặc global using alias
global using U = (A or B or C);
- Việc tạo được thực hiện bằng cách gán một instance của một trong các kiểu thành viên của union cho biến kiểu ad hoc union
record A(int x, string y);
record B(int z);
record C() { public static C Singleton = new C(); };
(A or B or C) u = new A(10, "ten");
- Việc phân rã được thực hiện bằng kiểm tra kiểu và pattern matching
if (u is A a) {...}
if (u is A(var x, var y)) { ... }
- Ad hoc union cũng được xem là exhaustive, nên nếu xử lý tất cả kiểu thành viên thì không cần default case
- Có thể bao gồm null bằng ký pháp nullable
(A or B)? x = null;
- Các ad hoc union có cùng kiểu thành viên được trình biên dịch hiểu là cùng một kiểu, bất kể thứ tự
(A or B) x = new A(10, "ten");
(B or A) y = x;
Gán, khả năng hoán đổi và suy luận của ad hoc union
- Ad hoc union cùng kiểu hoặc là subset có thể được gán cho ad hoc union superset mà không cần kiểm tra runtime
(A or B) x = new A(10, "ten");
(A or B or C) y = x;
- Để gán ad hoc union superset cho ad hoc union subset, cần coercion tường minh và kiểm tra runtime
(A or B or C) x = new A(10, "ten");
var y = (A or B)x;
- Nếu mọi kiểu thành viên của source union bằng hoặc là subtype của ít nhất một thành viên trong target union, có thể implicit coercion mà không cần kiểm tra runtime
(Chihuahua or Siamese) pet = ...;
(Cat or Dog) animal = pet;
- Ngay cả khi không như vậy, nếu ít nhất một trong các kiểu thành viên của source là subtype của một trong các kiểu thành viên của target, thì có thể thực hiện coercion tường minh và kiểm tra runtime
(Cat or Chihuahua) mostlyCats = ...;
(Dog or Siamese) mostlyDogs = (Dog or Siamese)mostlyCats;
- Trong việc xác định khả năng gán, các kiểu không phải ad hoc union cũng có thể được xem như ad hoc union gồm một kiểu duy nhất
- Quy tắc này cũng hoạt động với các interface đã được triển khai
- generalized coercions cũng được định nghĩa
- Nếu có implicit coercion từ một kiểu nào đó sang một trong các kiểu thành viên của union, thì giá trị của kiểu đó có thể được implicit coercion sang kiểu union
- Nếu mọi kiểu thành viên của union đều có thể implicit coercion sang một kiểu nào đó, thì giá trị union có thể được implicit coercion sang kiểu đó
- Nếu một trong các kiểu thành viên của union có thể coercion sang một kiểu nào đó, thì giá trị union có thể được explicit coercion sang kiểu đó
- Nếu mọi kiểu thành viên của source union đều có thể implicit coercion sang một trong các thành viên của target union, thì có thể implicit coercion giữa các union
- Nếu ít nhất một thành viên của source union có thể explicit coercion sang một trong các thành viên của target union, thì có thể explicit coercion giữa các union
- Vẫn cần có quy tắc về việc chọn coercion nào khi có nhiều coercion khả dụng
- Quan hệ assignability này không phải là quan hệ subtyping
- Một ad hoc union không phải là subtype của một ad hoc union khác
- Các ad hoc union có cùng kiểu thành viên có thể hoán đổi cho nhau thông qua generics và array elements
(T1 or T2)[] F(T1 v1, T2 v2) => new (T1 or T2)[] { v1, v2 };
(Dog or Cat)[] pets = F(rufus, petunia);
- Ad hoc union dùng làm generic type arguments có thể được sử dụng cho covariance và contravariance khi mọi kiểu thành viên của hai union liên quan đều có quan hệ subtype với thành viên tương ứng
- Thay vì quy tắc cụ thể, vẫn còn một ghi chú “Have Mads write this part”
- Ad hoc union hoạt động trong pattern matching tương tự
or pattern và có thể đi kèm khai báo biến
if (u is Dog or Cat) { ... }
if (u is (Dog or Cat)) { ... }
if (u is (Dog or Cat) pet) {...}
- Khi gán vào biến ad hoc union, boxing kiểu giá trị có thể xảy ra
- conditional expression và switch expression có thể suy luận ad hoc union result type từ các expression cấu thành
Dog rufus = ...;
Cat petunia = ...;
Bird polly = ...;
var u =
x == 1 ? rufus
: x == 2 ? petunia
: polly;
- Kiểu trả về của lambda expression cũng có thể được suy luận thành ad hoc union được tạo từ các kiểu trả về của lambda body
- Trong trường hợp này, boxing kiểu giá trị cũng có thể xảy ra
Triển khai và giới hạn của ad hoc union
- Ad hoc union được triển khai bằng erasure và kiểm tra runtime
(A or B) ab = new A(10, "ten");
- Đoạn code trên được chuyển đổi như sau
object ab = new A(10, "ten");
- Các assignment không thể xác định là đúng về mặt tĩnh cần được kiểm tra runtime
- Compiler tạo custom method cho mỗi ad hoc union duy nhất được dùng trong module
object value = ...;
var ab = (A or B)value;
object value = ...;
object ab = (value);
object (object? value) =>
value is A or B ? value : throw ...;
- Tại method entry, parameters không được kiểm tra
- Trong metadata, kiểu ad hoc union được mã hóa bằng custom attributes
void M((A or B) x);
void M([AdHocUnion([typeof(A), typeof(B)])] object x);
- Chi tiết của attribute vẫn chưa được chỉ định
- Vì mọi ad hoc union đều bị erase về cùng một kiểu, true runtime overloading của method có parameter là ad hoc union là không thể
public void Wash((Cat or Dog) pet) { ... }
public void Wash((Compact or Sedan) car) { ... }
- Overloading vẫn là một lĩnh vực còn đang thảo luận mở
Custom unions
- Nếu cần hành vi không thể chỉ định bằng cú pháp union class hoặc union struct, có thể tự khai báo custom class hoặc struct và để C# nhận diện nó là custom union type
- Khi triển khai union bằng class hierarchy, nếu gắn attribute
Closed thì có thể có được hành vi exhaustiveness giống như union class
[Closed]
public class U { ... }
public class A(int x, string y) : U { ... }
public class B(int z) : U { ... }
- Khi triển khai union bằng struct wrapper có specialized storage rules, nếu gắn attribute
Union và cung cấp API theo union pattern thì về mặt chức năng sẽ giống union struct
[Union]
public struct U
{
public record struct A(int x, string y);
public record struct B(int z);
public bool TryGetA(out var A a) { ... }
public bool TryGetB(out var B b) { ... }
}
- Nếu union không bao gồm member type hoặc dùng API pattern khác, có thể cung cấp API mà compiler kỳ vọng thông qua extensions
- Toàn bộ union struct API pattern vẫn chưa được chỉ định
- Ad hoc union không thể tùy biến hành vi, ngoại trừ việc sửa đổi hành vi của từng member type riêng lẻ
Common unions: Option và Result
Option là struct union tương tự các kiểu cùng tên hoặc cùng mục đích trong các ngôn ngữ khác
- Biểu thị rằng một giá trị có thể tồn tại hoặc không tồn tại
public union struct Option
{
Some(TValue value);
None = default;
}
Option x = new Some("text");
Option y = None;
if (x is Some(var value)) {...}
var v = x is Some(var value) ? value : 0;
- Kiểu
Option chưa được chỉ định đầy đủ
Result cũng là struct union tương tự các kiểu cùng tên hoặc cùng mục đích trong các ngôn ngữ khác
- Dùng để trả về kết quả thành công hoặc lỗi từ hàm
public union struct Result
{
Success(TValue value);
Failure(TError error);
}
Result x = Success("hurray!");
Result y = Failure("boo");
switch (x)
{
case Success(var value): ...;
case Failure(var error): ...;
}
- Kiểu
Result cũng chưa được chỉ định hoàn toàn
Các đề xuất liên quan
- Đề xuất này bao gồm các đề xuất được giả định là tồn tại hoặc các tính năng vẫn chưa được đề xuất
-
Closed Hierarchies
- Khi áp dụng attribute
Closed cho abstract base type, tất cả subtype trong module khai báo sẽ được khai báo là một tập subtype đóng
- Nếu khai báo subtype bên ngoài module khai báo, trình biên dịch sẽ báo lỗi
- closed hierarchy được trình biên dịch xử lý theo kiểu exhaustive; nếu tất cả subtype đều được xử lý trong switch thì không cần default case
-
Singleton values
- Một kiểu singleton có static property
Singleton có thể được truy cập ngầm định tới property đó trong non-type context và dùng như một giá trị
var x = U.C.Singleton;
- Đoạn mã trên có thể viết như sau
var x = U.C;
-
Nested Member Shorthand
- Một tên chưa được bind có thể được bind vào static member hoặc nested type của target type
Color color = Color.Red;
- Đoạn mã trên có thể viết như sau
Color color = Red;
U u = new U.A(10, "ten");
- Đoạn mã trên có thể viết như sau
U u = new A(10, "ten");
Các quyết định thiết kế và giới hạn nêu trong Q&A
- union class không nhất thiết cần thiết nếu có thể dễ dàng khai báo trực tiếp nested record hierarchy, nhưng ưu điểm là cú pháp ngắn gọn và dễ chuyển sang union struct chỉ bằng một modifier
struct
- union struct ít allocation hơn và có thể dùng nhiều loại kiểu hơn, nhưng không phải lúc nào cũng phù hợp hơn
- Dù bản thân không có allocation, nó không nhất thiết nhanh hơn
- stack footprint lớn hơn và thường bị sao chép khi assignment, pass, return
- Không dễ thay thế lẫn nhau với anonymous ad hoc union nên không thật phù hợp
- Khi ở trạng thái boxed hoặc được biểu diễn tĩnh dưới dạng generic type parameter, có vấn đề với type tests, casts và pattern matching
- union struct về nội bộ vừa là tagged union vừa là type union
- Bên trong, nó có thể phơi bày một enum property đóng vai trò tag để làm cho mã do trình biên dịch sinh ra nhanh hơn
- Trên bề mặt ngôn ngữ, nó xuất hiện như type union để có thể được xử lý bằng những cách quen thuộc như type tests, casts và pattern matching
- Trình biên dịch có thể tối ưu hóa bằng cách bỏ qua việc tạo member type khi gán ngay member type của union struct vào biến union struct
- Khi deconstruct union trực tiếp vào biến, cũng kỳ vọng có tối ưu hóa bỏ qua việc sao chép biến trạng thái union struct sang member type
- union struct không phải là base type thực sự của member type như union class
- struct không cho phép kế thừa thực sự
- Về mặt logic, nó hoạt động như base type thông qua chuyển đổi tự động, nhưng quan hệ đó không mở rộng ra toàn bộ type system và runtime
- ad hoc union hiện không thể được khai báo trực tiếp bằng tên
- Nếu muốn tránh lặp lại union dài hoặc cần tên để mô tả, phải dùng global using alias
- ad hoc union boxing các kiểu giá trị
- Nếu cần tránh boxing, phải dùng union struct
- ad hoc union không thể bao gồm ref types
- Nếu cần ref types, phải dùng union struct
- Lý do ad hoc union bị erase thành
object là vì đây là cách cải thiện giải pháp dựa trên object mà các nhà phát triển chủ yếu dùng hiện nay bằng compile-time type safety và generated validation checks
- Không thể truy cập trực tiếp property hoặc method chung của ad hoc union mà không xử lý từng type case
- Chỉ có thể truy cập giá trị sau khi chuyển đổi thành công sang từng kiểu riêng lẻ
- union types của F# tương ứng với union class và union struct trong đặc tả này, nhưng đề xuất này xem member là các kiểu của ngôn ngữ chứ không phải tag state và associated state variables
- ad hoc union tương tự type unions của Typescript
Option có thể không cần thiết nếu có thể đạt cùng mục đích bằng null và nullable reference types
- Một số nhà phát triển thích option type hơn vì muốn có ràng buộc mạnh hơn nullable types của C#
- C# hiện không đưa các monadic behaviors có thể có trong F# cho
Option và Result vào ngôn ngữ
- Nhiều use case của
Result có thể được giải quyết bằng exception handling của C#
- Tuy nhiên, khi lỗi được dự đoán và thường xảy ra trong runtime, có thể muốn tránh exception handling và buộc caller xử lý lỗi một cách tường minh
- Các kiểu tương tự
Option và Result đã có trong thư viện bên thứ ba, nhưng nhiều nhà phát triển đã yêu cầu đưa các kiểu chuẩn hóa vào runtime để phục vụ interoperability giữa các thư viện
1 bình luận
Ý kiến trên Hacker News
Đã dùng discriminated unions trong F# vài năm nên cứ nghĩ giờ C# đương nhiên cũng phải có rồi
Có thể không phải tính năng ai cũng thích, nhưng với ngôn ngữ có kiểu, một khi đã quen có algebraic data types (ADT) dưới hình thức nào đó thì thật sự rất khó quay lại ngôn ngữ không có chúng. Giờ tôi đang dùng Java và nhìn chung cũng ổn, nhưng việc phải lách bằng các lớp bọc cho những thứ trong F# chỉ cần ba dòng thì khá khó chịu
Đây là điểm cân bằng rất khéo léo vì nó mang lại nhiều lợi ích của lập trình hàm mà không bị trói vào những ràng buộc như thuần hàm, nên làm việc rất thoải mái. Dù vậy, C# cũng đang dần dần bị biến thành F# theo cách rất chậm, và nhìn điều đó cũng khá thú vị
Ít nhất là theo tiêu chuẩn của Java
Ví dụ,
SomevàNonelà các lớp dẫn xuất của lớpOption, và có thể xác nhận điều đó khi xem assembly F# bằng decompiler. Tôi không biết bây giờ còn vậy không, nhưng đề xuất C# này có vẻ khó mà chỉ tạo bằng cú pháp đường cho kế thừa, nhất là với enum ẩn danh theo cú phápA or B. Vì thế để cái này hoạt động, có lẽ CLR sẽ phải hỗ trợ enum như một khái niệm hạng nhấtVà cũng có pattern matching
Records https://en.wikipedia.org/wiki/Java_version_history#Java_16
Sealed classes https://en.wikipedia.org/wiki/Java_version_history#Java_17
Và từ gần 1 năm trước cũng đã có pattern matching https://en.wikipedia.org/wiki/Java_version_history#Java_21
Dù vậy, tôi rất đồng ý rằng phát triển với F# thật sự rất thú vị
Tôi thật sự rất mong chờ đề xuất này. Đây luôn là tính năng lớn còn thiếu mà mỗi lần nói về ưu điểm của C# tôi đều phải thêm vào như thể đang xin lỗi
Ngoài chuyện này ra, tôi khó nghĩ ra tính năng ngôn ngữ lớn nào mà C# còn thiếu. Giờ ngay cả khi cái này được thêm vào, tôi cũng mong chờ cảnh trên HN người ta vẫn sẽ đối xử với C# như thể nó vẫn là ngôn ngữ của 10 năm trước
Vì vậy nó có thể phù hợp với nhiều người hơn và không đẩy người ta ra xa như những ngôn ngữ có cá tính quá mạnh, nhưng đổi lại có thể sẽ hơi khó học lúc ban đầu
Có ai giải thích giúp vì sao lại gọi đây là type unions không? Tôi chưa từng nghe cách gọi này
Nó trông không giống union giữa các kiểu theo kiểu ALGOL68, mà giống tagged union của các ngôn ngữ họ ML hơn. Tôi cũng tự hỏi có phải đây là một trường hợp nữa các lập trình viên C# tự đặt tên khác thay vì dùng thuật ngữ sẵn có không, kiểu như
SelectMany,IEnumerableCó lẽ họ thêm từ “type” trước “union” để làm rõ đây là khái niệm liên quan đến kiểu. Còn trong tài liệu thì chính cú pháp dường như chỉ là
union. Trong FAQ có viết như sau:Q: Why are there no tagged unions?
A: Union structs are both tagged unions and type unions. Under the hood, a union struct is a tagged union, even exposing an enum property that is the tag to enable faster compiler generated code, but in the language it is presented as a type union to allow you to interact with it in familiar ways, like type tests, casts and pattern matching.
Bao gồm hệ phân cấp đóng của reference type được compiler kiểm tra khi build, tagged unions là value type được compiler xử lý để hoạt động như hệ phân cấp đóng, các kiểu do người dùng định nghĩa có triển khai tùy ý nhưng vẫn có thể nối vào cùng cơ chế của compiler, và cả ad-hoc union của các kiểu có sẵn
Dù mình đã bỏ lỡ hoàn toàn các ẩn dụ màu sắc kiểu red/blue/white/black pill, nhưng union có pattern matching đầy đủ là một trong những tính năng ngôn ngữ mà một khi đã biết thì rất khó sống thiếu
Mình chưa bao giờ cảm thấy đã hiểu trọn vẹn hàm ý của expression problem, nhưng giả thuyết hiện tại là thế này. Cách cung cấp điểm mở rộng bằng đa hình truyền thống phù hợp khi những client tương lai mà mình không biết sẽ mở rộng code; còn union có pattern matching đầy đủ thì hợp hơn với code do mình hoặc team sở hữu. Thường không phải là mình muốn người ngoài mở rộng đoạn code đó, mà là muốn cập nhật cấu trúc dữ liệu lõi theo sự thay đổi trong hiểu biết về business domain, rồi sau đó nhận được càng nhiều lỗi biên dịch càng tốt ở những chỗ mà code mệnh lệnh không còn khớp với cấu trúc domain nữa
https://deliberate-software.com/christmas-f-number-polymorph...
Với cách dùng interface/kế thừa trong hướng đối tượng, việc thêm biến thể kiểu mới vào base class hoặc interface là dễ, nhưng thêm tính năng mới thì khó vì phải triển khai cho mọi kiểu hiện có. Với cách discriminated union trong lập trình hàm, việc tạo hàm mới rồi match trên union là dễ vì compiler đảm bảo mọi trường hợp đều được xử lý; nhưng thêm biến thể kiểu mới thì khó vì phải cập nhật mọi chỗ pattern matching đầy đủ trong toàn bộ codebase. Kotlin hỗ trợ cả hai cách khá tốt nên là ví dụ phù hợp
Có phải thuật ngữ ở đây hơi lệch không? Mình nhớ là TypeScript có union types
Nhưng cái này trông giống discriminated union như trong F# hay Haskell hơn. Theo mình, điểm khác của discriminated union là có các case constructor được đặt tên
“type union” ít nhất không phải là thuật ngữ chính thức mà mình từng nghe. Có vẻ đây là cách diễn đạt phía C# tạo ra để mô tả sum type
Có thể việc trì hoãn nó đến tận bây giờ cũng có lý do. Phải biết người dùng mục tiêu là ai. Theo thời gian thì cũng quen, nhưng khi
A|B|undefinedxuất hiện quá nhiều thì đọc rất mệt. Nó cũng tạo ra sự lười biếng kiểu nhận vào một thứ rồi trả ra đại khái tổ hợp của ba thứ trở lên, và càng đi ngược lên call stack thì càng rối hơnMình thích việc C# buộc xử lý chuyện này theo cách có ràng buộc. Tuy vậy, nếu phía mong chờ tính năng này có lập luận thuyết phục thì mình sẵn sàng nghe
Mình đã dùng C# rất lâu nhưng vẫn có cảm giác đang bỏ sót điều gì đó trong đề xuất này. Có vẻ use case chưa được định nghĩa rõ; có ai cho ví dụ thực tế được không?
Ví dụ trong đề xuất có vẻ như có thể làm bằng cách khai báo một interface rỗng rồi để vài record class “implement” nó. Mình không rõ như vậy thì mất đi điều gì
Bất kỳ ai, kể cả ở ngoài code của mình, cũng có thể tạo implementation mới cho interface đó. Ngoài ra, các lựa chọn có thể chẳng chia sẻ bề mặt chung nào cả, nên “interface” của tất cả các type sẽ rỗng, điều này hơi không tự nhiên trong hướng đối tượng. Về mặt nội bộ thì cuối cùng nó vẫn là một hệ phân cấp kiểu, nhưng điểm khác cốt lõi so với cách tự triển khai là việc mở rộng bị đóng lại, và khi code dùng case mà bỏ sót case nào thì sẽ có lỗi tại thời điểm biên dịch
Lấy JSON làm ví dụ, thay vì nhét dữ liệu vào một lớp
JsonValue, bạn có thể tạo nó thành một union type gồm một trong các kiểu: chuỗi, boolean, số, mảng của union type đó, hoặc map có key là chuỗi và value là union type đó. Cũng có thể triển khai kiểu kết quả nhưResultcủa Rust, để API định nghĩa rằng nó trả về либо giá trị hoặc lỗi. Mình không thấy đề xuất có bàn đến generics hay không. Trong thế giới lập trình hàm, người ta thường gọi đây là algebraic data type, và khi đã quen với kiểu mô hình hóa type như vậy thì sẽ thật sự nhớ nó ở những ngôn ngữ không hỗ trợ[1] https://en.m.wikipedia.org/wiki/Algebraic_data_type
[0] https://github.com/mcintyre321/OneOf
Mình đã dùng nó để cho các lớp ngoài cùng trả về kiểu
Resultgói DTO, lỗi, v.v. vào một chỗ. Có thể pattern match trên kết quả nên việc xử lý lỗi dễ hơn, đồng thời model có thể tiến hóa theo thời gian mà không cần thay đổi function signature ở ranh giới hệ thốngVấn đề kiểu “không xử lý hết mọi case trong switch” xảy ra cực hiếm, khoảng một lần trên mỗi 50 nghìn dòng code, và thường được sửa sau lần chạy smoke test đầu tiên. Vì vậy nó không phải vấn đề lớn. Mình nghĩ lý do team .NET vẫn chưa thêm discriminated union cũng là vì có thể bắt chước khá hiệu quả bằng interface
Ok(T value)hoặcError(string message)Để lấy giá trị, bạn buộc phải
switchcả hai trường hợp, nên trường hợp lỗi sẽ bị ép xử lý ngay tại chỗBuồn cười là dưới mục covariance / contravariance lại ghi “Note: Have Mads write this part.” :)
Có đoạn nói rằng “bố cục nội bộ của union struct sẽ do trình biên dịch chọn điểm cân bằng giữa tốc độ và kích thước để lưu trữ dữ liệu của các kiểu thành viên khả dĩ một cách hiệu quả”
Từ góc nhìn của người từng thử quá nhiều trò "hắc ám" union trong C# bằng
FieldOffsetvà bị phản tác dụng nặng nề, ở đây có một vấn đề đáng tiếc. Việc aliasing giữa giá trị con trỏ/ref và kiểu giá trị là UB. Nghĩa là struct union củau64vàobjectsẽ cần các trường riêng biệt cho từng cái, nên lãng phí 8 byte. Vẫn sẽ như vậy trừ khiryujit/GC được cập nhật để nhận biết điều nàyTrong .NET 8.0, mã đặt
UInt64 FoovàObject Barcùng ởFieldOffset(0)vẫn biên dịch được, nhưng khi tải sẽ némSystem.TypeLoadException. Thông báo nói rằng object field ở offset 0 bị căn chỉnh sai hoặc chồng lấp với non-object field. Điều thú vị là trình biên dịch AOT cảnh báo rằng phương thức này luôn throw, còn trình biên dịch C# thì không đưa ra bất kỳ cảnh báo nàoThật đáng tiếc khi C#, thay vì trở thành một ngôn ngữ hướng đối tượng tốt hơn, lại có vẻ cứ muốn trở thành một F# xấu hơn
Ví dụ, tại sao cú pháp đa phân phối vẫn còn vụng về như vậy? Pseudo OO đã thống trị thế giới, rồi mọi người phản ứng lại với nó, nên tôi hiểu rằng để giữ tính liên quan thì việc trở thành “một ngôn ngữ hơi thiên hàm có dấu ngoặc nhọn” dễ hơn là cố làm hướng đối tượng cho thật tốt
Vậy phía hướng đối tượng đang thiếu điều gì? C# hay ngôn ngữ khác cần có gì để đạt trạng thái “làm hướng đối tượng tốt”?
Tôi muốn biết bạn đang nghĩ đến khái niệm hay tính năng nào
Hiện tại tôi đang dùng record lồng nhau có constructor private cùng với gói NuGet https://github.com/shuebner/ClosedTypeHierarchyDiagnosticSup... để đảm bảo switch theo kiểu không cần case
_Về bản chất, đây là một phiên bản desugaring của “Union Classes” trong đề xuất này, và nó đã hoạt động khá tốt. Dù vậy, sẽ tốt hơn nếu không còn cần gói NuGet và có thêm cú pháp đường nữa, nên tôi thích đề xuất này
Trình biên dịch tạo ra một constructor protected để phục vụ thao tác sao chép, và constructor đó có thể bị kế thừa. Muốn ngăn điều này thì phải tự định nghĩa constructor protected và khiến nó ném ngoại lệ lúc chạy nếu không phải là một case hợp lệ