对逻辑流使用运行时类型(在 C# 中)

本文关键字:类型 运行时 | 更新日期: 2023-09-27 18:35:14

这个问题类似于这个问题的(子集)

在这种情况下,它使用运行时类型来区分成功和失败返回的结果。

我经常看到以下模式:

public struct Result {
    public boolean IsSuccess { get;set;}
    public string ErrorMessage {get;set;}
    public int Value {get;set;}
}
...
Result result = someObject.SomeMethod();
if (result.IsSuccess) DoSomething(result.Value);
else handleError(result.ErrorMessage);

我认为以下更自然,表达意图更明确(在我看来):

public abstract class Result { }
public sealed class Failure : Result {
    public string ErrorMessage { get; set; }
}
public sealed class Success : Result {
    public int Value { get; set; }
}
...
Result result = someObject.SomeMethod();
if (result is Success) DoSomething((result as Success).Value);
else if (result is Failure) handleError((result as Failure).ErrorMessage);

另请注意,.Net(和许多其他语言)在具有多个 catch 子句的 try-catch 块中使用此模式(其中异常类型选择一个 catch 块)。

编辑:此模式(即依赖于运行时类型)与 F# 的区分联合相同,区别在于在 F# 中它是本机的,而在 C# 中它是使用用于不同目的的构造模拟的。

编辑:我认为我对第一个代码的主要问题是代码气味"部分初始化的对象"。在 100% 的情况下,只会初始化对象的一半。它也几乎违反了ISP,"几乎"因为曾经.IsSuccess被评估后,从此以后只会使用部分对象(如果它只是成功。如果是错误,则使用结果 - 仅使用错误属性)。运行类型检查解决方案没有这些问题。

所以问题是:使用这种模式有什么问题?我从以下角度对问题特别感兴趣:可维护性、可读性、可测试性、概念纯度和 OOP/OOD。

对逻辑流使用运行时类型(在 C# 中)

作为一般规则,我更喜欢第一种形式。

第二个示例需要更多的击键并添加 4 个类型检查操作。这对消费者来说也不太明显(你不能混淆IsSuccess属性的含义)。仅仅为了确定操作的结果而必须执行类型转换似乎不太合乎逻辑(似乎类似于对程序流使用异常;您可以这样做,但不应该这样做)。

此外,第一种形式可以作为描述数据的独立于平台的机制。第二个不能。换句话说,您可以轻松地序列化由基元组成的结果类型,并将其与任何使用者共享,但需要类型检查限制/消除这种可能性。

为了提高Result对象的质量,您可以创建一个Messages集合,其中包含的不仅仅是一个字符串(可能包括代码、严重性、消息等)。这允许所有结果返回消息,而不仅仅是失败的操作。如果操作失败,您可能会假定Messages至少包含一个错误消息(和/或使用 LINQ 查询集合以查找所有与错误相关的消息)。

第二种形式错误的学术原因如下:

C# 作为一种面向对象语言,设计时假设将用于对基础系统的类别(例如业务概念)进行建模,而实例将用于对事实进行建模。

使用对事实进行建模是对工具的误用。

虽然我仍然不知道实际后果是什么。

面向对象的设计对丢弃的信息并不重要。此结果对象的生存期非常有限,因此属于堆栈上的结构。

如果你想谈论维护/可读性,那么人们最初会对这段代码感到困惑。

如果你想在错误传播上面向对象,那么纯粹使用异常(尽管这会降低这些用例的性能)。