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

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

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

在这篇文章中,我将向您展示如何使用OneOf来创建类似于F#中的判别性联合类型,从而在各种场景中编写更简洁、更具表达力的代码——从多态的返回类型到状态机,再到优雅的错误处理机制。

目录

什么是OneOf?

OneOf包为C#提供了判别性联合类型,使得您可以从一个方法中返回多个预定义的类型之一。与不同,后者将多个值组合在一起,而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?

  • 类型安全性

  • 自文档化功能

  • 不需要继承关系

  • 模式匹配

  • 灵活性

安装OneOf

使用终端导航到项目文件夹,然后运行以下命令:

dotnet add package OneOf

选项2:

使用您的IDE(Visual Studio、Rider或VS Code):

  1. 右键点击项目文件

  2. 选择“管理NuGet包”

  3. 搜索“OneOf”

  4. 点击“安装”

核心概念与功能

要充分利用OneOf库并了解其真正的好处,您需要理解几个核心概念。

联合类型:众多选项中的一种

本质上,OneOf代表一种联合类型。它可以在任何时候只包含几种预定义类型中的一种。可以将其视为一种类型安全的容器,但它可以容纳您指定的任何类型。

// 这个变量可以保存字符串、整数或布尔值中的一种
// 但是一次只能保存其中一种
OneOf<string, int, bool> myValue;

myValue = "hello";         // 当前保存的是字符串
myValue = 42;          // 现在保存的是整数
myValue = true;        // 现在保存的是布尔值

这与C#中的类型完全不同,后者可以同时存储多个值。

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

类型安全性和全面的处理

OneOf不仅方便使用,而且由编译器强制执行。当您使用OneOf值时,编译器会确保您处理了所有可能的类型。这避免了因为忽略某些情况而导致的错误。

例如:


OneOf<Success, Failure, Pending> result = GetResult();
// 编译器会强制你处理所有三种情况
result.Match(
    success => HandleSuccess(success.Value),
    failure => HandleFailure(failure.Type),
);
// 如果遗漏了某种情况,程序将无法编译!”

你会收到编译器提示,如果你在IDE或代码编辑器中悬停查看的话,你会看到这样的提示:

图像显示智能提示,提醒开发者错过了某个处理函数,因为有3种类型,但实际上只有2个处理函数

.Match()方法

.Match()方法是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 UnsupportedFileFormatException(new UnsupportedFileFormatException("未知支付方式"))
    ;
}

这种方式的优点是什么?

  • 它可以统一处理不同的文件格式

  • 导入向导可以接受多种文件类型

  • 配置加载器可以支持多种格式

OneOf的主要优势

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

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

  • 针对不同场景有不同的数据形态

  • 类型安全的分支处理,让编译器强制处理所有情况

  • 领域建模中,不同的状态携带不同的信息

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

本质上,OneOf是一种以类型安全的方式表示“此方法返回A或B或C”的方法,从而强制用户必须明确处理每种情况。这样可以使代码更加健壮、易于维护,同时也减少了错误的发生几率。

如您所见,如果您喜欢阅读这篇文章,请随时在Twitter上联系我吧。

Comments are closed.