.NET异常处理有多重?

Vit*_*kov 6 .net c# error-handling try-catch

有时我会遇到这种情况,因为在try-catch块中包装整段代码要相当容易,而不是进行大量检查会严重降低代码的可读性.例如,这个

var result = string.Empty;
if (rootObject != null)
{
    if (rootObject.FirstProperty != null)
    {
        if (rootObject.FirstProperty.SecondProperty != null)
        {
            if (!string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString))
            {
                result = rootObject.FirstProperty.SecondProperty.InterestingString;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我真的更喜欢这样做

var result = string.Empty;
try
{
    result = rootObject.FirstProperty.SecondProperty.InterestingString;
}
catch { }
Run Code Online (Sandbox Code Playgroud)

但是在代码审查之后,我经常听到我的导师说我应该避免使用try-catch块来进行简单的检查.它是否真的如此重要,每个try-catch块都会占用大量的系统资源(相对而言)?这些资源是仅在引发错误或每种情况(成功或不成功)同样"重"时使用的吗?

Eri*_*ert 17

异常是重量级还是轻量级是完全无关紧要的.可以轻易防止的抛出异常是一个错误.不要抓住异常:修复bug,这样你就不必了.

  • @VitaliiKorsakov:你错过了我的观点.我再说一遍:**异常是否轻量级是无关紧要的.将所有可避免的异常视为错误**.对于例外,这是正确和正确的态度:它们是*例外*并表明某些事情是错误的*.您应该能够在调试器中打开"中断所有处理的异常"并且*每次调试器中断*时,这都是一个错误. (4认同)
  • @EricLippert'您应该能够在调试器中打开"中断所有已处理的异常",并且每次调试器都会中断,这是一个错误. - 使用第三方库时,这是不可能的; 有些是关于例外的设计不佳.另一个经典示例是I/O,您可以在读取之前检查文件是否存在,但是另一个用户可以在检查和实际读取文件之间删除文件. (2认同)
  • @phoog:至于你的第一点:其他人的错误图书馆的存在是不让问题变得更糟的一个很好的理由.开始向他们报告错误.至于你的第二点:你所描述的外生异常是*不可避免的*,但仍然是*例外*.在调试时偶然发生这种事情的可能性非常小. (2认同)

Bev*_*van 8

.NET框架中的异常相对较重 - 为避免它们而需要付出一些努力.毕竟,它们被称为例外 - 它们不应该是常见的.

也就是说,它们远没有人们想象的那么昂贵.

在Visual Studio下进行调试时,处理异常可能需要几秒钟,因为IDE会显示捕获异常的行.因此,有些人认为每个例外都需要几秒钟来处理,因此必须不惜一切代价避免它们.

我看到人们因为每小时抛出几十个异常这一事实而将系统性能指责得很糟糕.

这是一个神话.该系统完全能够每秒抛出和捕获数千个异常.

也就是说,您可以使用以下几个简单的扩展函数来整理原始代码:

var result
    = rootObject.With( r => FirstProperty)
          .With( r => r.SecondProperty)
          .Return( r => r.InterestingString, string.Empty);
Run Code Online (Sandbox Code Playgroud)

With这个定义在哪里:

    public static TResult With<TInput, TResult>(
        this TInput o, 
        Func<TInput, TResult> evaluator)
        where TResult : class
        where TInput : class
    {
        if (o == null)
        {
            return null;
        }

        return evaluator(o);
    }
Run Code Online (Sandbox Code Playgroud)

Return有这样的:

    public static TResult Return<TInput, TResult>(
        this TInput o, 
        Func<TInput, TResult> evaluator, 
        TResult defaultValue) 
        where TInput : class
    {
        if (o == null)
        {
            return defaultValue;
        }

        return evaluator(o);
    }
Run Code Online (Sandbox Code Playgroud)

  • 名称*是一个挑战 - 我发现有一整套方法的名称与十年前的旧动词/名词惯例不相符.我没有比With()更好的东西 - 像`EvaluateIfNotNull()这样的描述性名称引入了太多的噪音,它比我们试图治愈的问题更糟糕. (2认同)

Mer*_*ham 5

异常处理

我不担心这样的代码中"有多重"异常.我稍后会解释原因.

例外情况适用于特殊情况.拥有异常处理程序意味着您希望这些属性永远不会为空.

何时写保护条件

这些房产是否常见null

如果是这样,这不是特殊情况.您应该编写适当的空测试,并将异常处理程序代码更改为空条件代码.

那些属性是不常见的null,即你能合理地期望它们永远不会是null吗?

如果是这样,你可以简单地避免编写null检查,只是让底层代码抛出.但是,您不会获得有关哪个属性引发异常的大量背景信息.

您还可以执行null检查并抛出更具特定于上下文的异常.

何时编写异常处理程序

如果这些属性不常见null,那么这是一种特殊情况.但这并不意味着你应该有一个处理程序.

您是否有一种简单的方法来测试异常情况?

如果是这样,那么你应该在允许你使用的底层代码抛出异常之前测试它.既然你只需要检查null,我会说这很容易.

你有合理的逻辑来处理这个案件吗?

如果您有合理的方法来处理该级别的异常情况,并且仍然保证您的方法正确执行,那么继续并添加处理程序代码.如果你依赖于像返回这样的机制null,那么从消费者的角度来看,确定它们不会总是得到结果是有道理的.例如,命名方法FindInterestingString而不是GetInterestingString.

如果您没有合理的方法来处理这种情况,请不要将异常处理程序放在该级别.让您的异常冒泡并在代码中的更高位置处理它.

如果你没有合理的方法来处理异常,那就让程序崩溃吧.这总是比吞咽异常并继续下去更好.这隐藏了错误.

这些规则的例外情况

有时你不能轻易地测试条件而不抛出异常.

外部依赖项(如文件系统)将在程序下更改.即使您进行了预测试,即使预测试通过,一旦您尝试使用该对象,也可能会抛出异常.在这种情况下,您无法对此做任何事情,并且必须依赖异常处理.

像电子邮件地址和URI这样的复杂验证可能要求您使用抛出异常的构造.然后,它可能不会.您应始终寻找最合适的方法来执行与您的意图相匹配的错误处理.只有在必要时才弯曲以使用异常处理.

性能

性能不太可能是错误检测代码中的问题.

性能在高使用率代码(当您编写框架时),应用程序中的瓶颈以及已知为CPU /内存密集型的算法中都很重要.您应该知道何时担心perf,但它应该始终是代码的可读性,可维护性和正确性的次要问题.

您会发现无法完全预测整个应用程序中的性能问题.获得准确图片的唯一方法是在现实条件下使用逼真的场景运行代码并对其进行分析.在你开发一个应用程序之前,你不应该担心perf,除非你知道这将是一个问题.

使用例外情况并不像许多人可能认为的那样高.在.Net中,它们被设计为在不抛出异常时表现良好.这就是例外是针对特殊情况的事实.


您的代码示例

您提供的代码示例还有一些其他问题.希望我能在你被他们困住之前指出其中的一些.如果没有,希望您在遇到问题时可以回顾这些指导.

编写异常处理程序

您为异常处理程序编写的代码根本不可接受.以下是编写更好的异常处理程序代码的一些指导:

坏:

try
{
}
catch // Note: Doesn't catch `Exception e`
{
    // ... eats the exeption
}
Run Code Online (Sandbox Code Playgroud)

这是不好的形式,永远不应该使用.绝对没有办法正确处理所有异常类型.最常用的例子是OutOfMemoryException.

可能接受:

try
{
}
catch(Exception e)
{
    logger.Log(e.ToString());
    // ... eats the exeption
}
Run Code Online (Sandbox Code Playgroud)

如果你捕获异常并记录或显示它,那么吃掉异常可能是可以的.如果您正在积极监控/报告这些异常,并且有办法保证将诊断出这些异常,那么这是可以的.

好的:

try
{
}
catch(Exception e)
{
    logger.Log(e.ToString()); // Make sure your logger never throws...
    throw; // Note: *not* `throw e;`
}

// Or:

try
{
}
catch
{
    // Todo: Do something here, but be very careful...
    throw;
}
Run Code Online (Sandbox Code Playgroud)

如果您非常小心不创建新的异常,并且重新抛出异常,则可以在异常处理程序中执行任何操作.这将保证错误得到注意.如果你重新抛出一个异常,请确保使用throw;而不是throw e;,否则你的原始堆栈跟踪将被销毁.

好:

try
{
}
catch(NullReferenceException e)
{
    // ... Do whatever you want here ...
}
Run Code Online (Sandbox Code Playgroud)

这是安全的,因为您只捕获已知由try块中的代码抛出的某些异常类型.很容易理解代码的意图,并且易于代码审查.很容易理解异常处理程序代码是否正常.

避免重复代码

如果可以避免,请不要重新访问属性.而不是编写访问您的属性的代码,如下所示:

rootObject ...
rootObject.FirstProperty ...
rootObject.FirstProperty.SecondProperty ...
rootObject.FirstProperty.SecondProperty.InterestingString ...
Run Code Online (Sandbox Code Playgroud)

...只召唤一次吸气剂:

var firstProperty = rootObject.FirstProperty;
var secondProperty = firstProperty.SecondProperty;
var interestingString = secondProperty.InterestingString;
Run Code Online (Sandbox Code Playgroud)

您的代码示例将更像这样:

if (rootObject != null)
{
    var firstProperty = rootObject.FirstProperty;

    if (firstProperty != null)
    {
        var secondProperty = firstProperty.SecondProperty;

        if (secondProperty != null)
        {
            var interestingString = secondProperty.InterestingString;

            if (!string.IsNullOrEmpty(interestingString))
            {
                result = interestingString;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这样做的一个原因是getter可能具有复杂的逻辑,并且多次调用它可能会对性能产生影响.

另一个原因是你不要重复自己.当代码没有大量重复时,代码总是更具可读性.

当您重复自己时,可维护性也会受到影响.如果更改其中一个属性的名称,则必须更改它出现的每行代码,这将使得更难以推断更改的影响.

避免深入挖掘依赖层次结构

您应该在同一方法中避免链接属性访问.即:

rootObject.FirstProperty.SecondProperty.InterestingString
Run Code Online (Sandbox Code Playgroud)

即使你把它分开以避免重复自己(就像我上面提到的那样),你仍然可能没有正确地考虑你的代码.您的代码仍然紧密耦合到该数据结构的层次结构.每次更改该层次结构时,都需要更改遍历该层次结构的任何代码.如果这是你的所有代码,那么你的状态就糟糕了.

为避免这种情况,请将了解每个级别的代码与其下级别分开.

处理根对象的代码应该只调用直接处理根目录下的对象的代码.处理的代码FirstProperty应该只知道SecondProperty级别(下FirstProperty)的属性.应该知道的唯一代码InterestingString是返回的对象类型的处理程序代码SecondProperty.

一种简单的方法是将遍历代码拆分,然后将其移动到对象本身.

看到:

拆分逻辑的示例代码:

public class SomeClassUsingRoot
{
    public string FindInterestingString()
    {
        return root != null
            ? root.FindInterestingString()
            : null;
    }

    private RootSomething root;
}

public class RootSomething
{
    public string FindInterestingString()
    {
        return FirstProperty != null
            ? FirstProperty.FindInterestingString()
            : null;
    }

    public SomethingTopLevel FirstProperty { get; set; }
}

public class SomethingTopLevel
{
    public string FindInterestingString()
    {
        return SecondProperty != null
            ? SecondProperty.InterestingString
            : null;
    }

    public SomethingLowerLevel SecondProperty { get; set; }
}

public class SomethingLowerLevel
{
    public string InterestingString { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

这不是解决问题的唯一方法.关键是将处理每个级别的逻辑拆分为单独的方法,或者(甚至更好)单独的对象.这样,当层次结构发生变化时,您的影响会更小.