1 คะแนน โดย GN⁺ 2024-08-09 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เป็นข้อเสนอเพื่อนำ type unions (discriminated unions) เข้ามาใน C# เพื่อแสดงว่าตัวแปรหรือพารามิเตอร์หนึ่งตัวสามารถเก็บได้เพียงหนึ่งในหลายชนิดข้อมูลที่ถูกจำกัดไว้เท่านั้น โดยปัจจุบันอยู่ในสถานะ Proposed และ prototype·implementation·specification อยู่ในสถานะ Not Started
  • การใช้งานด้วยลำดับชั้น inheritance เดิมหรือแบบอิง object ทำให้ตอบโจทย์เรื่องชุดชนิดข้อมูลแบบปิด, การรวมชนิดข้อมูลที่ไม่เกี่ยวข้องกัน, การเก็บค่าโดยไม่มี wrapper และการหลีกเลี่ยง allocation พร้อมกันได้ยาก จึงแบ่งออกเป็น union สี่หมวดหมู่
  • ข้อเสนอนี้แยกระหว่าง union class, union struct, ad hoc union และ custom union โดยแต่ละวิธีมีข้อจำกัดต่างกันในด้านวิธีประกาศ, การมี allocation และความสามารถในการนำชนิดข้อมูลเดิมมาใช้ซ้ำ
  • ใน switch และ pattern matching หากจัดการชนิดสมาชิกครบทั้งหมด จะให้ exhaustiveness ที่ไม่จำเป็นต้องมี default แต่ union struct อาจมี default ที่ไม่สอดคล้องกับสมาชิกที่ประกาศไว้ จึงต้องมีคำเตือนและการระบุสมาชิก default อย่างชัดเจน
  • ad hoc union ใช้ไวยากรณ์ (A or B or C) เพื่อรวมชนิดข้อมูลเดิม และใช้งานด้วย erasure กับการตรวจสอบ runtime โดยมีข้อจำกัดเช่น boxing ของ value type, ไม่รองรับ ref type และไม่สามารถทำ true runtime overloading ได้

วัตถุประสงค์และแรงจูงใจของข้อเสนอ

  • Type unions for C# คือข้อเสนอให้นำ type unions หรือ discriminated unions เข้ามาใน C#
    • สถานะเอกสารคือ Proposed
    • Prototype, Implementation, Specification ทั้งหมดถูกระบุว่า Not Started
  • ในการพัฒนาซอฟต์แวร์ มีสถานการณ์ที่ตัวแปรหนึ่งตัวไม่ได้เก็บชนิดเดียวกันทุกครั้ง แต่ต้องเก็บหนึ่งในกลุ่มชนิดข้อมูลที่เกี่ยวข้องกันแบบจำกัด
    • ตัวอย่างคือกรณีอย่าง Customer และ Supplier ที่แชร์เพียงบางคุณสมบัติ แต่ต้องทำงานคล้ายกันตามความแตกต่างของแต่ละชนิด
  • วิธีที่กระจาย implementation ไปยังแต่ละชนิดผ่าน abstract method หรือ interface ร่วม เหมาะเมื่อชนิดนั้นมีอยู่เพื่อการทำงานดังกล่าว หรือเมื่องานนั้นเป็นส่วนสำคัญโดยธรรมชาติของชนิดข้อมูล
    • หากชนิดข้อมูลมีวัตถุประสงค์กว้างกว่านั้น การเพิ่ม method ดังกล่าวอาจไม่ใช่สิ่งที่พึงประสงค์
  • สามารถสร้างชนิดฐานร่วมอย่าง Contact ด้วย inheritance ได้ แต่ในกรณีต่อไปนี้อาจทำได้ยากหรือไม่เหมาะสม
    • เมื่อไม่ได้เป็นเจ้าของนิยามชนิดข้อมูล
    • เมื่อมีสถานการณ์คล้ายกันจำนวนมากจน inheritance แก้ได้เพียงแบบเดียว
    • เมื่อไม่ต้องการให้ข้อกำหนดของงานเฉพาะรั่วไหลเข้าไปในนิยามข้อมูล
  • การใช้ object สามารถทำให้ใช้งานได้ แต่ต้องจัดการการรับประกันว่ามีเฉพาะค่าที่ถูกต้องเข้ามาด้วยเอกสารและคอมเมนต์
    • อาจป้องกันด้วยลำดับชั้น wrapper หรือ custom aggregate type ได้เช่นกัน แต่หากมีชุดชนิดข้อมูลตามสถานการณ์จำนวนมาก จะใช้เวลามากและยุ่งยาก
  • เป้าหมายคือให้ C# สามารถประกาศได้ว่าให้เก็บ หนึ่งในหลายชนิดข้อมูลที่จำกัดไว้ ในตำแหน่งเดียวกัน และให้ภาษาเป็นผู้ดูแลการปกป้องตัวแปร

union สี่หมวดหมู่

  • เนื่องจากยากที่จะตอบโจทย์ use case ทั้งหมดด้วย implementation เดียว จึงแบ่งเป็น สี่หมวดหมู่
  • Standard - union classes

    • ใช้เมื่อสามารถนิยาม union และสมาชิกไว้ด้วยกันได้ และตั้งใจให้สมาชิกถูกใช้เหมือน class อิสระ
    • มุ่งใช้ในกรณีที่ class allocation ไม่เป็นปัญหา
    • ตัวอย่าง:
      • protocol, serialization, ชนิดข้อมูลสำหรับส่งถ่ายข้อมูล
      • โมเดลข้อมูล UI (XAML)
      • syntax tree
      • สถานะของ state machine ที่ไม่ได้เปลี่ยนบ่อย
      • โมเดลข้อมูลเชิง polymorphic อื่น ๆ
      • ค่าที่คงอยู่ในรูปแบบ union เป็นเวลานาน เช่น field หรือ property
  • Specialized - union structs

    • ใช้เมื่อจำเป็นต้องหลีกเลี่ยง allocation หรือต้องใช้ชนิดพิเศษ และสามารถยอมรับข้อจำกัดบางอย่างเพื่อสิ่งนั้นได้
    • ตัวอย่าง:
      • ค่าที่ถูกจัดสรรในอาร์เรย์ต่อเนื่อง
      • ค่าที่แมปอยู่บนบล็อกหน่วยความจำ (interop)
      • สถานะของ state machine ที่เปลี่ยนบ่อย
      • ค่าที่คงอยู่ในรูปแบบ union ช่วงสั้น ๆ เช่น argument หรือ return value
      • ชนิดข้อมูลของไลบรารีที่มีความเป็นไปได้ในการใช้งานแบบพิเศษ
  • Ad Hoc - ad hoc unions

    • ใช้เมื่อต้องประกอบ union จากชนิดข้อมูลที่มีอยู่แล้วและอาจไม่เกี่ยวข้องกัน
    • union ที่ประกาศด้วยชนิดสมาชิกเดียวกันควรแลกเปลี่ยนกันได้
  • Custom unions

    • เป็นวิธีสำหรับกรณีที่ไม่เข้ากับหมวดหมู่อื่น ๆ
    • ตัวอย่าง:
      • ชนิดข้อมูลและลำดับชั้นเดิมที่นิยามใหม่ได้ยาก
      • layout ของ storage แบบกำหนดเอง
      • รูปแบบและพฤติกรรมของ API แบบกำหนดเอง

Standard - union classes

  • union class คือ named type union ที่วางชนิดสมาชิกทั้งหมดไว้ในประกาศแบบ self-contained เดียว
  • การประกาศคล้าย enum แต่ต่างกันตรงที่สมาชิกแต่ละตัวเป็นชนิดข้อมูลที่มีสถานะได้ด้วยตัวแปรสถานะตั้งแต่หนึ่งตัวขึ้นไป
union U
{
    A(int x, string y);
    B(int z);
    C;
}
  • แต่ละสมาชิกสามารถระบุได้เฉพาะชื่อและรายการตัวแปรสถานะ
  • การสร้างทำโดย allocate ชนิดสมาชิก
U u = new A(10, "ten");
  • ชนิดของสมาชิกที่สร้างขึ้นคือ A และเมื่อกำหนดให้ตัวแปร u จะถูกแปลงเป็น U
  • การแตกค่าออกทำด้วย type test และ pattern matching
if (u is A a) { ... }

if (u is A(var x, var y)) { ... }

if (u is A { y: var y }) { ... }
  • union class ถือว่าเป็น exhaustive
    • ใน switch expression หรือ statement หากจัดการชนิดสมาชิกครบทั้งหมด ก็ไม่จำเป็นต้องมี default case
var x = u switch {
    A a => a.x,
    B b => b.z,
    C c => 0
    };
  • สามารถรวม null ได้ด้วยสัญลักษณ์ nullable มาตรฐาน
U? u = null;
  • implementation แสดงแทนด้วย abstract record class และ 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 ทำให้ภาษาเข้าใจว่านี่คือ ลำดับชั้นแบบปิด ซึ่งไม่มีการประกาศ subtype นอกโมดูลของชนิดฐาน

Specialized - union structs

  • union struct ก็เป็น named type union ที่วางชนิดสมาชิกทั้งหมดไว้ในประกาศแบบ self-contained เดียวเช่นกัน
    • ทั้ง union และชนิดสมาชิกเป็น struct จึงใช้ได้โดยไม่มี heap allocation
  • การประกาศคล้าย union class แต่เพิ่มคีย์เวิร์ด struct
union struct U
{
    A(int x, string y);
    B(int z);
    C;
}
  • การสร้าง, การแตกค่า, exhaustive switch และสัญลักษณ์ nullable คล้ายกับ union class
U u = new A(10, "ten");

if (u is A a) { ... }

U? u = null;
  • union struct อาจยังไม่ได้ถูกกำหนดค่า หรือถูกกำหนดเป็น default จนกลายเป็น undefined state ได้
    • สถานะนี้ไม่สอดคล้องกับชนิดสมาชิกใด ๆ ที่ประกาศไว้
    • อาจเกิด runtime exception ใน switch ที่พึ่งพา exhaustiveness
U u = default;

var x = u switch
{
    A a => a.x,
    B b => b.z,
    C c => 0
}
  • คอมไพเลอร์จะสร้างคำเตือนเมื่อมีการกำหนด default ให้ struct union
// warning: default not a valid state
U u = default;
  • หากต้องการหลีกเลี่ยงคำเตือน สามารถประกาศสถานะ default ใน union struct เพื่อเชื่อมโยงกับชนิดสมาชิกเฉพาะได้
union struct U
{
    A(int x, string y);
    B(int z);
    C = default;
}
  • การติดตั้งใช้งานจะแทนด้วย struct ที่มีชนิดสมาชิกเป็น nested record struct และมี API สำหรับแปลงระหว่างชนิดสมาชิกกับ aggregate union struct
    • คอมไพเลอร์จะเลือกเลย์เอาต์ภายในเพื่อจัดเก็บข้อมูลของชนิดสมาชิกที่เป็นไปได้อย่างมีประสิทธิภาพ
    • คอมไพเลอร์จะเป็นผู้เลือก tradeoff ระหว่างความเร็วกับขนาดด้วย
[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 ใช้ระบุว่าชนิดดังกล่าวเป็น union struct
  • union struct ที่มีสถานะ default จะประกาศ UnionKind ที่สอดคล้องกันเป็น 0
  • API การสร้างทั้งหมดของ union struct ยังไม่ได้แสดงไว้ในเอกสาร

การทดสอบชนิด, boxing, reflection ของ union struct

  • เมื่อทำการทดสอบชนิดกับ union struct ที่รู้จัก จะเรียก union struct API แทนที่จะตรวจสอบชนิดของ struct เอง
u is A a
  • นิพจน์ข้างต้นจะถูกแปลงเป็นดังนี้
u.TryGetA(out var a)
  • switch expression ก็จะถูกแปลงเป็นรูปแบบที่ใช้ Kind และการเรียก 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 ไม่ใช่ค่าชนิดสมาชิกที่ถูก boxing แต่เป็น ตัว union struct เองที่ถูก boxing
    • use case หลักของ union struct คือหลีกเลี่ยง boxing แต่บางครั้งก็อาจจำเป็นต้อง boxing
  • เนื่องจากเป็นที่ทราบว่า union struct type และสมาชิกมีความเกี่ยวข้องกัน จึงสามารถทดสอบชนิด boxed union struct เป็นชนิดสมาชิกและ unbox ได้
U u = ...;
object value = u;

if (value is A a) {...}
  • โค้ดข้างต้นจะถูกแปลงเป็นดังนี้
if (value is A a || (value is U u && u.TryGetA(out a))) {...}
  • ในทางกลับกัน boxed member type ก็สามารถทดสอบและ unbox เป็น union struct ได้
A a = ...;
object value = a;

if (value is U u) {...}
  • หากไม่สามารถรู้ได้แบบ static ว่าทั้งสองฝั่งของการทดสอบชนิดเกี่ยวข้องกับ union struct การทดสอบชนิดจะล้มเหลว
bool IsType(object value) => value is T;

U u = new A(...);

if (IsType(u)) {...}
  • เมื่อใช้ reflection อาจจำเป็นต้องแปลง boxed member type A เป็น boxed union struct U
  • ฟีเจอร์ struct union มีเมธอดยูทิลิตีสำหรับแปลงระหว่าง boxed union struct กับ 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 และ ad hoc union อยู่ในรูปแบบที่ถูกต้องอยู่แล้วสำหรับการใช้ reflection จึงไม่ต้องแปลง

Ref union structs

  • union struct ที่มี modifier ref สามารถมี refs หรือ ref structs เป็นตัวแปรสถานะได้
ref union struct U
{
    A(ref int x);
    B(ReadOnlySpan y);
    C;
}
  • ในกรณีนี้ การติดตั้งใช้งาน union และชนิดสมาชิกที่มีค่า ref struct จะถูกแปลงเป็น 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; }
    ...
}
  • หากมีการเพิ่ม ref record struct type ใน C# ชนิดสมาชิกที่ได้รับผลกระทบอาจยังคงเป็น record struct ต่อไปได้

Ad Hoc - ad hoc unions

  • Ad hoc union คือ anonymous union ที่สร้างจากชนิดต่าง ๆ ที่ประกาศไว้ที่อื่น
  • ไวยากรณ์ใช้วงเล็บและ pattern syntax or
(A or B or C)
  • หากต้องการอ้างอิงด้วยชื่อร่วม ให้ใช้ file หรือ global using alias
global using U = (A or B or C);
  • การสร้างทำโดยกำหนดอินสแตนซ์ของหนึ่งในชนิดสมาชิกของ union ให้กับตัวแปรชนิด 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");
  • การแยกออกทำโดยใช้การทดสอบชนิดและ pattern matching
if (u is A a) {...}

if (u is A(var x, var y)) { ... }
  • ad hoc union ก็ถือว่า exhaustive เช่นกัน ดังนั้นหากจัดการชนิดสมาชิกทั้งหมดแล้วก็ไม่จำเป็นต้องมี default case
  • สามารถรวม null ได้ด้วย notation แบบ nullable
(A or B)? x = null;
  • ad hoc union ที่มีชนิดสมาชิกเหมือนกัน คอมไพเลอร์จะเข้าใจว่าเป็นชนิดเดียวกันโดยไม่ขึ้นกับลำดับ
(A or B) x = new A(10, "ten");
(B or A) y = x;

การกำหนดค่า, ความสามารถในการใช้แทนกันได้, และการอนุมานของ ad hoc union

  • ad hoc union ที่เป็นชนิดเดียวกันหรือเป็น subset สามารถกำหนดค่าให้กับ superset ad hoc union ได้โดยไม่ต้องตรวจสอบใน runtime
(A or B) x = new A(10, "ten");
(A or B or C) y = x;
  • หากต้องการกำหนดค่า superset ad hoc union ให้กับ subset ad hoc union ต้องใช้ explicit coercion และการตรวจสอบใน runtime
(A or B or C) x = new A(10, "ten");
var y = (A or B)x;
  • หากชนิดสมาชิกทั้งหมดของ source union เหมือนกับหรือเป็น subtype ของสมาชิกอย่างน้อยหนึ่งชนิดใน target union จะสามารถทำ implicit coercion ได้โดยไม่ต้องตรวจสอบใน runtime
(Chihuahua or Siamese) pet = ...;
(Cat or Dog) animal = pet;
  • แม้จะไม่เป็นเช่นนั้น หาก type ของสมาชิก source อย่างน้อยหนึ่งตัวเป็น subtype ของ type สมาชิก target ตัวใดตัวหนึ่ง ก็สามารถทำ explicit coercion และตรวจสอบตอน runtime ได้
(Cat or Chihuahua) mostlyCats = ...;
(Dog or Siamese) mostlyDogs = (Dog or Siamese)mostlyCats;
  • type ที่ไม่ใช่ ad hoc union ก็สามารถมองเป็น ad hoc union แบบ type เดี่ยวได้ในการพิจารณาว่าสามารถ assign ได้หรือไม่
    • กฎนี้ทำงานกับ interface ที่ implement ไว้ด้วย
  • มีการนิยาม generalized coercions ด้วย
    • หากมี implicit coercion จาก type ใด type หนึ่งไปยัง type สมาชิกตัวใดตัวหนึ่งของ union ก็สามารถ implicit coercion ค่าของ type นั้นไปเป็น union type ได้
    • หาก type สมาชิกทั้งหมดของ union สามารถ implicit coercion ไปยัง type ใด type หนึ่งได้ ก็สามารถ implicit coercion ค่า union ไปเป็น type นั้นได้
    • หาก type สมาชิกตัวใดตัวหนึ่งของ union สามารถ coercion ไปยัง type ใด type หนึ่งได้ ก็สามารถ explicit coercion ค่า union ไปเป็น type นั้นได้
    • หาก type สมาชิกทั้งหมดของ source union สามารถ implicit coercion ไปยังสมาชิกตัวใดตัวหนึ่งของ target union ได้ ก็สามารถทำ implicit coercion ระหว่าง union ได้
    • หากสมาชิกอย่างน้อยหนึ่งตัวของ source union สามารถ explicit coercion ไปยังสมาชิกตัวใดตัวหนึ่งของ target union ได้ ก็สามารถทำ explicit coercion ระหว่าง union ได้
  • ยังต้องมีกฎสำหรับเลือกว่าจะใช้ coercion ใดเมื่อมี coercion ได้หลายแบบ
  • ความสัมพันธ์ด้าน assignability นี้ไม่ใช่ความสัมพันธ์แบบ subtyping
    • ad hoc union หนึ่งไม่ใช่ subtype ของ ad hoc union อีกตัวหนึ่ง
  • ad hoc union ที่มี type สมาชิกเหมือนกันสามารถใช้แทนกันได้ผ่าน generics และ 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 ที่ใช้เป็น generic type arguments สามารถใช้กับ covariance และ contravariance ได้ เมื่อ type สมาชิกทั้งหมดของ union สองตัวที่เกี่ยวข้องมีความสัมพันธ์แบบ subtype กับสมาชิกฝั่งตรงข้าม
    • แทนที่จะมีกฎที่ชัดเจน ยังมี note ทิ้งไว้ว่า “ให้ Mads เขียนส่วนนี้”
  • ad hoc union ทำงานคล้ายกับ pattern or ใน pattern matching และสามารถมีการประกาศตัวแปรร่วมด้วยได้
if (u is Dog or Cat) { ... }

if (u is (Dog or Cat)) { ... }

if (u is (Dog or Cat) pet) {...}
  • เมื่อ assign ให้ตัวแปร ad hoc union อาจเกิด boxing ของ value type ได้
  • conditional expression และ switch expression สามารถ infer ad hoc union result type จาก expression ที่ประกอบกันได้
Dog rufus = ...;
Cat petunia = ...;
Bird polly = ...;

var u =
      x == 1 ? rufus
    : x == 2 ? petunia
    : polly;
  • return type ของ lambda expression ก็สามารถ infer เป็น ad hoc union ที่สร้างจาก return type ต่าง ๆ ของ lambda body ได้
    • กรณีนี้ก็อาจเกิด boxing ของ value type ได้เช่นกัน

การ implement และข้อจำกัดของ ad hoc union

  • ad hoc union ถูก implement ด้วย erasure และการตรวจสอบตอน runtime
(A or B) ab = new A(10, "ten");
  • โค้ดด้านบนจะถูกแปลงดังนี้
object ab = new A(10, "ten");
  • assignment ที่ไม่สามารถรู้ได้แน่ชัดว่าถูกต้องแบบ static ต้องมีการตรวจสอบตอน runtime
    • compiler จะสร้าง custom method สำหรับ ad hoc union เฉพาะแต่ละตัวที่ใช้ในโมดูล
object value = ...;
var ab = (A or B)value;
  • ตัวอย่างการแปลงเป็นดังนี้
object value = ...;
object ab = (value);

object (object? value) =>
    value is A or B ? value : throw ...;
  • ที่ method entry จะไม่มีการตรวจสอบ parameters
  • ใน metadata จะใช้ custom attributes เพื่อ encode type ของ ad hoc union
void M((A or B) x);
  • ตัวอย่างการแปลงเป็นดังนี้
void M([AdHocUnion([typeof(A), typeof(B)])] object x);
  • รายละเอียดของ attribute ยังไม่ได้กำหนด
  • เนื่องจาก ad hoc union ทั้งหมดถูก erase เป็น type เดียวกัน จึงไม่สามารถทำ true runtime overloading ของ method ที่มี ad hoc union parameter ได้
public void Wash((Cat or Dog) pet) { ... }
public void Wash((Compact or Sedan) car) { ... }
  • overloading ยังเป็นประเด็นที่เปิดให้ถกเถียงต่อไป

Custom unions

  • หากต้องการพฤติกรรมที่ระบุด้วยไวยากรณ์ union class หรือ union struct ไม่ได้ สามารถประกาศ custom class หรือ struct เอง แล้วให้ C# รับรู้สิ่งนั้นเป็น custom union type ได้
  • หาก implement union ด้วย class hierarchy เมื่อใส่ attribute Closed จะได้พฤติกรรมด้าน exhaustiveness แบบเดียวกับ union class
[Closed]
public class U { ... }
public class A(int x, string y) : U { ... }
public class B(int z) : U { ... }
  • หาก implement union ด้วย struct wrapper ที่มี specialized storage rules เมื่อใส่ attribute Union และ提供 API ที่เป็นไปตาม union pattern ก็จะมีฟังก์ชันเทียบเท่ากับ 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) { ... }
}
  • หาก union ไม่ได้รวม member type หรือใช้ API pattern อื่น สามารถ提供 API ที่ compiler คาดหวังผ่าน extensions ได้
  • pattern ของ union struct API ทั้งหมดยังไม่ได้กำหนด
  • ad hoc union ไม่สามารถ custom พฤติกรรมได้ นอกจากการแก้ไขพฤติกรรมของ member type แต่ละตัว

Common unions: Option และ Result

  • Option เป็น struct union ที่คล้ายกับ type ชื่อเดียวกันหรือ type ที่มีจุดประสงค์เดียวกันในภาษาอื่น
    • ใช้แสดงว่าค่าอาจมีอยู่หรืออาจไม่มีอยู่ก็ได้
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;
  • type Option ยังไม่ได้กำหนดไว้อย่างสมบูรณ์
  • Result ก็เป็น struct union ที่คล้ายกับ type ชื่อเดียวกันหรือมีจุดประสงค์เดียวกันในภาษาอื่น
    • ใช้สำหรับคืนค่าผลลัพธ์ที่สำเร็จหรือข้อผิดพลาดจากฟังก์ชัน
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): ...;
}
  • type Result ก็ยังไม่ได้ระบุไว้อย่างสมบูรณ์

ข้อเสนอที่เกี่ยวข้อง

  • ข้อเสนอนี้รวมถึงข้อเสนอที่ถือว่ามีอยู่แล้ว หรือฟีเจอร์ที่ยังจะถูกเสนอในอนาคต
  • Closed Hierarchies

    • เมื่อใช้ attribute Closed กับ abstract base type จะประกาศ subtype ทั้งหมดภายในโมดูลที่ประกาศให้เป็นชุด subtype แบบปิด
    • หากประกาศ subtype นอกโมดูลที่ประกาศ จะเกิดข้อผิดพลาดจากคอมไพเลอร์
    • closed hierarchy จะถูกคอมไพเลอร์จัดการแบบ exhaustive และหากจัดการ subtype ทั้งหมดใน switch แล้ว ก็ไม่จำเป็นต้องมี default case
  • Singleton values

    • singleton type ที่มี static property ชื่อ Singleton สามารถเข้าถึง property ดังกล่าวโดยนัยใน non-type context และใช้เหมือนเป็นค่าได้
var x = U.C.Singleton;
  • โค้ดข้างต้นสามารถเขียนได้ดังนี้
var x = U.C;
  • Nested Member Shorthand

    • ชื่อที่ยังไม่ได้ bind สามารถ bind เข้ากับ static member หรือ nested type ของ target type ได้
Color color = Color.Red;
  • โค้ดข้างต้นสามารถเขียนได้ดังนี้
Color color = Red;
U u = new U.A(10, "ten");
  • โค้ดข้างต้นสามารถเขียนได้ดังนี้
U u = new A(10, "ten");

การตัดสินใจด้านการออกแบบและข้อจำกัดที่ระบุใน Q&A

  • union class อาจไม่จำเป็นเสมอไป หากสามารถประกาศ nested record hierarchy ได้ง่ายโดยตรง แต่มีข้อดีคือไวยากรณ์ที่กระชับ และสามารถเปลี่ยนเป็น union struct ได้ง่ายด้วย modifier struct เพียงตัวเดียว
  • union struct มีการจัดสรรน้อยกว่าและใช้ชนิดของ type ได้หลากหลายกว่า แต่ไม่ได้เหมาะกว่าในทุกกรณี
    • แม้จะไม่มี allocation ในตัวเอง ก็ไม่ได้หมายความว่าจะเร็วกว่าเสมอไป
    • stack footprint ใหญ่กว่า และมักถูกคัดลอกเมื่อ assignment, pass และ return
    • ไม่เหมาะกับ anonymous ad hoc union เพราะไม่สามารถสลับใช้แทนกันได้ง่าย
    • มีปัญหากับ type tests, casts และ pattern matching เมื่ออยู่ในสถานะ boxed หรือเมื่อถูกแสดงแบบ static เป็น generic type parameter
  • union struct ภายในเป็น tagged union และเป็น type union ด้วย
    • ภายในสามารถเปิดเผย enum property ที่ทำหน้าที่เป็น tag เพื่อให้โค้ดที่คอมไพเลอร์สร้างทำงานได้เร็วขึ้น
    • บนผิวหน้าของภาษา จะดูเหมือน type union เพื่อให้จัดการได้ด้วยวิธีที่คุ้นเคย เช่น type tests, casts และ pattern matching
  • คอมไพเลอร์สามารถทำ optimization โดยข้ามการสร้าง member type ได้ ในกรณีที่กำหนด member type ให้กับตัวแปร union struct ทันที
  • เมื่อ deconstruct union ลงในตัวแปรโดยตรง คาดว่าจะมี optimization ที่ข้ามการคัดลอกตัวแปรสถานะของ union struct ไปเป็น member type ด้วย
  • union struct ไม่ใช่ base type จริงของ member type เหมือน union class
    • struct ไม่อนุญาตให้มีการสืบทอดจริง
    • ในเชิงตรรกะจะทำงานเหมือน base type ผ่านการแปลงอัตโนมัติ แต่ความสัมพันธ์นั้นไม่ได้ขยายไปทั่วทั้ง type system และ runtime
  • ปัจจุบัน ad hoc union ไม่สามารถประกาศโดยตรงด้วยชื่อได้
    • หากต้องการหลีกเลี่ยงการเขียน union ยาว ๆ ซ้ำ หรือจำเป็นต้องมีชื่อเพื่อการอธิบาย ควรใช้ global using alias
  • ad hoc union จะ boxing value type
    • หากต้องการหลีกเลี่ยง boxing ต้องใช้ union struct
  • ad hoc union ไม่สามารถรวม ref types ได้
    • หากต้องการ ref types ต้องใช้ union struct
  • เหตุผลที่ ad hoc union ถูก erase เป็น object คือเป็นแนวทางที่ปรับปรุงวิธีแก้ปัญหาบนฐาน object ที่นักพัฒนาใช้กันมากในปัจจุบัน ด้วย compile-time type safety และ generated validation checks
  • property หรือ method ร่วมของ ad hoc union ไม่สามารถเข้าถึงโดยตรงได้โดยไม่จัดการแต่ละ type case
    • ต้องแปลงเป็น type รายตัวให้สำเร็จก่อน จึงจะเข้าถึงค่าได้
  • union types ของ F# สอดคล้องกับ union class และ union struct ในสเปกนี้ แต่ข้อเสนอนี้มอง member เป็น type ของภาษา ไม่ใช่ tag state และ associated state variables
    • ad hoc union คล้ายกับ type unions ของ Typescript
  • Option อาจไม่จำเป็น หาก null และ nullable reference types สามารถบรรลุวัตถุประสงค์เดียวกันได้
    • นักพัฒนาบางส่วนชอบ option type เพราะให้การบังคับที่เข้มงวดกว่า nullable types ของ C#
  • ปัจจุบัน C# ยังไม่รวม monadic behaviors สำหรับ Option และ Result แบบที่ทำได้ใน F# ไว้ในภาษา
  • use case จำนวนมากของ Result สามารถแก้ได้ด้วย exception handling ของ C#
    • อย่างไรก็ตาม เมื่อคาดว่าจะเกิดข้อผิดพลาดใน runtime และเกิดขึ้นบ่อย อาจต้องการหลีกเลี่ยง exception handling และให้ผู้เรียกจัดการข้อผิดพลาดอย่างชัดเจน
  • type ที่คล้ายกับ Option และ Result มีอยู่แล้วในไลบรารีของบุคคลที่สาม แต่มีนักพัฒนาหลายคนร้องขอให้รวม type ที่เป็นมาตรฐานไว้ใน runtime เพื่อ interoperability ระหว่างไลบรารี

1 ความคิดเห็น

 
GN⁺ 2024-08-09
ความคิดเห็นจาก Hacker News
  • ผมเคยใช้ discriminated union ใน F# มาหลายปี เลยนึกไปว่าเดี๋ยวนี้ C# ก็น่าจะมีเป็นเรื่องปกติแล้ว
    อาจไม่ใช่ฟีเจอร์ที่ทุกคนชอบ แต่สำหรับภาษาแบบมี type แล้ว พอเคยใช้ algebraic data type (ADT) ในรูปแบบใดรูปแบบหนึ่ง ก็ยากจริง ๆ ที่จะกลับไปใช้ภาษาที่ไม่มี ตอนนี้ผมใช้ Java อยู่และโดยรวมก็โอเค แต่บางครั้งก็รู้สึกรำคาญไม่น้อยเวลาต้องเลี่ยงไปใช้ wrapper class กับงานที่ใน F# เขียนแค่สามบรรทัดก็จบ

    • F# ถ้าคุ้นเคยแล้วจะย้อนกลับไปได้ยากมาก อยากให้ Microsoft สนับสนุนและผลักดันให้จริงจังกว่านี้
      มันเป็น จุดสมดุลที่ลงตัว ที่ได้ข้อดีของ functional programming มามาก โดยไม่ถูกผูกมัดกับข้อจำกัดอย่าง pure function ทำให้พัฒนาได้สบาย แต่ในอีกด้าน C# ก็ค่อย ๆ ถูกเปลี่ยนให้เหมือน F# อย่างช้ามาก ๆ ซึ่งดูแล้วก็รู้สึกแปลกดี
    • ข่าวดีก็คือ ทุกวันนี้ Java ก็ทำได้ด้วย sealed interface และ record โดยใช้ boilerplate ค่อนข้างน้อย
      แน่นอนว่านี่พูดตามมาตรฐานของ Java
    • เท่าที่จำได้ตอนเลิกใช้ F# กับ .NET ในปี 2020 ใน CLR มีแค่ inheritance และ enum ของ F# จริง ๆ แล้วก็ ถูก implement ด้วย inheritance
      เช่น Some กับ None เป็น derived class ของคลาส Option และถ้าเปิด assembly ของ F# ด้วย decompiler ก็จะเห็นได้ ไม่รู้ว่าตอนนี้ยังเหมือนเดิมไหม แต่ proposal ของ C# นี้ดูเหมือนจะทำได้ยากถ้าเป็นแค่ syntactic sugar บน inheritance รวมถึง anonymous enum ใน syntax แบบ A or B ด้วย ดังนั้นถ้าจะให้สิ่งนี้ทำงานได้ CLR น่าจะต้องรองรับ enum แบบ first-class
    • อ้างอิงไว้ว่า Java มี tagged union type ตั้งแต่เวอร์ชัน 16
      และมี pattern matching ด้วย
    • Java มี ADT มาหลายปีแล้ว ดังนั้นประโยคที่ว่า “ต้องเลี่ยงไปใช้ wrapper class กับงานที่สามบรรทัดก็จบ” จึงฟังดูแปลกนิดหน่อย
      Records https://en.wikipedia.org/wiki/Java_version_history#Java_16
      Sealed classes https://en.wikipedia.org/wiki/Java_version_history#Java_17
      และมี pattern matching มาเกือบ 1 ปีแล้วด้วย https://en.wikipedia.org/wiki/Java_version_history#Java_21
      แต่ก็เห็นด้วยอย่างมากว่าเขียน F# แล้วสนุก
  • คาดหวังกับ proposal นี้มาก นี่คือ ฟีเจอร์ใหญ่ที่สุดที่ขาดไป ซึ่งเวลาพูดถึงข้อดีของ C# ผมต้องคอยเสริมแบบเหมือนขอโทษอยู่เสมอ
    นอกจากเรื่องนี้แล้ว นึกฟีเจอร์ใหญ่ ๆ ของภาษาที่ยังขาดใน C# ไม่ค่อยออก ตอนนี้ต่อให้สิ่งนี้ถูกใส่เข้ามาแล้ว ก็คงยังได้เห็นคนใน HN ปฏิบัติกับ C# เหมือนเป็นภาษาเดิมเมื่อ 10 ปีก่อนอยู่ดี ซึ่งก็เป็นอะไรที่น่าตั้งตารอเหมือนกัน

    • ถ้ามี type alias ที่ proper ด้วยก็คงดี แต่ทุกวันนี้การเขียน C# สนุกจริง ๆ
    • ไม่ว่าจะดีหรือร้าย เป้าหมายการออกแบบของ C# ดูเหมือนจะเป็นการใส่ฟีเจอร์ทุกอย่างที่เป็นไปได้ แถมยังใส่มาหลาย variant ด้วย
      เลยปรับเข้ากับใครก็ได้ และไม่ผลักคนออกไปเหมือนภาษาที่มีจุดยืนแรง ๆ แต่สำหรับคนเริ่มเรียนอาจยากขึ้นเล็กน้อย
  • อธิบายได้ไหมว่าทำไมถึงเรียกสิ่งนี้ว่า type unions? ผมไม่เคยได้ยินชื่อนี้มาก่อน
    มันไม่ได้เป็น union ระหว่าง type แบบ ALGOL68 แต่ดูเหมือน tagged union ในภาษาตระกูล ML มากกว่า เลยสงสัยว่าเป็นอีกกรณีที่นักพัฒนา C# สร้างชื่อใหม่มาใช้แทนคำศัพท์เดิมหรือเปล่า เหมือน SelectMany, IEnumerable อะไรพวกนั้น

    • “tagged” ฟังเหมือนเป็นรายละเอียดการ implement ว่าข้างในมี tag สำหรับแยก type
      คงใส่คำว่า “type” ไว้หน้า “union” เพื่อทำให้ชัดว่าเป็นเรื่องของ type ในเอกสาร syntax เองดูเหมือนจะเป็นแค่ union ส่วน FAQ เขียนไว้แบบนี้:
      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.
    • เพราะ proposal นี้พูดถึง union หลายชนิด
      ครอบคลุมตั้งแต่ลำดับชั้นแบบปิดของ reference type ที่ compiler ตรวจสอบตอน build, tagged union ซึ่งเป็น value type ที่ compiler จัดการให้ทำงานเหมือนลำดับชั้นแบบปิด, type ที่ผู้ใช้กำหนดเองซึ่งมี implementation ใดก็ได้แต่ยังเชื่อมเข้ากับกลไก compiler เดียวกันได้, ไปจนถึง ad-hoc union ของ type ที่มีอยู่แล้ว
  • แม้จะพลาดมุกอุปมาสีอย่าง red/blue/white/black pill ไปทั้งหมด แต่ union ที่มี pattern matching แบบครอบคลุมทุกกรณี เป็นหนึ่งในฟีเจอร์ของภาษาที่พอรู้จักแล้วจะอยู่แบบไม่มีมันได้ยากที่สุด
    ผมไม่เคยรู้สึกว่าเข้าใจนัยของ expression problem ได้อย่างสมบูรณ์ แต่สมมติฐานปัจจุบันคือแบบนี้ วิธีที่ให้จุดขยายด้วย polymorphism แบบดั้งเดิมเหมาะกับเวลาที่ไคลเอนต์ในอนาคตซึ่งผมไม่รู้จักจะมาขยายโค้ด ส่วน union ที่มี pattern matching แบบครอบคลุมทุกกรณีเหมาะกับโค้ดที่ผมหรือทีมเป็นเจ้าของมากกว่า โดยปกติไม่ได้อยากให้ภายนอกมาขยายโค้ดนั้นเท่าไร แต่อยากอัปเดตโครงสร้างข้อมูลหลักให้สอดคล้องกับความเข้าใจ business domain ที่เปลี่ยนไป แล้วให้ compiler error ช่วยจับจุดที่โค้ดเชิง imperative ไม่สอดคล้องกับโครงสร้างโดเมนอีกต่อไปให้ได้มากที่สุด

    • ผมเขียนบทความนี้เพื่อพยายามทำความเข้าใจความต่าง
      https://deliberate-software.com/christmas-f-number-polymorph...
    • ผมคิดว่าแก่นของ expression problem อธิบายได้ด้วยการเปรียบเทียบวิธีที่ OOP กับ functional programming ใช้เพื่อให้โค้ดขยายได้
      ในแนวทาง interface/inheritance ของ OOP การเพิ่มชนิดย่อยใหม่ของ base class หรือ interface ทำได้ง่าย แต่การเพิ่มฟีเจอร์ใหม่ทำได้ยากเพราะต้อง implement ในทุก type เดิม ส่วนในแนวทาง discriminated union ของ functional programming การเพิ่มฟีเจอร์ใหม่ทำได้ง่าย เพราะสร้างฟังก์ชันใหม่แล้ว match กับ union และ compiler จะรับประกันว่าจัดการครบทุกกรณี แต่ถ้าเพิ่มชนิดย่อยใหม่ก็ยาก เพราะต้องอัปเดต pattern matching แบบครอบคลุมทุกกรณีทั่วทั้ง codebase Kotlin รองรับทั้งสองแนวทางได้ค่อนข้างดี จึงเป็นตัวอย่างที่ดี
  • คำศัพท์มันคลาดเคลื่อนนิดหน่อยหรือเปล่า? เท่าที่รู้ TypeScript มี union types
    แต่สิ่งนี้ดูเหมือน discriminated union ที่เคยเห็นใน F# หรือ Haskell มากกว่า ผมมองว่าความต่างของ discriminated union คือมี case constructor ที่มีชื่อ

    • TypeScript มี “union types” และข้อเสนอนี้ดูเหมือนจะเรียกมันว่า ad hoc unions
      “type union” อย่างน้อยก็ไม่ใช่คำทางการที่ผมเคยได้ยิน ดูเหมือนเป็นคำที่ฝั่ง C# สร้างขึ้นเพื่ออธิบาย sum type
    • พอย้ายจาก C# ไปใช้ type union ของ TypeScript แล้วรู้สึกแปลกอยู่เสมอ
      อาจมีเหตุผลที่ดีที่เลื่อนเรื่องนี้มาจนถึงตอนนี้ เพราะต้องรู้ว่าผู้ใช้เป้าหมายคือใคร เมื่อเวลาผ่านไปก็ชินได้ แต่ถ้าเห็น A|B|undefined มากเกินไปก็อ่านเหนื่อย อีกอย่างคือมันทำให้เกิดความขี้เกียจแบบรับอย่างหนึ่งแล้วส่งคืนแบบรวมกันสามอย่างขึ้นไปอย่างลวก ๆ และยิ่งไล่ขึ้นไปตาม call stack ก็ยิ่งสับสนมากขึ้น
      ผมชอบที่ C# จะจัดการเรื่องนี้ใน รูปแบบที่มีข้อจำกัด แต่ถ้าฝ่ายที่คาดหวังฟีเจอร์นี้มีเหตุผลที่โน้มน้าวได้ก็อยากฟัง
  • ใช้ C# มานาน แต่รู้สึกเหมือนพลาดอะไรบางอย่างในข้อเสนอนี้ไป use case ดูยังไม่ชัดเจน ช่วยยกตัวอย่างที่ใช้ได้จริงได้ไหม?
    ตัวอย่างในข้อเสนอดูเหมือนจะทำได้ด้วยการประกาศ interface เปล่า ๆ แล้วให้ record class หลายตัว “implement” มัน ผมไม่ค่อยเข้าใจว่าทำแบบนั้นแล้วเสียอะไรไป

    • hierarchy แบบนั้นเป็น open hierarchy ในความหมายที่ว่าเวลา process มัน เราไม่รู้ว่าจัดการครบทุกกรณีแล้วหรือไม่
      ใครก็ได้ แม้แต่นอกโค้ดของเรา ก็สร้าง implementation ใหม่ของ interface ได้ อีกทั้งตัวเลือกต่าง ๆ อาจไม่มี surface ร่วมกันเลย ทำให้ “interface” ของทุก type ว่างเปล่า ซึ่งใน OOP ค่อนข้างไม่เป็นธรรมชาติ ภายในสุดท้ายก็ยังเป็น type hierarchy แต่ความต่างหลักจากการ implement เองคือการขยายถูกปิดไว้ และถ้ามี case ที่ตกหล่นตอนโค้ดใช้ case จะเกิด compile-time error
    • ตัวอย่างที่สมจริงที่สุดคือ AST หรือ data protocol
      ถ้ายก JSON เป็นตัวอย่าง แทนที่จะใส่ข้อมูลทั้งหมดไว้ในคลาส JsonValue เดียว ก็ทำให้เป็น union type ที่เป็นหนึ่งใน string, boolean, number, array ของ union type, หรือ map ที่ใช้ key เป็น string และ value เป็น union type แบบเดียวกันได้ นอกจากนี้ยัง implement result type แบบ Result ของ Rust ได้ ทำให้กำหนดได้ว่า API คืนค่าหรือ error ผมไม่เห็นว่าข้อเสนอพูดถึง generics หรือไม่ ในโลก functional programming มักเรียกสิ่งนี้ว่า algebraic data type และเมื่อคุ้นกับการทำ type modeling แบบนี้แล้ว ในภาษาที่ไม่รองรับจะคิดถึงมันมากจริง ๆ
      [1] https://en.m.wikipedia.org/wiki/Algebraic_data_type
    • ไลบรารีนี้ implement discriminated union สไตล์ F#
      [0] https://github.com/mcintyre321/OneOf
      เคยใช้ตอนให้ layer ด้านนอกคืนค่าเป็น type Result ที่ห่อ DTO, error ฯลฯ ไว้รวมกัน การทำ pattern matching กับผลลัพธ์ได้ช่วยให้จัดการ error ง่ายขึ้น และทำให้ model วิวัฒน์ไปตามเวลาได้โดยไม่ต้องเปลี่ยน signature ของฟังก์ชันตาม boundary
    • ตลอดกว่า 5 ปีที่ผ่านมา ผมเลียนแบบ discriminated union ของ F# ด้วย interface เปล่า ๆ และมันทำงานได้ดีมาก
      ปัญหา “ไม่ได้จัดการทุก case ใน switch” เกิดขึ้นน้อยมาก ประมาณหนึ่งครั้งต่อโค้ด 50,000 บรรทัด และมักแก้หลังรัน smoke test ครั้งแรก ดังนั้นจึงไม่ใช่ปัญหาใหญ่ ผมคิดว่าเหตุผลที่ทีม .NET ยังไม่เพิ่ม discriminated union ก็เพราะสามารถเลียนแบบได้อย่างมีประสิทธิภาพด้วย interface
    • ตัวอย่างง่าย ๆ คือ Result ที่อาจเป็น Ok(T value) หรือ Error(string message) อย่างใดอย่างหนึ่ง
      ถ้าต้องการเอาค่าออกมา ก็ต้อง switch ทั้งสองกรณี จึงถูกบังคับให้จัดการกรณี error ตรงจุดนั้น
  • ใต้หัวข้อ covariance / contravariance มีข้อความว่า “Note: Have Mads write this part.” เลยขำ :)

    • Mads ในที่นี้หมายถึง Mads Torgersen หัวหน้าผู้ออกแบบ C#
  • มีส่วนที่ระบุว่า “เลย์เอาต์ภายในของ union struct จะให้คอมไพเลอร์เลือกจุดประนีประนอมระหว่างความเร็วกับขนาด เพื่อจัดเก็บข้อมูลในชนิดสมาชิกที่เป็นไปได้อย่างมีประสิทธิภาพ”
    จากมุมของคนที่เคยพยายามใช้มนตร์ดำทำ union ใน C# ด้วย FieldOffset มากเกินไปในอดีตจนเจ็บตัวหนัก ตรงนี้มีปัญหาที่น่าเสียดายอยู่ การทำ aliasing ระหว่างค่า pointer/ref กับ value type เป็น UB กล่าวคือ struct union ของ u64 กับ object จำเป็นต้องมีฟิลด์แยกกัน ทำให้เสียเปล่าไป 8 ไบต์ จะเป็นเช่นนั้นตราบใดที่ ryujit/GC ยังไม่ได้ถูกอัปเดตให้รู้เรื่องนี้

    • ไม่ใช่ UB แต่เป็นสิ่งที่ ผิดกฎหมาย ตาม ECMA 335, II.10.7 แม้จะสามารถทำให้ฟิลด์ซ้อนทับกันแบบนี้ได้ แต่ออฟเซ็ตที่ object reference ครอบครองต้องไม่ทับกับออฟเซ็ตที่ built-in value type หรือส่วนหนึ่งของ object reference อื่นครอบครองอยู่
      ใน .NET 8.0 โค้ดที่วาง UInt64 Foo กับ Object Bar ไว้ด้วยกันที่ FieldOffset(0) จะคอมไพล์ได้ แต่จะโยน System.TypeLoadException ตอนโหลด ข้อความมีเนื้อหาว่า object field ที่ offset 0 จัดแนวไม่ถูกต้องหรือซ้อนทับกับ non-object field อย่างน่าสนใจคือ AOT compiler จะเตือนว่าเมธอดนี้จะ throw เสมอ แต่ C# compiler ไม่เตือนอะไรเลย
  • น่าเสียดายที่แทนที่ C# จะกลายเป็นภาษาเชิงวัตถุที่ดีกว่าเดิม กลับดูเหมือนพยายามจะกลายเป็น F# เวอร์ชันที่หน้าตาขี้เหร่ลงเรื่อย ๆ
    ตัวอย่างเช่น ทำไมไวยากรณ์ของ multiple dispatch ถึงยังเทอะทะแบบนี้อยู่? pseudo OO ครองโลกไปแล้ว และผู้คนก็ต่อต้านมัน ผมเข้าใจนะว่าการกลายเป็น “ภาษาเชิงฟังก์ชันนิดหน่อยที่มีวงเล็บปีกกา” นั้นรักษาความเกี่ยวข้องไว้ได้ง่ายกว่าการพยายามทำ object-oriented ให้ดีจริง ๆ

    • อีกฝั่งก็มีความไม่พอใจแบบเดียวกัน ภาษากระแสหลักเมื่อก่อนมีสถานะที่เปลี่ยนแปลงได้อย่างกว้างขวางและ side effect อยู่แล้ว และตอนนี้ก็แค่เพิ่ม lambda เข้าไปเท่านั้น
      แล้วฝั่ง object-oriented ขาดอะไรไป? ถ้าจะให้ C# หรือภาษาอื่น ๆ อยู่ในสถานะที่ “ทำ object-oriented ได้ดี” ต้องมีอะไรบ้าง?
    • ในที่นี้ “object-oriented ที่ดีกว่า” หมายถึงอะไร?
      อยากรู้ว่ามีแนวคิดหรือฟีเจอร์อะไรอยู่ในใจ
  • ตอนนี้ใช้ nested record ที่มี private constructor ร่วมกับแพ็กเกจ NuGet https://github.com/shuebner/ClosedTypeHierarchyDiagnosticSup... เพื่อรับประกันว่าไม่ต้องมี case _ ในชนิด switch
    โดยสาระสำคัญแล้วนี่คือเวอร์ชัน desugaring ของ “Union Classes” ในข้อเสนอนี้ และใช้งานได้ค่อนข้างดีอยู่แล้ว ถึงอย่างนั้นถ้าไม่ต้องใช้แพ็กเกจ NuGet และมี syntax sugar เพิ่มมาก็ดี ดังนั้นจึงชอบข้อเสนอนี้

    • ต้องระวังว่า record type แม้จะมี private constructor ก็ไม่ได้ปิดสนิท
      คอมไพเลอร์จะสร้าง protected constructor สำหรับงานคัดลอก และ constructor นั้นสามารถถูกสืบทอดได้ หากต้องการป้องกันเรื่องนี้ ต้องนิยาม protected constructor เอง และให้โยน runtime exception ถ้าไม่ใช่ case ที่ถูกต้อง