引发异常或让代码失败


52

我想知道是否有反对这种风格的利弊:

private void LoadMaterial(string name)
{
    if (_Materials.ContainsKey(name))
    {
        throw new ArgumentException("The material named " + name + " has already been loaded.");
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );
}

对于每个方法,该方法name只能运行一次。_Materials.Add()如果同一对象将被多次调用,将抛出异常name。结果是我的后卫完全多余了,还是有一些不太明显的好处?

如果有人感兴趣,那就是C#,Unity。


3
如果在没有防护装置的情况下两次装载材料会怎样?是否会_Materials.Add引发异常?
user253751

2
实际上,我已经提到过抛出异常:P
异步

9
旁注:1)考虑使用string.Formatover字符串连接来构建异常消息。2)仅as在您期望强制转换失败并检查的结果时使用null。如果您希望总是得到a Material,请使用(Material)Resources.Load(...)。与之后发生的空引用异常相比,清除强制转换异常更容易调试。
CodesInChaos

3
在这种特定情况下,您的呼叫者也可能会发现有用的同伴LoadMaterialIfNotLoadedReloadMaterial(此名称可能会使用改进)方法。
jpmc26 2015年

1
另外请注意:这种模式可以用于有时将项目添加到列表中。但是,请勿使用此模式来填充整个列表,因为您会遇到O(n ^ 2)的情况,在这种情况下,使用大列表会遇到性能问题。您不会在100个条目中注意到它,但是在1000个条目中您肯定会注意到它,而在10.000个条目中它将是一个主要的瓶颈。
Pieter B

Answers:


109

这样做的好处是,您的“自定义”异常中包含一条错误消息,该消息对于调用此函数的任何人都有意义,而不知道其实现方式(将来可能是您!)。

当然,在这种情况下,他们可能能够猜测“标准”异常的含义,但您仍要清楚表明他们违反了您的合同,而不是偶然发现代码中的一些奇怪错误。


3
同意。对于先决条件违反,抛出异常几乎总是更好的选择。
Frank Hileman

22
“这样做的好处是,您的“自定义”异常中包含一条错误消息,该错误消息对于任何调用此函数而不知道其实现方式的人都有意义(将来可能是您!)。” 最佳建议。
2015年

2
“显式比隐式好。” - Python的禅
jpmc26

4
即使抛出的错误_Materials.Add不是您要传递给调用方的错误,我认为这种重新标记错误的方法效率也不高。对于(正常操作)的每次成功调用,LoadMaterial您将在中进行两次相同的健全性测试,LoadMaterial然后在中进行一次_Materials.Add。如果包装了更多的层,每个层都使用相同的样式,则您甚至可以进行更多次相同的测试。
Marc van Leeuwen

15
...而是考虑_Materials.Add无条件地陷入,然后捕获潜在的错误并在处理程序中抛出另一个错误。现在,仅在错误的情况下才需要执行额外的工作,这是您完全不必担心效率的异常执行路径。
Marc van Leeuwen

100

我同意Ixrec的回答。但是,您可能要考虑第三种选择:使函数幂等。换句话说,请尽早返回,而不要抛出ArgumentException。如果通常会在LoadMaterial每次调用之前强制您检查它是否已经加载,则通常更可取。前提条件越少,程序员的认知负担就越小。

如果确实是程序员错误,则抛出异常将是可取的选择,在这种情况下,应该很明显并在编译时就知道是否已经加载了材质,而不必在运行时在调用之前进行检查。


2
另一方面,使函数静默失败会导致意外行为,从而导致以后更难发现错误。
菲利普

30
@Philipp函数不会静默失败。幂等意味着多次运行与一次运行具有相同的效果。换句话说,如果材料已经加载,则我们的规格(“材料应加载”)已经满足,因此我们无需执行任何操作。我们可以返回。
华宝(Warbo)2015年

8
实际上,我更喜欢这一点。当然,根据应用程序要求给出的约束,这可能是错误的。再说一次,我绝对支持幂等方法。
FP 2015年

6
如果我们考虑@Philipp,它LoadMaterial具有以下约束:“在调用它之后,始终加载材料,并且加载的材料数量没有减少”。对于第三个约束“该材料不能添加两次”抛出异常对我来说似乎很愚蠢且违反直觉。使函数幂等可减少代码对执行顺序和应用程序状态的依赖。对我来说似乎是双赢。
本杰明·格伦鲍姆

7
我喜欢这个主意,但建议做一个较小的更改:返回一个布尔值,反映是否已加载任何内容。如果调用方确实关心应用程序逻辑,则他们可以检查并引发异常。
user949300

8

您必须问的基本问题是:您希望函数的接口是什么?尤其是在状态_Materials.ContainsKey(name)的功能的前提条件?

如果不是前提条件,则该函数必须为的所有可能值提供明确定义的结果name。在这种情况下,如果name不属于该异常,则抛出的异常_Materials成为函数接口的一部分。这意味着它需要成为接口文档的一部分,如果您将来决定更改该异常,那将是一个重大的接口更改

更有趣的问题是,如果这是前提条件,将会发生什么。在这种情况下,前提条件本身变成了功能接口的一部分,但是,当这个条件被破坏功能的行为方式是不是一定是接口的一部分。

在这种情况下,您发布的检查先决条件违规并报告错误的方法就是防御性编程。防御性编程的好处在于,它可以在用户犯错并以伪参数调用函数时尽早通知用户。不利之处在于,这会大大增加维护负担,因为用户代码可能依赖于该函数以某种方式处理前提条件违规。特别是,如果您发现运行时检查将来会成为性能瓶颈(这种情况不太可能发生,但对于更复杂的前提条件却很常见),则可能无法再删除它。

事实证明,这些缺点可能非常显着,这使防御性编程在某些圈子中声誉不佳。但是,最初的目标仍然有效:我们希望函数的用户在犯错时尽早注意到。

因此,当今许多开发人员针对此类问题提出了一种略有不同的方法。他们没有抛出异常,而是使用类似断言的机制来检查前提条件。也就是说,可以在调试版本中检查前提条件,以帮助用户尽早发现错误,但前提条件不是功能界面的一部分。乍一看,这种差异似乎微妙,但在实践中却可以带来巨大的改变。

从技术上讲,调用具有先决条件的函数是未定义的行为。但是实现可能决定检测这些情况,并在发生这种情况时立即通知用户。不幸的是,异常不是实现此目的的好工具,因为用户代码可以对异常做出反应,因此可能开始依赖它们的存在。

有关经典防御方法存在的问题的详细说明,以及断言式前提条件检查的可能实现,请参阅John Lakos在CppCon 2014上发表的演讲“ Defensive Programming Right Right”幻灯片视频)。


4

这里已经有一些答案,但是这里的答案考虑了Unity3D(答案是非常特定于Unity3D的,在大多数情况下,我会以不同的方式进行大多数操作):

通常,Unity3D传统上不使用异常。如果您在Unity3D中引发异常,则不会像普通的.NET应用程序中那样,即它不会停止程序,因此您最多可以将编辑器配置为暂停。它将被记录下来。这很容易使游戏处于无效状态,并产生级联效应,使错误很难被追踪。因此,在Unity的情况下,Add抛出异常是一个特别不可取的选择。

但是,在某些情况下,由于Mono在某些平台上如何在Unity中工作,因此在检查异常的速度时并不是某些情况下的过早优化。实际上,iOS上的Unity3D支持某些高级脚本优化,而禁用的例外*是其中之一的副作用。这确实是要考虑的事情,因为这些优化对许多用户而言已被证明是非常有价值的,这表明了考虑限制在Unity3D中使用异常的现实情况。(*来自引擎的托管异常,不是您的代码)

我想说的是,在Unity中,您可能想采用更专业的方法。具有讽刺意味的是,在撰写本文时一个非常不赞成的答案表明了我可能会在Unity3D的上下文中专门实现这种方式的一种方式(在其他地方,这种方式确实是不可接受的,甚至在Unity中也相当不雅)。

我考虑的另一种方法实际上并不是在调用者关注的范围内指示错误,而是使用Debug.LogXX函数。这样,您得到的行为与抛出未处理的异常(由于Unity3D如何处理它们)的行为相同,而不必冒险将某些事物置于异常状态。还要考虑这是否真的是错误的(在您的情况下尝试两次加载相同的材料必然会导致错误吗?或者这可能Debug.LogWarning是更适用的情况)。

关于使用Debug.LogXX函数而不是异常之类的东西,您仍然必须考虑当从返回值的东西(如GetMaterial)抛出异常时会发生什么。我倾向于通过传递null并记录错误来解决此问题(同样,仅在Unity中)。然后,我在MonoBehaviors中使用null检查,以确保像材料这样的任何依赖项都不是null值,如果是,则禁用MonoBehavior。需要一些依赖项的简单行为示例如下所示:

    public void Awake()
    {
        _inputParameters = GetComponent<VehicleInputParameters>();
        _rigidbody = GetComponent<Rigidbody>();
        _rigidbodyTransform = _rigidbody.transform;
        _raycastStrategySelector = GetComponent<RaycastStrategySelectionBehavior>();

        _trackParameters =
            SceneManager.InstanceOf.CurrentSceneData.GetValue<TrackParameters>();

        this.DisableIfNull(() => _rigidbody);
        this.DisableIfNull(() => _raycastStrategySelector);
        this.DisableIfNull(() => _inputParameters);
        this.DisableIfNull(() => _trackParameters);
    }

SceneData.GetValue<>与您的示例类似,它在字典上调用引发异常的函数。但是它没有抛出异常,而是Debug.LogError像正常异常那样使用了堆栈跟踪并返回null。*后面的检查将禁用该行为,而不是让它继续以无效状态存在。

*检查看起来像是因为我使用了一个小的助手,当它禁用游戏对象时会打印出格式化的消息**。简单的空值检查if可以在这里工作(**帮助程序的检查仅在Debug版本中编译(如asserts)。在Unity中使用lambda和表达式可能会降低性能)


1
+1,用于向其中添加一些真正的新信息。我没想到这一点。
Ixrec

3

我喜欢两个主要答案,但想建议您可以改善函数名称。我已经习惯了Java,所以使用YMMV,但是IMO如果该项目已经存在,则不应该引发Exception。它应该再次添加该项目,或者,如果目的地是Set,则什么也不做。由于那不是Materials.Add的行为,因此应将其重命名为TryPut或AddOnce或AddOrThrow或类似名称。

同样,应将LoadMaterial重命名为LoadIfAbsent或Put或TryLoad或LoadOrThrow(取决于您使用答案#1或#2)还是类似的名称。

遵循C#Unity命名约定。

如果您还有其他AddFoo和LoadBar函数可以允许两次加载相同的东西,这将特别有用。没有明确的名称,开发人员将感到沮丧。


C#Dictionary.Add()语义(请参阅msdn.microsoft.com/en-us/library/k7z0zy8k%28v=vs.110%29.aspx)和Java Collection.add()语义之间有区别。 oracle.com/javase/8/docs/api/java/util / ...)。C#返回void并引发异常。而Java返回bool并将先前存储的值替换为新值,或者在无法添加新元素时抛出异常。
卡斯珀范登伯格2015年

卡巴斯尔-谢谢,有趣。如何替换C#词典中的值?(等效于Java put())。在该页面上看不到内置方法。
user949300

好问题,可以做Dictionary.Remove()(请参阅msdn.microsoft.com/en-us/library/bb356469%28v=vs.110%29.aspx),然后再做Dictionary.Add()(请参阅 msdn.microsoft .com / en-us / library / bb338565%28v = vs.110%29.aspx),但这看起来有点尴尬。
卡斯珀范登伯格2015年

@ user949300 dictionary [key] =值将替换条目(如果该条目已存在)
Rupe

@Rupe,如果没有,可能会抛出一些东西。一切对我来说似乎都很尴尬。大多数客户建立字典不在乎wheteher的条目在那里,他们只是关心,他们的安装程序代码后,它有。在Java Collections API(IMHO)上得分一。PHP的字典也很笨拙,遵循该先例是C#的错误。
user949300

3

所有答案都添加了宝贵的想法,我想将它们结合起来:

确定操作的预期和预期语义LoadMaterial()。至少存在以下选项:

  • 上一个先决条件nameLoadedMaterials:→

    当前提条件被违反,效果LoadMaterial()是不确定的(如答案ComicSansMS)。这允许在实施和将来更改时有最大的自由度LoadMaterial()。要么,

  • 调用的效果LoadMaterial(name)nameLoadedMaterials指定; 要么:

确定语义后,必须选择一个实现。建议以下选项和注意事项:

  • 抛出一个自定义异常(如建议通过Ixrec)→

    好处是您的“自定义”异常中包含一条错误消息,该消息对于调用此函数的任何人(IxrecUser16547)都有意义

    • 为了避免重复检查name加载材料的成本,您可以按照Marc van Leeuwen的建议进行操作:

      ...而是考虑无条件地插入_Materials.Add,然后捕获潜在的错误并在处理程序中抛出另一个错误。

  • Dictionay.Add抛出异常→

    异常抛出代码是多余的。— 乔恩·雷诺Jon Raynor)

    虽然,大多数选民更同意Ixrec

    选择此实现的其他原因是:

    • 调用者已经可以处理ArgumentException异常,并且
    • 避免丢失堆栈信息。

    但是,如果这两个原因很重要,您还可以从中派生自定义异常,ArgumentException并将原始异常用作链式异常。

  • LoadMaterial()幂等作为anwser卡尔比勒费尔特和upvoted最多(75次)。

    此行为的实现选项:

    • 检查与Dictionary.ContainsKey()

    • 当要插入的键已经存在并且忽略该异常时,请始终调用Dictionary.Add()捕获ArgumentException引发的异常。记录一下,忽略异常是您的意图以及原因。

      • 如果LoadMaterials()是(几乎)总是为每调用一次name,这样就避免了重复检查的费用nameLoadedMaterials比照 马克·凡·莱恩。然而,
      • LoadedMaterials()常被多次调用 时name,这会导致抛出ArgumentException和展开堆栈的(昂贵)成本 。
    • 我认为存在TryAddTryGet()类似的-method方法, 它可以让您避免昂贵的异常抛出和对Dictionary.Add失败调用的堆栈展开。

      但是,该TryAdd方法似乎不存在。


1
+1是最全面的答案。这可能远远超出了OP最初的要求,但是它是所有选项的有用总结。
Ixrec 2015年

1

异常抛出代码是多余的。

如果您致电:

_Materials.Add(name,Resources.Load(string.Format(“ Materials / {0}”,name))作为材料

使用相同的键,它将抛出一个

System.ArgumentException

消息将是:“具有相同的密钥的项目已被添加。”

ContainsKey检查是多余的,因为如果没有检查,基本上可以实现相同的行为。唯一不同的是例外中的实际消息。如果拥有自定义错误消息确实有好处,那么保护代码就有一些优点。

否则,在这种情况下,我可能会放松警惕。


0

我认为这更多是关于API设计的问题,而不是编码约定问题。

致电的预期结果(合同)是什么:

LoadMaterial("wood");

如果调用者可以期望/保证在调用此方法之后,将加载“木材”材质,那么在该材质已经加载时,我看不到任何引发异常的原因。

例如,如果在加载物料时发生错误,则无法打开数据库连接,或者在存储库中没有物料“木材”,那么抛出异常以通知调用者该问题是正确的。


-2

为什么不更改方法,以使其在添加了物料时返回“ true”,如果没有则返回“ false”?

private bool LoadMaterial(string name)
{
   if (_Materials.ContainsKey(name))

    {

        return false; //already present
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );

    return true;

}

15
返回错误值的函数正好是应该使异常过时的反模式。
菲利普

总是这样吗?我认为只有半谓词(en.wikipedia.org/wiki/Semipredicate_problem)就是这种情况,因为在OP代码示例中,不向系统中添加材料并不是真正的问题。该方法旨在确保您在运行物料时该物料在系统中,而这正是此方法的作用。(即使失败),返回的布尔值仅指示该方法是否已尝试添加此问题。ps:我没有考虑到可能有多个同名的物质。
MrIveck

1
我想我自己找到了解决方案:en.wikipedia.org/wiki/…这指出Ixrec的答案是最正确的
MrIveck

@Philipp如果编码器/规范确定两次尝试加载时没有错误。从这个意义上说,返回值不是错误值,而是参考值。
user949300
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.