.NET异常处理有多重?

时间:2012-01-16 22:40:41

标签: c# .net 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;
            }
        }
    }
}

我真的更喜欢这样做

var result = string.Empty;
try
{
    result = rootObject.FirstProperty.SecondProperty.InterestingString;
}
catch { }

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

9 个答案:

答案 0 :(得分:17)

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

答案 1 :(得分:8)

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

尽管如此,它们远没有一些人想象的那么昂贵。

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

我看到有人将系统性能表现糟糕归咎于它每小时抛出几十个例外情况。

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

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

var result
    = rootObject.With( r => FirstProperty)
          .With( r => r.SecondProperty)
          .Return( r => r.InterestingString, string.Empty);

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);
    }

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);
    }

答案 2 :(得分:5)

异常处理

我不担心&#34;多么沉重&#34;例外是在这样的代码中。我稍后会解释原因。

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

何时写保护条件

这些属性通常是null吗?

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

这些属性null是不常见的,即您是否可以合理地预期它们永远不会是null

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

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

何时编写异常处理程序

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

您是否有一种简单的方法可以在异常情况发生之前对其进行测试?

如果是这样,那么在允许您使用的基础代码抛出异常之前,应该对此进行测试。由于您只需检查null,我就说这很容易。

您是否有合理的逻辑来处理此级别的案例?

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

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

如果您没有合理的方法来处理异常,只需让程序崩溃即可。这总是比吞咽异常并继续下去更好。这隐藏了错误。

这些规则的例外情况

有时你不能轻易地测试一个条件而不会抛出异常。

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

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

性能

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

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

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

使用例外情况并不像许多人可能认为的那样高。在.Net中,它们被设计为在没有抛出异常时表现得非常好。这就是例外情况的例外情况。


您的代码示例

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

编写异常处理程序

您为异常处理程序编写的代码根本不可接受。这里有关于编写更好的异常处理程序代码的一些指导:

为:

try
{
}
catch // Note: Doesn't catch `Exception e`
{
    // ... eats the exeption
}

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

可能接受:

try
{
}
catch(Exception e)
{
    logger.Log(e.ToString());
    // ... eats the exeption
}

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

好:

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;
}

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

好:

try
{
}
catch(NullReferenceException e)
{
    // ... Do whatever you want here ...
}

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

避免重复代码

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

rootObject ...
rootObject.FirstProperty ...
rootObject.FirstProperty.SecondProperty ...
rootObject.FirstProperty.SecondProperty.InterestingString ...

...只打电话给吸气者一次:

var firstProperty = rootObject.FirstProperty;
var secondProperty = firstProperty.SecondProperty;
var interestingString = secondProperty.InterestingString;

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

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;
            }
        }
    }
}

执行此操作的一个原因是getter可能具有复杂的逻辑,并且多次调用它可能会对性能产生影响。

另一个原因是你avoid repeating yourself。当代码没有重复时,代码总是更具可读性。

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

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

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

rootObject.FirstProperty.SecondProperty.InterestingString

即使您将其拆分以避免重复自己(就像我上面建议的那样),您仍然可能无法正确地考虑您的代码。您的代码仍然紧密耦合到该数据结构的层次结构。每次更改该层次结构时,都需要更改遍历该层次结构的任何代码。如果这是您的所有代码,那么您的状况就会很糟糕。

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

处理根对象的代码应该只调用直接处理根目录下方对象的代码。处理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; }
}

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

答案 3 :(得分:2)

例外是......好的,特殊情况。它们适用于无法以其他方式计划的事情。异常有一定的开销,使用它们来捕获像这样的一般问题被认为是不好的做法,特别是如果你只是忽略了异常的结果(使用空的catch块)。

他们可能会使您的代码 LOOK 更清洁,但它们不会使您的代码 EXECUTE 更清洁。

答案 4 :(得分:1)

取决于。

如果rootObject可能为null,则以第一种方式对其进行编码会更好,因为它不是特殊情况。但是,它会使方法的执行速度稍慢。虽然有一些方法可以重新编码嵌套的if语句,以避免深度嵌套,并允许从方法中快速退出。

另一方面,如果正常执行速度是一个问题而且rootObject不太可能为null,那么编码它的第二种方式更好,因为它**是< / strong>和特殊情况。

您需要分析系统,以了解哪种方式适用于您的应用

答案 5 :(得分:1)

提高可读性的一种方法是扭转您的状况:

var result = string.Empty;
if (rootObject == null) return result;
if (rootObject.FirstProperty == null) return result;
if (rootObject.FirstProperty.SecondProperty == null) return result;
if (!string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString))
{
    result = rootObject.FirstProperty.SecondProperty.InterestingString;
}

下一步是使用编译器将为您执行的条件快捷方式:

var result = string.Empty;
if (rootObject == null || rootObject.FirstProperty == null ||
    rootObject.FirstProperty.SecondProperty == null) return result;
if (!string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString))
{
    result = rootObject.FirstProperty.SecondProperty.InterestingString;
}

答案 6 :(得分:1)

一种选择是使用code contracts。它们是一种非常干净的方式来执行您正在进行的检查类型,如果您正确配置调试版本,编译器实际上可以找到违反合同的代码。一个空的catch块确实不是一个好主意(并不是因为它会使用资源......由于很多原因它只是编码不好)。

答案 7 :(得分:0)

如果你可以防止你的代码首先抛出异常,那么你应该这样做。

Try / Catch块在正确使用时很有用(例如当你访问代码无法控制的内容时,比如打开网络连接)。

如果您认为代码的某个部分中的错误是非致命的,您也可以使用它们继续运行代码。只需确保正确处理catch块中的错误。

然而,您应该考虑的一件事是编译器如何执行谓词。

它将采用谓词的最左边的子句rootObject != null,如果这是假的,并且它与你的其他子句是并行的,那么该谓词将被保证评估为false。然后编译器将忽略谓词的其余部分,因此您可以执行以下操作:

if (rootObject != null && rootObject.FirstProperty != null && rootObject.FirstProperty.SecondProperty != null && !string.IsNullOrEmpty(rootObject.FirstProperty.SecondProperty.InterestingString))
{
    result = rootObject.FirstProperty.SecondProperty.InterestingString;
}

答案 8 :(得分:0)

在大多数情况下,您不应该测试引用是否为null,因为null甚至不应该在可能值的范围内。如果函数无法返回可能值范围内的某些内容,则此函数除了抛出异常外别无选择。这应该在没有显式空检查的情它应该在为例如构造返回值时发生。

但这意味着你需要非可空类型,语言理想情况下应该支持,但C#不支持。

你可以使用Jon Skeet的NonNullable<>之类的东西。