在一个项目中,我正在尝试使用记录来实现可区分的联合,以摆脱抛出异常以处理应用程序层的“预期”错误。添加第三方库似乎有点矫枉过正,所以我尝试自己滚动并以与此记录类似的内容结束:
public abstract record CreateCustomerResponse
{
private CreateCustomerResponse() { }
public sealed record Success(Customer Customer) : CreateCustomerResponse;
public sealed record Error(string Code, string Message) : CreateCustomerResponse, IErrorResponse;
public sealed record Unauthorized() : CreateCustomerResponse;
}
这基本上是一个抽象记录,除了它的子记录外不能被继承,这些子记录又被密封,限制了你可以拥有的结果类型。
它的实现方式与使用库的任何其他 DU 的实现方式没有太大区别:
static CreateCustomerResponse CreateCustomer(Customer customer)
{
// Or do data validation however you prefer.
if (string.IsNullOrEmpty(customer.FirstName))
return new CreateCustomerResponse.Error(nameof(customer.FirstName), "First name is required");
if (string.IsNullOrEmpty(customer.LastName))
return new CreateCustomerResponse.Error(nameof(customer.LastName), "Last name is required");
return new CreateCustomerResponse.Success(customer);
}
并且可以根据需要使用更新的 C# 功能(例如模式匹配)非常轻松地使用/转换它:
static string PrintResponse(CreateCustomerResponse response)
{
return response switch
{
CreateCustomerResponse.Success result => $"OK, so {result.Customer.FirstName} was created",
CreateCustomerResponse.Error => $"Sorry, operation failed: {response}",
CreateCustomerResponse.Unauthorized => "You're unauthorized pal",
_ => throw new NotImplementedException()
};
}
我见过很多人使用第三方库(OneOf 和其他人)来完成类似的事情,但看起来很简单,不需要这个用例的库;它甚至允许使用模式匹配,因此您不需要“匹配”方法等来处理结果。
我发现的唯一问题是,如果不包含
switch
模式,_
表达式认为不会涵盖所有情况(这是不正确的),但添加它不会造成伤害。然而我看到了同样的好处:你必须检查实际结果才能使用它,并且绑定到一组已知的选项。
所以问题是这样的:
我可能没有考虑到此实施中的任何明显缺点吗?在这种情况下不使用已知的第三方库,我是否遗漏了什么,这似乎被普遍接受?
非常感谢社区的意见。
例如使用 OneOf 的优点是编译时检查是否处理了联合的每个可能值。
在您的实施中,如果您为
CreateCustomerResponse
添加另一个可能的值,没有什么可以阻止您在不更改 switch 表达式的情况下构建和运行程序,并且您最终可能会抛出 NotImplementedException
因为您忘记了处理它那里。
使用 OneOf,如果不处理刚刚添加到记录中的值,您甚至无法首先构建解决方案。
这里的一个主要缺点是这些是类。现在每个结果都涉及堆分配 和 类型测试。那个可怜的垃圾收集者!
结构记录在这里可能工作得更好,也许有一些简单的访问器来检查结果(可以通过布尔值或枚举)。
当然这仍然在every结果中留下很多分支,而不是让成功案例无分支地运行,并且只在失败案例中进行错误处理(这可能很少见)。您仍然通常需要异常处理,因为这些不是唯一可能失败的方式,因此您添加了大量分支但没有删除任何故障处理。