这是从聊天中的对话中得到启发的,该对话始于对C#7.0元组和参数声明的讨论,从而引发了这样的想法,即没有“ good1”方式可以在C#中返回错误状态而不会引发异常。 br />
出于好奇,我想知道设计一个对开发人员透明的类型会怎样,但允许他们安全地返回异常而不必解开堆栈。

对于那些不知道的人,当您使用C#(或VB.NET,F#,任何.NET语言遵循相同的要求)中的throw / catch对象时,最昂贵的部分往往是堆栈。抛出异常是很便宜的,但是抛出异常,堆栈必须放松并反省自己,以便为您提供所需的信息。当然,这是设计使然。语言和框架设计人员希望异常表示程序进入“例外状态”,即存在一个需要解决的问题。

问题是某些方法实际上并不需要如果出错则抛出异常,他们可以只返回通过/失败,然后填充一个Exception参数。另一种选择是返回catch,其中out是返回类型。

当然,这不能使我们返回Tuple<bool, T>的能力,只是通过/失败。有时我们可能想返回出什么问题了。

所以,a,我得到了今天创建的T结构。通过包括与ExceptionFailable<T>之间的implicit转换,它使我们可以简单地T而不是抛出,从而对错误状态进行了更便宜的管理。那不只是从Exception定义隐式转换。这意味着return Exception无效,但是nullFailable<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);

在某些情况下,这两个都是“好的”;我只是想尝试出另一种可能的选择。

评论

重新发明轮子?看到此重构远离异常,我可以告诉您,这是一个可怕的想法。我尝试使用,结果是一团糟,所以我撤消了所有操作,现在使用经典的例外方式。这要容易得多。一旦开始使用结果/失败,您将陷入结果/失败的恶性循环,到处都是疯狂的ifs。您的实现只是一个简单的开始。

@ t3chb0t我认为可以以上述示例中使用int.TryParse方法相同的方式使用此类,在该示例中,方法返回后立即检查Passed状态,并且仅从那里使用Result。但是一旦您开始将那些Failable-s传递到堆栈中(就像通常的异常一样),我会看到它成为问题。

@NikitaB完全正确,当您拥有IEnumerable的失败/结果而只有其中一些失败而其他失败或当T本身是IEnumerable时,它会变得更加有趣。

我很好奇你们为什么认为这是一个坏主意; Rust的结果为并且效果很好。当然,它在std库中的任何地方都可以使用,因此它的用法比C#中的用法要多得多。

好的,那么API就不一致了,因为一次可以通过Exception属性访问Exception,而另一次是Result。在这种情况下,无法知道作为结果的异常是功能调用的实际结果还是错误的指示。如果我有一个实际上创建异常的函数,某些ExceptionFactory,但未能创建异常,现在结果包含一个Exception,但是该异常是工厂应该创建的异常,还是错误?

#1 楼

首先:恭喜,您已经重新发现错误monad。

https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Error.html

第二:如注释中所述,C#已经有了“包装值或异常,即Task<T>的概念。您可以使用Task.FromExceptionTask.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()。您可能应该这样做,因为ExceptionResult在您的设计中都可以为null。
您应该重新使用单一相等实现。例如,!=应该只返回!(a == b)。否则,此实现对我来说似乎还不错。我可能会将T转换设为隐式,将Exception转换设为显式(或将其删除,因为您最好只写.Exception),但这只是我不喜欢“透明”的东西。 :)

P.S.同样,Failable对我而言并不像典型的struct,更像class

评论


\ $ \ begingroup \ $
我只希望它成为结构,因为我希望Failable value = null;之所以失败,是因为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()
}


即使我不熟悉您的不及格课程,乍一看我也有很大的机会正确地确定AnotherWellNamedMethodNamingIsHard的返回类型是什么意思,但是没有理由假定AWellNamedMethod返回Exception,因为它可能会失败,并且不希望它返回null表示成功。
其中哪个更容易阅读和理解发生了什么?

...
if(someObject.MoveFilesToArchive() == null)
{
    ...
}
if(someObject.MoveFilesToArchive().Passed)
{
    ...
}


TException时会发生什么?

当您的方法应该做的事情是建立一个异常时,您的隐式转换将变得模棱两可。

/>其中一个代码路径应生成一个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);来解决

其他评论

这些可能是边界挑剔但仍然很认真的反馈:


函数调用中参数的顺序通常带有隐含的权重或参数的重要性。因此,选择第一个参数作为例外对我来说很奇怪。
考虑命名为哨兵属性SuccessComplete而不是Passed。在概念上类似的操作中,想到的几乎所有状态/结果/结果代码都使用“成功”来表示“良好”结果。我曾经看到“通过”的一个地方是用于数据验证,即使在这种情况下,“通过”也会表明数据良好,而不是验证完成。