有时我最终不得不为类库编写方法或属性,对于该类库,没有真正的答案不是偶然,而是失败。无法确定,无法找到,无法找到,当前无法实现或没有更多数据可用的方法。 C#4:


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

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

我正在寻找实际的答案,例如涉及设计准则的答案(我不知道Try*方法),可用性(如我所要求的类库),与BCL的一致性和可读性。



返回没有其他含义的魔术值:


Collection<T>.IndexOf返回-1,

StreamReader.Read返回-1,

Math.Sqrt返回NaN,

Hashtable.Item返回null;


抛出异常:


Dictionary<,>.Item引发KeyNotFoundException,

Double.Parse引发FormatException;或


返回false并在out参数中提供实际的返回值:



请注意,由于在C#中没有泛型时创建了Dictionary<,>.TryGetValue,因此它使用Double.TryParse,因此可以将Hashtable作为魔术值返回。但是对于泛型,在object中使用了异常,并且最初它没有null。显然,见解发生了变化。

显然,存在对偶性Dictionary<,>-TryGetValueItem-TryGetValue对偶是有原因的,所以我认为在C#4中不会抛出非异常失败的异常。但是,即使存在ParseTryParse方法也不总是存在。

评论

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

我并不是说您的问题太笼统,不是建设性的问题。没有标准答案,因为答案已经显示出来。

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

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

这个问题属于这里,不是因为它是主观的,而是因为它是概念性的。经验法则:如果您坐在IDE编码的前面,请在Stack Overflow上询问。如果您正站在白板上集思广益,请在此处通过程序员进行询问。

#1 楼

我不认为您的示例确实等效。有3个不同的组,每个组都有其行为依据。


当出现“直到”情况(例如StreamReader.Read)或简单时使用永远不会是有效答案的值(对于IndexOf,则为-1)。
当函数的语义是调用者确定它将起作用时,抛出异常。在这种情况下,不存在密钥或错误的双精度格式确实是例外。
如果语义是要探查是否可以进行操作,请使用out参数并返回布尔值。

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

NaN返回的Math.Sqrt是一个特例-它遵循浮点标准。

评论


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

– 0b101010
16-2-1在17:53



但是Either 单子呢?

–萨拉
16年5月20日在19:04

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

– jmoreno
18年2月18日在1:16

#2 楼

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

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

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

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

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

评论


就我个人而言,我经常使用类似于您的IMyResult的结果容器,因为它可能传达比真或假或结果值更复杂的结果。 Try *()仅对简单的东西有用,例如从字符串到int的转换。

–this.myself
2015年6月2日,7:14

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

–海洋空投
16年1月1日在8:54



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

–卷
16-10-18在23:25

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

– drizin
17年7月15日在7:23

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

–冬天
18年7月18日在21:34



#3 楼

如您的示例所示,每种情况都必须分别进行评估,并且在“特殊情况”和“流控制”之间存在相当大的灰度范围,尤其是如果您的方法旨在可重用并且可能以完全不同的方式使用比最初设计的要好。不要指望这里的所有人都同意“非例外”的含义,尤其是如果您立即讨论使用“例外”来实现这一点的可能性。该代码最容易阅读和维护,但我将假定图书馆设计者对此有清晰的个人见解,只需要在涉及的其他注意事项之间取得平衡即可。

简短答案

请遵循自己的直觉,除非您正在设计相当快速的方法,并期望有无法预料的重用。双向的异常;这使得这两种设计方法几乎相同,除了性能,调试器友好性和某些受限制的互操作性上下文之外。这通常归结为性能,因此,我们将重点放在性能上。


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

那么,教训是什么?

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

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

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

可重复使用的精益功能需要更多的考虑。通过使用异常,如果函数的主体执行得非常快,则您将使调用者的速度降低100倍,这些调用者将在一半(多次)调用中看到异常。例外仍然是一种设计选择,但是您将不得不为负担不起此费用的呼叫者提供一种开销较低的替代方法。让我们看一个例子。

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


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


评论


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

– gbjbaanb
2012年8月2日在10:20

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

–Jirka Hanika
2012年8月2日,11:51



那么,ref / out参数的成本是否很高?为何如此?并且在对索引器的调用之前调用Contains可能会导致TryGetValue不必存在的竞争条件。

– Daniel A.A.佩尔斯迈克
2012年8月2日在20:22

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

–Jirka Hanika
2012年8月2日在20:49

@Virtlink-总体上同意线程安全,但是鉴于您特别提到了.NET 4,请对多线程访问使用ConcurrentDictionary,对单线程访问使用Dictionary以获得最佳性能。也就是说,不使用``Contains`不会使该特定Dictionary类的代码线程安全。

–Jirka Hanika
2012年8月2日在20:53



#4 楼


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


您不应允许失败。 />我知道,这有点易事和理想,但请听我说。在进行设计时,在许多情况下,您有机会偏爱没有故障模式的版本。 LINQ使用的where子句不会返回失败的'FindAll',而只是返回一个空的可枚举值。让构造函数初始化该对象(或在检测到未初始化时进行初始化),而不是使用之前需要对其进行初始化的对象。关键是删除使用者代码中的故障分支。这是问题所在,因此请专注于此。从3.0开始,几乎在我开发的每个代码库中,都存在类似这种扩展方法的东西:
KeyNotFound具有类似的内置ConcurrentDictionary

总而言之,有时总是无法避免的。这三者都有自己的位置,但是我倾向于第一种选择。尽管所有这些都是由null的危险造成的,但它是众所周知的,并且适合构成“非异常失败”集合的许多“未找到项目”或“结果不适用”方案。尤其是当您创建可为空的值类型时,“这可能会失败”的含义在代码中非常明确,难以忘记/增加。有点蠢。为您提供格式错误的字符串,尝试将日期设置为12月42日...您希望在测试过程中快速迅速地删除错误内容,以便识别并修复错误的代码。

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

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

评论


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

–斯科特·惠特洛克
2012年8月2日在12:07

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

– Telastyn
2012年8月2日,12:58

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

– Daniel A.A.佩尔斯迈克
2012年8月2日在20:26

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

– Telastyn
2012年8月2日在20:52

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

–罗伯特·哈维(Robert Harvey)
2012年8月3日在5:13



#5 楼

总是喜欢抛出异常。它在所有可能发生故障的功能之间具有统一的接口,并且它尽可能多地指示出故障-这是一个非常理想的属性。 。 Parse也可以返回值的事实确实有些正交。考虑一下您正在验证某些输入的情况。只要值是有效的,您实际上并不关心值是什么。提供一种TryParse功能没有错。但是它永远不能成为主要的操作模式。

评论


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

–罗伯特·哈维(Robert Harvey)
2012年8月2日,3:24

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

– DeadMG
2012年8月3日在4:30

所以你说。但是您确实使用了“始终”一词。 :)

–罗伯特·哈维(Robert Harvey)
2012年8月3日5:14



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

–杰拉德·戴维斯(Gerald Davis)
14年4月12日在16:19

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

–马修·怀特(Matthew Whited)
19年7月2日在7:53

#6 楼

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

出于历史原因或出于性能原因,可能会使用此解决方案,但应避免使用此解决方案。 br />这里的经验法则是,程序不应对异常做出反应,除非程序/无意/违反了某些条件。应该使用探测来确保不会发生这种情况。因此,异常要么意味着没有事先执行相关的探测,要么发生了完全出乎意料的事情。

示例:给定的路径。

您应该使用File对象预先评估该路径对于创建或写入文件是否合法。到非法或其他不可写的路径,您应该得到一个摘录。这可能是由于争用情况而发生的(某些问题,在您有问题后,其他用户已删除目录或将其设置为只读)。如果条件适合进行预先操作(探测),通常将采用不同的结构,因此应使用不同的机制。

#7 楼

我认为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;
        }
    }


评论


如果您调用GetXElement()并失败多次,则try catch将严重破坏您的性能。

–罗伯特·哈维(Robert Harvey)
2012年8月1日在22:19

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

–GSerjo
2012年8月1日22:30

您的Bag clas与System.Nullable 又名“ nullable对象”相同

– Aeroson
17年2月2日,11:26



是的,几乎是公共结构Nullable ,其中T:构造约束中的主要区别。顺便说一句,最新版本在这里github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…

–GSerjo
17年2月2日在16:41

#8 楼

如果您对“魔术值”路线感兴趣,那么解决此问题的另一种方法是重载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));
    }