使用C# 9.0记录来构建类似智能枚举/鉴别联合/总和类型的数据结构?

15

在C#中尝试使用record类型时,它似乎可以很好地用于构建类似于判别联合的数据结构,我只是想知道我是否遗漏了一些将来会后悔的问题。例如:

abstract record CardType{
    // Case types
    public record MaleCardType(int age) : CardType{}
    public record FemaleCardType : CardType{}

    // Api
    public static MaleCardType Male(int age) => new MaleCardType(age);
    public static FemaleCardType Female => new FemaleCardType();
}

var w = CardType.Male(42);
var x = CardType.Male(42);
var y = CardType.Male(43);
var z = CardType.Female;
Assert.Equal<CardType>(w,x); //true
Assert.Equal<CardType>(x,y); //false
Assert.Equal<CardType>(y,z); //false

看起来比构建具有单例和等式比较器的抽象类要简单得多,但我是否错过了不想这样做的原因?


1
DUs允许您基于类型编写详尽的switch表达式。您可以通过不同类型继承相同的空接口来拥有非详尽的switch,无需单例或抽象类。问题在于详尽性。这个代码如何让您编写详尽的switch表达式? - Panagiotis Kanavos
似乎你试图复制 F# 类型的构造函数。这不是让 C# DUs 工作所缺少的东西。实际上,如果你使用布尔属性(或任何类型,在编译器中已知值的情况下,如果存在这样的东西),你可以获得短整型的全面匹配。查看类似问题的答案 - Panagiotis Kanavos
1
谢谢您,@PanagiotisKanavos! 我认为我的问题不在于模式匹配方面,因为在C#中这从来都不容易,而是关于使用记录来完成这个数据结构,而不是使用类。 (是的,我正在尝试让C#像F#一样工作。不幸的是,我没有权威带领团队使用F#,但如果我能够用C#接近F#,我就会很满意!) - user2320861
2
模式匹配很容易,而且是如此新颖,以至于“从未容易”不适用。 DU 的问题在于广泛的匹配,否则您可以使用 C# 做与 F# 相同的事情。如果它们那么容易,C# 团队就不会推迟两次了。他们本可以选择 TypeScript 所做的强制人们切换标签,但那样会使 DU 非常难以使用。 - Panagiotis Kanavos
谢谢@PanagiotisKanavos,我很感激您的评论。 - user2320861
挺有趣的,但也很有限。比如说,你会怎么对这样的结构进行序列化呢? - Alexandre Daubricourt
2个回答

14

这是一个很好的方式,我一直在尝试,例如在 https://fsharpforfunandprofit.com/posts/designing-for-correctness/ 上有一些C#代码的示例、使用了类型和区分联合的一些F#代码,还有一些修改后(但仍然糟糕的)C#代码。所以我使用C# 9的记录和同样的方式重写了C#代码。

示例代码比F#略丑一点,但仍然相当简洁,并具有与F#代码相同的优点。

using System;
using System.Collections.Immutable;

namespace ConsoleDU
{
    record CartItem(string Value);

    record Payment(decimal Amount);

    abstract record Cart
    {
        public record Empty () : Cart
        {
            public new static Active Add(CartItem item) => new(ImmutableList.Create(item));
        }
        public record Active (ImmutableList<CartItem> UnpaidItems) : Cart
        {
            public new Active Add(CartItem item) => this with {UnpaidItems = UnpaidItems.Add(item)};
            public new Cart Remove(CartItem item) => this with {UnpaidItems = UnpaidItems.Remove(item)} switch
            {
                var (items) when items.IsEmpty => new Empty(),
                { } active => active
            };

            public new Cart Pay(decimal amount) => new PaidFor(UnpaidItems, new(amount));
        }
        public record PaidFor (ImmutableList<CartItem> PaidItems, Payment Payment) : Cart;

        public Cart Display()
        {
            Console.WriteLine(this switch
            {
                Empty => "Cart is Empty",
                Active cart => $"Cart has {cart.UnpaidItems.Count} items",
                PaidFor(var items, var payment) => $"Cart has {items.Count} paid items. Amount paid: {payment.Amount}",
                _ => "Unknown"
            });
            return this;
        }

        public Cart Add(CartItem item) => this switch
        {
            Empty => Empty.Add(item),
            Active state => state.Add(item),
            _ => this
        };

        public static Cart NewCart => new Empty();

        public Cart Remove(CartItem item) => this switch
        {
            Active state => state.Remove(item),
            _ => this
        };

        public Cart Pay(decimal amount) => this switch
        {
            Active cart => cart.Pay(amount),
            _ => this
        };
    }

    class Program
    {
        static void Main(string[] args)
        {
            Cart.NewCart
                .Display()
                .Add(new("apple"))
                .Add(new("orange"))
                .Display()
                .Remove(new("orange"))
                .Display()
                .Remove(new("apple"))
                .Display()
                .Add(new("orange"))
                .Pay(23M)
                .Display();
            ;
        }
    }
}

关于内存分配呢?[StructLayout(LayoutKind.Explicit)] 属性给你一个类似于 C++ 联合体的联合。 - ATL_DEV

6
record PaymentType{
    public record CreditCard(CardNumber CardNumber, SecurityCode CVV, Expiration ExpirationDate, NameOnCard Name) : PaymentType();
    public record ACH(AccountNumber AccountNumber, RoutingNumber RoutingNumber) : PaymentType();
    public record Paypal(IntentToken Token) : PaymentType();

    private PaymentType(){} // private constructor can prevent derived cases from being defined elsewhere
}

public void HandlePayment(PaymentType paymentInfo){
    paymentInfo switch {
        CreditCard cardInfo => //...
        ACH checkInfo => //...
        Paypal paypalInfo => //...
    };
}

Link to source article


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接