您是否曾经需要一种方法,能够根据不同的情况返回不同类型的返回值呢?比如,支付处理程序可能需要返回多种支付方式;订单状态可能包含多种信息;或者文件加载器需要处理多种格式的文件。
在C#中,我们通常通过继承层次结构、标记接口或包装对象来解决这个问题——但这些方式都会增加代码的复杂性,同时降低类型安全性。不过幸运的是,还有更好的解决方案:使用OneOf库。
如果您之前使用过TypeScript进行编程,那么您可能已经熟悉了联合类型这个概念。联合类型并不是C#原生支持的功能,但未来会加入这一特性。在此之前,您可以使用OneOf库来实现类似的功能。
在这篇文章中,我将向您展示如何使用OneOf来创建类似于F#中的判别性联合类型,从而在各种场景中编写更简洁、更具表达力的代码——从多态的返回类型到状态机,再到优雅的错误处理机制。
目录
什么是OneOf?
OneOf包为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
选项1(推荐):
使用终端导航到项目文件夹,然后运行以下命令:
dotnet add package OneOf
选项2:
使用您的IDE(Visual Studio、Rider或VS Code):
-
右键点击项目文件
-
选择“管理NuGet包”
-
搜索“OneOf”
-
点击“安装”
核心概念与功能
要充分利用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或代码编辑器中悬停查看的话,你会看到这样的提示:
.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上联系我吧。

