您是否曾经需要一种方法,能够根据不同的情况返回不同类型的返回值呢?比如,支付处理程序可能需要返回多种支付方式;订单状态可能包含多种信息;或者文件加载器需要处理多种格式的文件。

在C#中,我们通常通过继承层次结构、标记接口或包装对象来解决这个问题——但这些方式都会增加代码的复杂性,同时也会降低类型的安全性。不过幸运的是,还有更好的解决方案:使用OneOf库来实现这种功能。

如果您之前使用过TypeScript进行编程,那么您可能已经熟悉了联合类型这个概念。联合类型并不是C#原生支持的功能,但未来会实现这一特性。在此之前,您可以使用OneOf库来解决问题。

在这篇文章中,我将向您展示如何使用OneOf库来实现类似F#中的判别性联合类型,从而让代码更加简洁、易读且具备更强的类型安全性,适用于各种场景——从多态的返回值到状态机,甚至优雅的错误处理。

目录

什么是OneOf?

OneOf库提供了判别性联合类型,使得您可以从一个方法中返回多个预定义的类型之一。与不同,后者将多个值组合在一起,而OneOf则表示一种选择(A或B或C)。

可以将其视为一种类型安全的方式,来表示:“该方法返回类型A、类型B或类型C中的一种”。编译器会确保您能够处理所有可能性。

// 而不是这样(无论是否需要,都返回全部类型)
public (User user, Error error) GetUser(int id) { ...  }

// 您可以这样做(只返回其中一种类型)
public OneOf<User, NotFound, DatabaseError> GetUser(int id) { ... }

这与C#中的类型有很大区别,后者可以同时存储多个值。

// Tuple:同时存储所有值(并且是一起存储的)
var tuple = ("hello", 42, true); // 包含了字符串和整数

因此,OneOf与C#中的相比,具有更高的灵活性。

为什么需要使用OneOf?

OneOf不仅方便使用,而且由编译器强制实现。当您使用OneOf时,编译器会确保您能够处理所有可能的返回值。

例如:


OneOf<Success,Failure> result = GetResult();
// 编译器会强制您处理所有情况
result.Match(
    success => HandleSuccess(success),
    failure => HandleFailure(failure),
)

这会给您带来编译时的警告,如果您在IDE或代码中悬停查看该警告,你会看到如下提示:

图像显示智能提示,提醒开发者遗漏了某个处理函数,因为只定义了两个处理函数

类型安全和全面的处理

OneOf不仅仅是方便而已,它还是由编译器强制实现的。当您使用OneOf时,编译器会确保您能够处理所有可能的返回值。

例如:


OneOf<CreditCardInfo,PayPalUser,CryptoAccount> result = GetPaymentMethod();
// MasterCard

result.Match(
    creditCard => ProcessCreditCard(creditCard),
    paypal => ProcessPayPal(paypal),
    crypto => ProcessCrypto(crypto)
)

为什么这种方式比继承更优呢?

  • 不需要额外的基类

  • 每种支付方式都可以拥有完全不同的属性

  • 可以清晰地处理每种情况

  • 易于添加新的支付方式(编译器会提示如何更新)

用例1:无需继承的多态返回类型

当您需要根据逻辑返回不同类型的数据时,就可以使用OneOf。


OneOf<CreditCardPayment, PayPalPayment, CryptoPayment> GetPaymentMethod(PaymentRequest request)
{
    return request.Method switch
    {
        "card" => new CreditCardPayment(request.CardNumber, request.CVV),
        "paypal" => new PayPalPayment(request.Email),
        "crypto" => new CryptoPayment(request.WalletAddress),
        _ => throw new ArgumentException("Unknown payment method")
    };

用例2:带有丰富数据的状态机

在工作流程中,每个状态都可能携带不同的信息。


Public Order
{
    Public OneOf<Pending, Processing, Shipped, Delivered, Cancelled> Status { Get; Set; }
}

Pending(DateTime OrderedAt);
Processing(DateTime StartedAt, string WarehouseId);
Shipped(DateTime ShippedAt, string TrackingNumber, string Carrier);
Delivered(DateTime DeliveredAt, string SignedBy);
Cancelled(DateTime CancelledAt, string Reason);

用例3:多通道通知

通过不同的渠道发送通知时,每种渠道都有自己需要的参数。


Public record EmailNotification(string To, string Subject, string Body);
Public record SmsNotification(string PhoneNumber, string Message);
Public record PushNotification(string DeviceToken, string Title, string Body);
Public record InAppNotification(int UserId, string Message);

Public async task SendNotification(
    OneOf<EmailNotification, SmsNotification, PushNotification, InAppNotification> notification)
{
    await notification.Match(
        async email => await _emailService.SendAsync(email.To, email.Subject, email.Body),
        async sms => await _smsService.SendAsync(sms.PhoneNumber, sms.Message),
        async push => await _pushService.SendAsync(push.DeviceToken, push.Title, push.Body),
        async inApp => await _notificationRepo.CreateAsync(inApp.UserId, inApp.Message)
    );

OneOf的主要优点

当有以下情况时,OneOf非常有用:

  • 需要多个有效的返回类型,而这些类型之间没有共享的基础类

  • 每种场景都需要不同的数据形态

  • 可以明确指定如何处理每种情况,从而实现类型安全

  • 适合用于领域建模,因为每个状态都携带不同的信息

  • 明确的结果,这些结果应该作为方法的签名的一部分

结论

OneOf为C#带来了判别性联合类型的强大功能,使得代码更加富有表现力且类型安全。无论是模拟支付方式、订单状态、通知渠道还是错误处理,OneOf都能提供清晰且易于维护的解决方案。

如果你喜欢这篇文章,请随时在Twitter上联系我吧。

Comments are closed.