【发布时间】:2017-04-10 06:31:39
【问题描述】:
在 C# 中,我希望可以选择以更“实用”的方式处理错误,而不是始终使用默认的异常抛出模型。在某些情况下,抛出模型很棒,因为它允许您在发生意外情况时强制您的代码停止执行。但是,投掷模型有几个主要缺点:
- 异常抛出基本上是一个颠覆主控流系统的应用控制流的整个次级系统。当一个方法被调用时,它的主体被执行,然后有效地控制权返回给调用者,不管它是由于
return还是throw语句。如果控制从throw语句返回给调用者,如果范围内没有合适的try/catch,控制将立即移至调用者;而如果控制从return语句返回,控制将照常继续。我知道实现比这更复杂和微妙,但我认为这是一个适当的概念总结。这种双重系统通常会以不同的方式在设计时和运行时造成混淆。 - 抛出系统在并行或异步场景中变得更加笨拙,正如
System.Threading.Tasks.Task的错误处理中所见。由Task执行引发的任何异常都存储在AggregateException中,并由调用代码通过Task.Exception属性访问。因此,虽然Tasks 的执行可能会中止,但调用代码必须使用正常的 C# 控制流查找存储在对象属性中的错误。 - 除了 XML cmets,没有关于方法是否可能抛出异常或它可能抛出的元数据。异常表现为方法输出的另一种形式,但在很大程度上被类型系统忽略。例如,
int Divide(int x, int y)方法可能会导致整数或DivideByZeroException,但此方法的签名不会提示任何有关错误的信息。相反,我经常听到关于 Java 的检查异常的抱怨,其中方法可以抛出的每个特定异常类型都必须添加到其签名中,这可能会变得非常冗长。对我来说,最简单的中间立场是像Nullable<T>这样包含值或异常的泛型类型。像Divide这样的方法将具有以下签名:Fallible<int> Divide(int x, int y)。然后,任何使用结果的操作都需要处理错误情况。方法还可以采用Fallible参数,以便更轻松地进行链接。
这是我草拟的Fallible 的实现:
public class Fallible<T> : IEquatable<Fallible<T>> {
#region Constructors
public Fallible() {
//value defaults to default(T)
//exception defaults to null
}
public Fallible(T value) : this() {
this.value = value;
}
public Fallible(Exception error) : this() {
if (error == null) throw new ArgumentNullException(nameof(error));
Error = error;
}
public Fallible(Func<T> getValue) : this() {
if (error == null) throw new ArgumentNullException(nameof(getValue));
try {
this.value = getValue();
}
catch(Exception x) {
Error = x;
}
}
#endregion
#region Properties
public T Value {
get {
if (!HasValue) throw new InvalidOperationException("Cannot get Value if HasValue is false.");
return value;
}
}
private T value;
public Exception Error { get; }
public bool HasValue => Error == null;
#endregion
#region Equality
public bool Equals(Fallible<T> other) => (other != null)
&& Equals(Error, other.Error)
&& Equals(Value, other.Value);
public override bool Equals(object obj) => Equals(obj as Fallible<T>);
public static bool operator ==(Fallible<T> a, Fallible<T> b) {
if (a == null) return b == null;
return a.Equals(b);
}
public static bool operator !=(Fallible<T> a, Fallible<T> b) {
if (a == null) return b != null;
return !a.Equals(b);
}
public override int GetHashCode() =>
HasValue
? Value.GetHashCode()
: Error.GetHashCode();
#endregion
public override string ToString() =>
HasValue
? $"Fallible{{{Value}}}"
: $"Fallible{{{Error.GetType()}: {Error.Message}}}";
}
还有问题:
- .NET 是否已经有类似
Fallible<T>的东西?也许是 Task Parallel Library、Reactive Extensions 或 F# 核心库中的一个类? - 上面的实现是否存在重大缺陷?
- 是否存在我可能忽略的控制流概念问题?
【问题讨论】:
-
不知道任何内置的类似结构。这个想法很有趣,并且可能有有效的用例,但请注意,几乎每次调用此类方法通常都会跟着 if-else 来检查错误,而 try-catch 的优点是能够保护更大的块的逻辑。此外,对返回
Fallible的方法的级联调用可能会很棘手,而默认情况下异常会冒泡,而不会弄乱对处理它们不感兴趣的代码 -
实现非常简单,对我来说看起来还可以。您可以使用这种方法,但其他开发人员(您的库的用户或您的同事)可能对它不太满意(因为它违背了 .NET 框架本身的设计方式)。但如果你自己使用它——当然,为什么不呢。
-
这是一个 F# 刚刚使用 2 元素 DU msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/… 的示例
-
一般来说,这种方法或类似的方法在 F# 中很常见,但在 C# 中变得困难。您确实需要详尽的模式匹配才能使其真正有用。
-
如果你要使用它,至少让它成为一个结构。如果你在任何地方都使用它,它会给 GC 增加不必要的压力,而使用值类型可以避免这种情况。但是 IMO 它使代码变得笨拙,因为您到处都需要条件。模式匹配应该会在未来改变这一点(尽管 C# 7 还没有)。
标签: c# .net exception-handling f# functional-programming