您是否曾经需要一种方法,能够根据不同的情况返回不同类型的返回值呢?比如,支付处理程序可能需要返回多种支付方式;订单状态可能包含多种信息;或者文件加载器需要处理多种格式的文件。
在C#中,我们通常通过继承层次结构、标记接口或包装对象来解决这个问题——但这些方式都会增加代码的复杂性,同时也会降低类型的安全性。不过幸运的是,还有更好的解决方案:使用OneOf库来实现这种功能。
如果您之前使用过TypeScript进行编程,那么您可能已经熟悉了联合类型这个概念。联合类型并不是C#原生支持的功能,但未来会实现这一特性。在此之前,您可以使用OneOf库来解决问题。
在这篇文章中,我将向您展示如何使用OneOf库来实现类似F#中的判别性联合类型,从而让代码更加简洁、易读且具备更强的类型安全性,适用于各种场景——从多态的返回值到状态机,甚至优雅的错误处理。
目录
什么是OneOf?
OneOf库提供了判别性联合类型,使得您可以从一个方法中返回多个预定义的类型之一。与
可以将其视为一种类型安全的方式,来表示:“该方法返回类型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上联系我吧。

