出于好奇,我想知道设计一个对开发人员透明的类型会怎样,但允许他们安全地返回异常而不必解开堆栈。
对于那些不知道的人,当您使用C#(或VB.NET,F#,任何.NET语言遵循相同的要求)中的
throw
/ catch
对象时,最昂贵的部分往往是堆栈。抛出异常是很便宜的,但是抛出异常,堆栈必须放松并反省自己,以便为您提供所需的信息。当然,这是设计使然。语言和框架设计人员希望异常表示程序进入“例外状态”,即存在一个需要解决的问题。问题是某些方法实际上并不需要如果出错则抛出异常,他们可以只返回通过/失败,然后填充一个
Exception
参数。另一种选择是返回catch
,其中out
是返回类型。当然,这不能使我们返回
Tuple<bool, T>
的能力,只是通过/失败。有时我们可能想返回出什么问题了。所以,a,我得到了今天创建的
T
结构。通过包括与Exception
和Failable<T>
之间的implicit
转换,它使我们可以简单地T
而不是抛出,从而对错误状态进行了更便宜的管理。那不只是从Exception
定义隐式转换。这意味着return Exception
无效,但是null
和Failable<string> value = null;
一样。如果框架/语言设计人员曾经公开过从
Failable<string> value = (Failable<string>)null;
进行Failable<string> value = new Failable<string>(null);
转换,则此结构将是完全透明的。public struct Failable<T> : IEquatable<Failable<T>>
{
public Exception Exception { get; }
public T Result { get; }
public bool Passed { get; }
private Failable(Exception exception, T result, bool passed)
{
Exception = exception;
Result = result;
Passed = passed;
}
public Failable(Exception exception)
: this(exception, default(T), false)
{
}
public Failable(T result)
: this(null, result, true)
{
}
public static implicit operator Failable<T>(Exception exception) => new Failable<T>(exception);
public static implicit operator Failable<T>(T result) => new Failable<T>(result);
public static implicit operator Exception(Failable<T> result) => result.Exception;
public static implicit operator T(Failable<T> result) => result.Result;
public override string ToString() => (Passed ? Result?.ToString() : Exception?.ToString()) ?? "null";
public override int GetHashCode() => Exception.GetHashCode() ^ Result.GetHashCode();
public bool Equals(Failable<T> other) => this == other;
public override bool Equals(object obj) => obj is Failable<T> && this == (Failable<T>)obj;
public static bool operator ==(Failable<T> a, Failable<T> b) => a.Exception == b.Exception && a.Result.Equals(b.Result);
public static bool operator !=(Failable<T> a, Failable<T> b) => a.Exception != b.Exception || !a.Result.Equals(b.Result);
public static readonly Failable<T> Empty = new Failable<T>();
}
现在,为了演示其工作原理,我定义了一个非常丑陋的方法,因此请不要对其进行复习,因为它遍历了此结构的所有可能功能:
static Failable<T> FailableTest<T>(bool pass, bool nullOrThrow, T result)
{
try
{
if (pass)
{
if (nullOrThrow)
{
// Both options are valid:
// return new Failable<T>(null);
return (Failable<T>)null;
}
else
{
return result;
}
}
else
{
if (nullOrThrow)
{
throw new ArgumentException($"Throwing as expected, {nameof(pass)}:'{pass}', {nameof(nullOrThrow)}:'{nullOrThrow}'.");
}
else
{
return new ArgumentException($"Returning as expected, {nameof(pass)}:'{pass}', {nameof(nullOrThrow)}:'{nullOrThrow}'.");
}
}
}
catch (Exception e)
{
return e;
}
}
按以下顺序排列的东西:Console.WriteLine("Pass : " + FailableTest(true, false, "1. String on pass").ToString());
Console.WriteLine("Fail : " + FailableTest(false, false, "2. String on pass").ToString());
Console.WriteLine("Null : " + FailableTest(true, true, "3. String on pass").ToString());
Console.WriteLine("Throw : " + FailableTest(false, true, "4. String on pass").ToString());
Console.WriteLine("Cast : " + (FailableTest(true, false, 15) - FailableTest(true, false, 5)));
返回:
Pass : 1. String on pass
Fail : System.ArgumentException: Returning as expected, pass:'False', nullOrThrow:'False'.
Null : null
Throw : System.ArgumentException: Throwing as expected, pass:'False', nullOrThrow:'True'.
at GenericFailableTest.Program.FailableTest[T](Boolean pass, Boolean nullOrThrow, T result) in c:\users\ebrown\documents\visual studio 2017\Projects\Test CSharp Projects\GenericFailableTest\Program.cs:line 44
Cast : 10
有趣的是,
implicit
运算符使您可以完全忽略此类:var str = FailableTest(true, false, "Some String");
这是设计使您想到的主要问题:
应该有
null
运算符吗?如果是,应该是implicit operator T
吗?应该有一个
T(Failable<T>)
运算符吗?如果是这样,应该是implicit
吗?API是否应该包括一个
Exception(Failable<T>)
构造函数,该构造函数允许一个人传递implicit
的元组?正确的定义可能是:static Failable<int> TryParse(string input)
{
int result;
if (int.TryParse(input, out result))
{
return result;
}
else
{
return new ArgumentException($"The string '{input}' was not a valid integer.");
}
}
var parseResult = new Failable<int>();
while (!parseResult.Passed)
{
parseResult = TryParse(Console.ReadLine());
}
var value = parseResult.Result;
// Do something with `value`
1:这里的“好”是主观的,主要有两个语言中已经存在的引发异常的替代方法:
1.使用
Failable(Tuple<bool, T>)
参数; 2.返回(pass, value)
; 在某些情况下,这两个都是“好的”;我只是想尝试出另一种可能的选择。
#1 楼
首先:恭喜,您已经重新发现错误monad。https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Error.html
第二:如注释中所述,C#已经有了“包装值或异常,即
Task<T>
的概念。您可以使用Task.FromException
和Task.FromResult
来构造它们。当然,在任务上使用Result
会产生结果或引发异常,就像await
一样。这也说明任务中不需要异步!任务只是“我将提供值或异常”的概念如果您现在已经拥有它,那就太好了;您可以使用任务来表示您的“失败”概念,并像其他任何任务一样等待它们。
是否应该有一个
T(Failable<T>)
运算符?如果是,应该隐式吗?将泛型转换为任何类型的运算符可能很难推理。不应该e隐式,因为操作不能保证成功!如果需要,应该明确。
在这里查看
Task
的设计以获取启发。请注意,工厂是静态方法,在调用它们时非常清楚。同样很清楚何时获取结果。类似地看一下可为空的设计。 (“也许单子”与错误单子非常相似;有关更多信息,请参见下文。)从T到T?有一个隐式转换,但从T?有一个隐式转换。 T是显式的。
是否应该有
Exception(Failable<T>)
运算符?如果是这样,它应该是隐式的吗?我会感到困惑。 (通过,值)的元组?
我不明白这个问题。 (尽管我注意到,(布尔T)元组是monad的结构,也就是C#中的
Failable(Tuple<bool, T>)
。)练习1:您已经创建了monad,因此您应该能够在其上定义monad运算符;如果这样做,则可以在LINQ查询中使用您的类型!您可以实现成员吗?
struct Failable<T> ... {
...
public Failable<T> Select<R>(Func<T, R> f) { ... }
public Failable<C> SelectMany<B, C>(
Func<T, Failable<B>> f1,
Func<T, B, C> f2) { ... }
public Failable<T> Where(Func<T, bool> f) { ... }
}
如果这样做,则可以编写查询:
Failable<int> f = OperationThatCanFail();
Failable<double> d = from i in f where i > 0 select Math.log(i);
如果您做对了,则d应该是故障代码或整数i的对数。
练习2:您已经实现了错误monad;您现在可以实现追踪单子吗?
Nullable<T>
具有T的值,但也具有将字符串附加到跟踪的操作,因此您可以跟踪T在程序周围的移动。 练习3:可为空的实现为(bool,T)对。失败实现为(Exception,T)对。跟踪被实现为(字符串,T)对。您是否可以设计和实现将S与T关联的广义
Trace<T>
类型,然后从中派生其他单子?最后,您可以考虑使用更高级的运算。例如:
public static Func<A, Failable<R>> ToFailable(this Func<A, R> f)
{
return a =>
{
try
{
return new Failable<R>(f(a));
}
catch(Exception x)
{
return new Failable<R>(x);
}
};
}
现在,您可以采用可以抛出的
State<S, T>
形式的现有函数,并将其转换为不能抛出的函数。评论
\ $ \ begingroup \ $
我还没有听说过跟踪monad(搜索它似乎并没有产生任何意义)。它到底在追踪什么,何时追踪?
\ $ \ endgroup \ $
–Rob
17年5月12日23:58
\ $ \ begingroup \ $
@Rob:hackage.haskell.org/package/monad-logger-您可以跟踪所需的任何内容。例如,字符串到达您的Web服务器。它去哪儿了?它在到达与被(1)在查询中使用(2)写入数据库,(3)回显给用户之间如何修改?当您认为自己可能受到sql注入,数据库损坏,跨站点脚本或其他攻击时,可以回答这些类型的数据流问题。
\ $ \ endgroup \ $
–埃里克·利珀特
17年5月13日在0:01
\ $ \ begingroup \ $
如果这是一个好主意,那么我很确定您已经在某些.Net版本中实现了它,但是我想您知道这是不可能完成的,而这些练习应该证明,无需您明确地说不这样做,就无法完成,或者C#具有其他(或更优的)方式来处理异常等。
\ $ \ endgroup \ $
–t3chb0t
17年5月13日在6:05
\ $ \ begingroup \ $
@ t3chb0t:在C#中,应通过处理异常来处理异常,这就是为什么我们将其称为“异常处理”。当您意识到C#中的异常处理以逻辑上形成方法激活为前提,而协程不是这样时,就会出现问题。 C#现在具有两种协程:迭代器块和异步方法。在这两种情况下,编译器团队都必须想出各种疯狂的规则来使异常处理正常工作。其中一项技术是使用Task
\ $ \ endgroup \ $
–埃里克·利珀特
17年5月13日下午6:45
\ $ \ begingroup \ $
任务的工作方式与预期的“失败/结果”不同,因此他们的想法不同。实际上,Task的使用是不确定的,并且就一致性而言,有关“失败/结果”的规则清晰易懂。返回Task的方法可以抛出或返回Task.IsFaulted == true,因此,您永远无法确定结果,而您始终可以确保不会抛出Failable / Result / etc方法。这就是为什么我认为您没有这样做的原因,即使Task提供了所有避免抛出的东西,您仍然使用异常。
\ $ \ endgroup \ $
–t3chb0t
17年5月13日在7:14
#2 楼
一些小事:您不会在
null
和等于运算符中检查GetHashCode()
。您可能应该这样做,因为Exception
和Result
在您的设计中都可以为null。您应该重新使用单一相等实现。例如,
!=
应该只返回!(a == b)
。否则,此实现对我来说似乎还不错。我可能会将T
转换设为隐式,将Exception
转换设为显式(或将其删除,因为您最好只写.Exception
),但这只是我不喜欢“透明”的东西。 :) P.S.同样,
Failable
对我而言并不像典型的struct
,更像class
。评论
\ $ \ begingroup \ $
我只希望它成为结构,因为我希望Failable
\ $ \ endgroup \ $
– Der Kommissar
17年5月12日15:36
#3 楼
为了获得一致且可发现的API,您应该具有Failable
的非通用版本。 为了保持一致性:
如果为我提供了两种可能无法完成工作的方法,那么我希望以同样的方式检查成功/失败。我不应该在某些时候检查
result.Passed
,而在其他时候不检查result == null
。关于发现性
使用
Failable
作为返回类型的线索提示人们看到了操作可能不会成功的代码。使用Exception
作为返回类型会误导人们,使人们看到该代码与操作与异常有关。为该接口提供了三种可能不会成功的方法:
public interface IYouDontKnowMyImplementation
{
Exception AWellNamedMethod();
Failable<T> AnotherWellNamedMethod<T>();
Failable NamingIsHard()
}
即使我不熟悉您的不及格课程,乍一看我也有很大的机会正确地确定
AnotherWellNamedMethod
和NamingIsHard
的返回类型是什么意思,但是没有理由假定AWellNamedMethod
返回Exception
,因为它可能会失败,并且不希望它返回null表示成功。其中哪个更容易阅读和理解发生了什么?
...
if(someObject.MoveFilesToArchive() == null)
{
...
}
if(someObject.MoveFilesToArchive().Passed)
{
...
}
当
T
是Exception
时会发生什么?当您的方法应该做的事情是建立一个异常时,您的隐式转换将变得模棱两可。
/>其中一个代码路径应生成一个
Failable<Exception>
其中Failable<Exception> BuildErrorInformation()
{
... Do information gathering
if(iWasAbleToGetTheInfoINeed)
{
return new Exception("the info I need");
}
else
{
return new Exception("I failed to get the info I needed");
}
}
,另一个应生成一个
result.Exception.Message == "I failed to get the info I needed"
这确实是一个边缘情况,可以改用
Failable<CustomException>
或只返回new Failable<Exception>(null, new Exception("the info I need"), true);
来解决其他评论
这些可能是边界挑剔但仍然很认真的反馈:
函数调用中参数的顺序通常带有隐含的权重或参数的重要性。因此,选择第一个参数作为例外对我来说很奇怪。
考虑命名为哨兵属性
Success
或Complete
而不是Passed
。在概念上类似的操作中,想到的几乎所有状态/结果/结果代码都使用“成功”来表示“良好”结果。我曾经看到“通过”的一个地方是用于数据验证,即使在这种情况下,“通过”也会表明数据良好,而不是验证完成。
评论
重新发明轮子?看到此重构远离异常,我可以告诉您,这是一个可怕的想法。我尝试使用,结果是一团糟,所以我撤消了所有操作,现在使用经典的例外方式。这要容易得多。一旦开始使用结果/失败,您将陷入结果/失败的恶性循环,到处都是疯狂的ifs。您的实现只是一个简单的开始。@ t3chb0t我认为可以以上述示例中使用int.TryParse方法相同的方式使用此类,在该示例中,方法返回后立即检查Passed状态,并且仅从那里使用Result。但是一旦您开始将那些Failable-s传递到堆栈中(就像通常的异常一样),我会看到它成为问题。
@NikitaB完全正确,当您拥有IEnumerable的失败/结果而只有其中一些失败而其他失败或当T本身是IEnumerable时,它会变得更加有趣。
我很好奇你们为什么认为这是一个坏主意; Rust的结果为
好的,那么API就不一致了,因为一次可以通过Exception属性访问Exception,而另一次是Result。在这种情况下,无法知道作为结果的异常是功能调用的实际结果还是错误的指示。如果我有一个实际上创建异常的函数,某些ExceptionFactory,但未能创建异常,现在结果包含一个Exception,但是该异常是工厂应该创建的异常,还是错误?