返回魔术值,引发异常或在失败时返回false?


83

有时我最终不得不为类库编写方法或属性,对于该类库,没有真正的答案不是偶然,而是失败。无法确定,无法使用,无法找到,当前无法执行或没有其他可用数据。

我认为对于这种相对非异常的情况,有三种可能的解决方案可以指示C#4失败:

  • 返回一个没有其他含义的魔术值(例如null-1);
  • 抛出异常(例如KeyNotFoundException);
  • return false并在out参数中提供实际的返回值(例如Dictionary<,>.TryGetValue)。

所以问题是:我应该在哪种非例外情况下抛出异常?而且,如果我不应该抛出:什么时候返回在实现Try*带有out参数的方法时产生的魔术值?(对我来说,该out参数似乎很脏,要正确使用该参数还需要更多工作。)

我正在寻找实际的答案,例如涉及设计准则的答案(我对Try*方法一无所知),可用性(因为我向类库提出的要求),与BCL的一致性以及可读性。


在.NET Framework基类库中,使用了所有三种方法:

请注意,正如HashtableC#中没有泛型时创建的那样,它使用object,因此可以null作为魔术值返回。但是对于泛型,在中使用了例外Dictionary<,>,而最初没有TryGetValue。见解显然在改变。

显然,Item- TryGetValueParse- TryParse偶是有原因的,所以我认为抛出异常的非特殊的故障是在C#4 没有这样做。但是,Try*即使存在,方法也不总是存在Dictionary<,>.Item


6
“异常意味着错误”。向字典查询不存在的项目是一个错误;每次使用流时,都会要求流在EOF时读取数据。(这总结了很长一段时间我没有机会提交精美格式的答案:))
KutuluMike 2012年

6
我并不是说您的问题太笼统,不是建设性的问题。由于已经显示出答案,因此没有规范的答案。
乔治·斯托克

2
@GeorgeStocker如果答案很简单明了,那么我就不会问这个问题。人们可以从任何角度(例如性能,可读性,可用性,可维护性,设计准则或一致性)争论为什么给定的选择更可取的事实,使它本质上是不规范的,但对我的满意是负责的。所有问题都可以在某种程度上由主观回答。显然,您希望一个问题的答案大致相同,这才是一个好问题。
Daniel AA Pelsmaeker 2012年

3
@Virtlink:George是一位社区选举主持人,他自愿花费大量时间来帮助维持Stack Overflow。他说了为什么要关闭问题,而FAQ则支持他。
Eric J.

15
这个问题属于这里,不是因为它是主观的,而是因为它是概念性的。 经验法则:如果您坐在IDE编码的前面,请在Stack Overflow上询问。如果您正站在白板上集思广益,请在此处与程序员联系。
罗伯特·哈维

Answers:


56

我认为您的示例并非真正等效。有三个不同的组,每个组都有其行为的理由。

  1. 当存在“直到”条件时,StreamReader.Read或者当存在一个永远不会是有效答案的简单使用的值(表示-1 IndexOf)时,魔术值是一个不错的选择。
  2. 当函数的语义是调用者确定它将起作用时,抛出异常。在这种情况下,不存在的密钥或错误的双精度格式确实是例外。
  3. 如果语义是用来探测该操作是否可行,请使用out参数并返回bool。

对于情况2和3,您提供的示例非常清楚。对于神奇的价值,可以说这是否是一个好的设计决策,并非在所有情况下都是如此。

NaN由归国Math.Sqrt是一种特殊情况-它遵循浮点标准。


10
不同意数字1。魔术值绝不是一个好的选择,因为它们需要下一个编码器才能知道魔术值的重要性。它们还会损害可读性。我认为在任何情况下魔术值的使用都不会比Try模式更好。
0b101010 '02

1
但是Either<TLeft, TRight>单子呢?
萨拉

2
@ 0b101010:只是花了一些时间查找streamreader.read如何安全地返回-1 ...
jmoreno

33

您正在尝试与API用户进行交流。如果抛出异常,则没有任何强迫他们捕获的异常,只有阅读文档才能让他们知道所有可能的情况。就我个人而言,我发现在文档中查找某个方法可能引发的所有异常情况既缓慢又乏味(即使是智能感知,我仍然必须手动将其复制出来)。

神奇的值仍然需要您阅读文档,并可能参考一些const表来对该值进行解码。至少对于所谓的非异常事件,它没有异常的开销。

这就是为什么尽管out有时不赞成使用参数,但我还是更喜欢该方法和Try...语法。它是规范的.NET和C#语法。您正在与API用户沟通,他们必须在使用结果之前检查返回值。您还可以在第二个out参数中包含有用的错误消息,这又有助于调试。这就是为什么我投票支持Try...with out参数解决方案。

另一个选择是返回一个特殊的“结果”对象,尽管我发现这更加繁琐:

interface IMyResult
{
    bool Success { get; }
    // Only access this if Success is true
    MyOtherClass Result { get; }
    // Only access this if Success is false
    string ErrorMessage { get; }
}

然后您的函数看起来正确,因为它仅具有输入参数,并且仅返回一件事。只是它返回的一件事有点像元组。

实际上,如果您喜欢这种事情,则可以使用Tuple<>.NET 4中引入的新类。就我个人而言,我不喜欢每个字段的含义不太明确的事实,因为我不能给出Item1Item2有用的名称。


3
就我个人而言,我经常像您的IMyResult原因一样使用结果容器,它可以传达比just truefalseresult值更复杂的结果。Try*()仅对简单的东西有用,例如从字符串到int的转换。
this.myself15年

1
好帖子。对我来说,我更喜欢上面概述的Result结构习惯用法,而不是在“返回”值和“输出”参数之间进行拆分。保持整洁。
Ocean空投

2
第二个输出参数的问题是,当您在复杂程序中深入了50个函数时,如何将错误消息传达给用户?抛出异常要比分层检查错误要简单得多。当您得到一个时,就扔它,不管您有多深。

@rolls-当我们使用输出对象时,我们假设直接调用者将对输出执行他想做的任何事情:在本地处理它,忽略它或通过将其包装到异常中而冒泡。最好用这两个词-调用者可以清楚地看到所有可能的输出(带有枚举等),可以决定如何处理错误,并且不需要每次调用都尝试捕获。如果您最希望立即处理结果或将其忽略,则返回对象会更容易。如果您想将所有错误都扔到上层,则异常会更容易。
drizin

2
这只是比Java中的检查异常更加繁琐的方式来重塑C#中的检查异常。

17

如您的示例所示,每种情况都必须分别进行评估,并且在“特殊情况”和“流控制”之间存在相当大的灰色范围,尤其是如果您的方法旨在可重用,并且可能以完全不同的方式使用时比最初设计的要好。不要指望这里的所有人都同意“非例外”的含义,尤其是如果您立即讨论使用“例外”来实现这一点的可能性。

我们可能也不同意哪种设计使代码最容易阅读和维护,但是我将假设图书馆设计者对此有清晰的个人见解,只需要在其他考虑因素之间取得平衡即可。

简短答案

遵循您的直觉,除非您正在设计非常快速的方法,并期望有无法预料的重用。

长答案

每个将来的调用者都可以根据需要在两个方向上自由地在错误代码和异常之间进行转换;除了性能,调试器友好性和某些受限制的互操作性上下文外,这使得这两种设计方法几乎等效。这通常归结为性能,因此让我们集中精力。

  • 根据经验,抛出异常的速度比正常返回的速度慢200倍(实际上,存在很大的差异)。

  • 根据另一条经验法则,与最原始的魔术值相比,抛出异常通常可以使代码更整洁,因为您不依赖程序员将错误代码转换为另一个错误代码,因为错误代码会通过多层客户端代码传递给另一个有足够的上下文可以以一致且适当的方式处理它的地方。(特殊情况:null由于NullReferenceException在某些但不是所有类型的缺陷的情况下,它倾向于自动将其自身转换为缺陷的趋势,因此在这里往往表现得比其他魔术值更好;通常但并非总是非常接近缺陷的来源。 )

那是什么教训?

对于在应用程序生命周期内仅被调用过几次的函数(例如应用程序初始化),请使用可使您的代码更简洁,更易于理解的函数。性能不必担心。

对于一次性功能,请使用任何可以使您的代码更简洁的东西。然后进行一些分析(如果需要的话),并根据测量结果或整个程序结构将异常更改为返回代码(如果它们属于可疑的最高瓶颈)。

对于昂贵的可重用功能,请使用任何可以使您的代码更简洁的方法。如果您基本上总是必须进行网络往返或解析磁盘上的XML文件,则抛出异常的开销可能可以忽略不计。与更快地从“非异常故障”返回相比,更重要的是不要丢失任何故障细节,甚至是偶然的故障。

精简的可重用功能需要更多的考虑。通过采用例外,你是迫使像100倍呼叫者谁将会看到他们(很多)调用的一半外,如果函数体执行速度非常快变慢。例外仍然是一种设计选择,但是您将不得不为负担不起此费用的呼叫者提供一种开销较低的替代方法。让我们看一个例子。

您列出了一个很好的例子Dictionary<,>.Item,从广义上讲,它从返回null值变为KeyNotFoundException在.NET 1.1和.NET 2.0之间抛出(仅当您愿意考虑Hashtable.Item成为其实用的非通用先行者时)。这种“变化”的原因在这里并非没有兴趣。值类型的性能优化(不再装箱)使原始魔术值(null)成为不可选项;out参数只会带回一小部分性能成本。与抛出的开销相比,后一种性能考虑可以忽略不计KeyNotFoundException,但是异常设计在这里仍然优越。为什么?

  • ref / out参数每次都会产生成本,而不仅仅是在“失败”情况下
  • 任何关心的人都可以Contains在调用索引器之前先调用,并且此模式完全自然地读取。如果开发人员想要但忘了打电话Contains,则不会出现性能问题。KeyNotFoundException大到足以引起注意和修复。

我认为200x对例外的表现持乐观态度……请在评论前参阅blogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx “性能和趋势”部分。
gbjbaanb 2012年

@gbjbaanb-好吧,也许吧。那篇文章使用1%的失败率来讨论这个话题,这并不是一个完全不同的话题。我自己的想法和模糊地记得的测量方法是基于表实现的C ++的上下文(请参阅本报告的 5.4.1.2节,其中一个问题是,此类的第一个例外很可能始于页面错误(剧烈而可变)。 。但摊销一次性开销),但我会尽,并与.NET 4发布一个实验,并可能调整这个球场价值,我已经强调了差异。
尔卡Hanika

那么,ref / out参数的成本是否很高?怎么会这样?并且Contains在对索引器的调用之前进行调用可能会导致竞争条件,而该竞争条件不必存在TryGetValue
Daniel AA Pelsmaeker 2012年

@gbjbaanb-实验完成。我很懒,在Linux上使用单声道。异常在3秒内给了我约563000次罚球。 退货在3秒内给了我约10900000退货。那是1:20,甚至不是1:200。我仍然建议将1:100+用于任何更实际的代码。(如我所预料的那样,参数变型的成本可以忽略不计-我实际上怀疑,如果没有异常,无论签名如何,在我的简约示例中,抖动可能完全优化了呼叫消除。)
Jirka Hanika

@Virtlink-总体上同意线程安全,但是鉴于您特别提到了.NET 4,因此将其ConcurrentDictionary用于多线程访问和Dictionary单线程访问以实现最佳性能。也就是说,不使用``包含''不会使该特定Dictionary类的代码线程安全。
Jirka Hanika

10

在这种相对非常规的情况下,最好的办法是指示失败,为什么?

您不应该允许失败。

我知道,这是随波逐流且理想主义的,但请听我说。在进行设计时,在许多情况下,您有机会偏爱没有故障模式的版本。LINQ使用的where子句不会返回失败的'FindAll',而只是返回一个空的可枚举值。让构造函数初始化该对象(或在检测到未初始化时进行初始化),而不是使用之前需要对其进行初始化的对象。关键是删除使用者代码中的故障分支。这是问题所在,因此请专注于此。

另一个方案是KeyNotFound场景。从3.0开始,几乎在我工作过的每个代码库中,都存在以下扩展方法:

public static class DictionaryExtensions {
    public static V GetValue<K, V>(this IDictionary<K, V> arg, K key, Func<K,V> ifNotFound) {
        if (!arg.ContainsKey(key)) {
            return ifNotFound(key);
        }

        return arg[key];
    }
}

没有真正的故障模式。ConcurrentDictionary具有类似的GetOrAdd内置功能。

综上所述,总有一些时候是不可避免的。这三者都有自己的位置,但我倾向于第一种选择。尽管所有这些都是由null的危险造成的,但它是众所周知的,并且适合构成“非异常失败”集合的许多“未找到项目”或“结果不适用”方案。尤其是在创建可为空的值类型时,“这可能会失败”的含义在代码中非常明确,难以忘记/增加。

当用户做一些愚蠢的事情时,第二种选择就足够了。给您一个格式错误的字符串,尝试将日期设置为12月42日。。。您希望事情在测试过程中迅速且引人入胜,以便识别并纠正错误的代码。

最后一个选择是我越来越不喜欢的选择。输出参数很尴尬,并且在制定方法时倾向于违反一些最佳实践,例如专注于一件事而没有副作用。另外,外部参数通常仅在成功期间有意义。也就是说,它们对于通常受并发问题或性能考虑因素约束的某些操作(例如,您不想第二次访问数据库)是必不可少的。

如果返回值和out参数不平凡,则首选Scott Whitlock关于结果对象的建议(如Regex的Match类)。


7
这里的烦恼:out参数与副作用完全正交。修改ref参数是一种副作用,而修改通过输入参数传递的对象的状态是一种副作用,但是out参数只是使函数返回多个值的尴尬方式。 没有副作用,只有多个返回值。
Scott Whitlock

我说倾向于,因为人们如何使用它们。就像您说的那样,它们只是多个返回值。
Telastyn

但是,如果您不喜欢参数,并使用异常在格式错误时将异常引人注目...那么您如何处理格式为用户输入的情况?然后,要么用户将其炸毁,要么招致抛出然后捕获异常的性能损失。对?
Daniel AA Pelsmaeker 2012年

@virtlink,具有独特的验证方法。无论如何,您都需要它来向UI提供正确的消息,然后他们才提交。
Telastyn 2012年

1
out参数有一个合法的模式,该函数具有可返回不同类型的重载。重载解析不适用于返回类型,但适用于out参数。
罗伯特·哈维

2

总是喜欢抛出异常。它在所有可能失败的功能之间都有一个统一的接口,它尽可能多地指示失败-这是一个非常理想的属性。

请注意,除故障模式外,ParseTryParse并不是完全一样。实际上也TryParse可以返回值的事实有些正交。考虑一下您正在验证某些输入的情况。只要值是有效的,您实际上并不关心值是什么。提供一种IsValidFor(arguments)功能没有错。但是它永远不能成为主要的操作模式。


4
如果要处理对方法的大量调用,则异常会对性能产生巨大的负面影响。应该为例外情况保留例外,对于验证表单输入,例外是完全可以接受的,但对于从大文件中解析数字则不能接受。
罗伯特·哈维

1
那是更专业的需求,而不是一般情况。
DeadMG

2
所以你说。但是您确实使用了“始终”一词。:)
罗伯特·哈维

@DeadMG,与RobertHarvey同意,尽管我认为答案过分否决,如果修改它以反映“大多数时间”,然后指出关于经常使用的高性能的一般情况的例外情况(无双关)呼吁考虑其他选择。
杰拉尔德·戴维斯

例外并不昂贵。捕获深度抛出的异常可能很昂贵,因为系统必须将堆栈展开到最近的临界点。但是,这种“昂贵”的功能相对较小,即使在紧密的嵌套循环中也不应担心。
马修·怀特

2

正如其他人指出的那样,魔术值(包括布尔返回值)不是一个很好的解决方案,只是作为“范围结束”标记。原因:即使您检查对象的方法,语义也不是明确的。您必须实际阅读整个对象的完整文档,直到“哦,是的,如果它返回-42,则表示bla bla bla”。

出于历史原因或性能原因,可以使用此解决方案,但应避免使用。

剩下两个一般情况:探测或异常。

这里的经验法则是,程序不应对异常做出反应,除非处理/无意中/违反某些条件。应该使用探测来确保不会发生这种情况。因此,异常要么意味着未事先执行相关探测,要么发生了完全出乎意料的事情。

例:

您要从给定的路径创建文件。

您应该使用File对象预先评估此路径对于创建或写入文件是否合法。

如果您的程序仍然以某种方式最终尝试写入非法或不可写的路径,那么您应该得到说明。这可能是由于争用情况而发生的(某些用户在您查看问题后已删除目录或将其设置为只读)

处理意外失败(带有异常信号)并事先检查条件是否适合进行预先操作(探测)的任务通常会以不同的方式进行结构设计,因此应使用不同的机制。


0

Try当代码仅指示发生了什么时,我认为模式是最佳选择。我讨厌参数,喜欢可为空的对象。我创建了以下课程

public sealed class Bag<TValue>
{
    public Bag(TValue value, bool hasValue = true)
    {
        HasValue = hasValue;
        Value = value;
    }

    public static Bag<TValue> Empty
    {
        get { return new Bag<TValue>(default(TValue), false); }
    }

    public bool HasValue { get; private set; }
    public TValue Value { get; private set; }
}

所以我可以写下面的代码

    public static Bag<XElement> GetXElement(this XElement element, string elementName)
    {
        try
        {
            XElement result = element.Element(elementName);
            return result == null
                       ? Bag<XElement>.Empty
                       : new Bag<XElement>(result);
        }
        catch (Exception)
        {
            return Bag<XElement>.Empty;
        }
    }

看起来像可以为空,但不仅限于值类型

另一个例子

    public static Bag<string> TryParseString(this XElement element, string attributeName)
    {
        Bag<string> attributeResult = GetString(element, attributeName);
        if (attributeResult.HasValue)
        {
            return new Bag<string>(attributeResult.Value);
        }
        return Bag<string>.Empty;
    }

    private static Bag<string> GetString(XElement element, string attributeName)
    {
        try
        {
            string result = element.GetAttribute(attributeName).Value;
            return new Bag<string>(result);
        }
        catch (Exception)
        {
            return Bag<string>.Empty;
        }
    }

3
try catch如果您GetXElement()多次打电话失败,将会对您的表现造成严重破坏。
罗伯特·哈维

有时候没关系。看一下Bag call。感谢您的观察

您的Bag <T> clas几乎与System.Nullable <T>也称为“ nullable对象”相同
Aeroson

是的,几乎public struct Nullable<T> where T : struct是约束的主要区别。顺便说一句,最新版本在这里github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…–
GSerjo

0

如果您对“魔术值”路线感兴趣,那么解决此问题的另一种方法是重载Lazy类的目的。尽管Lazy旨在推迟实例化,但并没有真正阻止您使用Maybe或Option的方法。例如:

    public static Lazy<TValue> GetValue<TValue, TKey>(
        this IDictionary<TKey, TValue> dictionary,
        TKey key)
    {
        TValue retVal;
        if (dictionary.TryGetValue(key, out retVal))
        {
            var retValRef = retVal;
            var lazy = new Lazy<TValue>(() => retValRef);
            retVal = lazy.Value;
            return lazy;
        }

        return new Lazy<TValue>(() => default(TValue));
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.