【问题标题】:Why does SortedList implementation use ThrowHelper instead of throwing directly?为什么 SortedList 实现使用 ThrowHelper 而不是直接抛出?
【发布时间】:2009-02-18 19:28:29
【问题描述】:

Reflector 告诉我 SortedList 使用 ThrowHelper 类来抛出异常,而不是直接抛出异常,例如:

public TValue this[TKey key]
{
    get
    {
        int index = this.IndexOfKey(key);
        if (index >= 0)
            return this.values[index];
        ThrowHelper.ThrowKeyNotFoundException();
        return default(TValue);
    }

ThrowKeyNotFoundException 的作用只不过是:

throw new KeyNotFoundException();

请注意,这需要一个无法访问的 duff 语句“return default(TValue)”。我必须得出结论,这是一种具有足够大的好处的模式来证明这一点。

这些好处是什么?

【问题讨论】:

  • 您是否查看了实际的 Microsoft 代码,而不是它编译成的内容?
  • 不,我没有。有显着不同吗?如果是,请在答案中解释这一点! :)

标签: c#


【解决方案1】:

根据ThrowHelper.cs源代码,主要目的是减少JITted代码大小。以下是链接中的直接复制粘贴:

// This file defines an internal class used to throw exceptions in BCL code.
// The main purpose is to reduce code size. 
// 
// The old way to throw an exception generates quite a lot IL code and assembly code.
// Following is an example:
//     C# source
//          throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
//     IL code:
//          IL_0003:  ldstr      "key"
//          IL_0008:  ldstr      "ArgumentNull_Key"
//          IL_000d:  call       string System.Environment::GetResourceString(string)
//          IL_0012:  newobj     instance void System.ArgumentNullException::.ctor(string,string)
//          IL_0017:  throw
//    which is 21bytes in IL.
// 
// So we want to get rid of the ldstr and call to Environment.GetResource in IL.
// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the
// argument name and resource name in a small integer. The source code will be changed to 
//    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key);
//
// The IL code will be 7 bytes.
//    IL_0008:  ldc.i4.4
//    IL_0009:  ldc.i4.4
//    IL_000a:  call       void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument)
//    IL_000f:  ldarg.0
//
// This will also reduce the Jitted code size a lot. 

【讨论】:

    【解决方案2】:

    另一个有趣的方面是性能。有趣的是,包含throw 语句的方法可能会更慢,即使不抛出异常只是因为 JIT 不喜欢内联此类方法(可能是为了提高调用堆栈的可读性)。考虑以下示例:

    private class TestClass
    {
        internal int RegularThrow(int value)
        {
            if (value < 0)
                throw new ArgumentOutOfRangeException(nameof(value));
            return value + 1;
        }
    
        internal int ThrowByHelper(int value)
        {
            if (value < 0)
                Throw.ArgumentOutOfRangeException(Argument.value); // Argument is an enum
            return value + 1;
        }
    }
    

    我的电脑上的性能结果:

    (请参阅源链接以及下面的一些注释)

    1. ThrowByHelper: average time: 5,24 ms
      #1           5,26 ms
      #2           5,16 ms   <---- Best
      #3           5,31 ms   <---- Worst
      Worst-Best difference: 0,16 ms (3,02%)
    2. RegularThrow: average time: 23,51 ms (+18,27 ms / 448,40%)
      #1          23,46 ms
      #2          23,42 ms   <---- Best
      #3          23,65 ms   <---- Worst
      Worst-Best difference: 0,22 ms (0,95%)
    

    意思是,带有显式throw 语句的方法慢了4.5 倍!但是……

    有趣的观察:

    • 从 .NET 4.0 开始,您可以应用 [MethodImpl(MethodImplOptions.AggressiveInlining)] 属性,但它不能保证任何事情。例如,在 .NET Fiddle 中,内联似乎通常被禁用,因此两种方式实际上具有相同的性能。 See the source code and the completely different results here
    • 在 .NET Core 3(Windows 10 上的 x64 版本)中,似乎只有从 ArgumentException 派生的异常类型才能阻止内联(不使用任何属性)。至少直接抛出NotSupportedExceptionInvalidOperationException 不会对性能产生负面影响。
    • 对于更复杂的方法,throw helper 无论如何都没有任何意义。但它对短的性能关键成员很有用。

    冗余/无法访问的代码问题和代码分析器:

    可以通过在ThrowHelper 中定义一些通用重载来避免多余的return 语句:

    // for regular usage:
    internal static void ArgumentException(Argument arg, string message) => throw new...
    
    // for expression usage:
    internal static T ArgumentException<T>(Argument arg, string message) => throw new...
    

    后者可以用在return 语句中,可以在case 块中备用break,从C# 7.0 开始,它可以像throw 表达式一样使用:

    return value >= 0 ? value + 1 : Throw.ArgumentOutOfRangeException<int>(Argument.value);
    

    另一个问题是 ReSharper 和 FxCop 无法识别 throw helper 成员并可能开始发出误报警告。对于 ReSharper,我们可以使用 ContractAnnotation 属性:

    // prevents PossibleNullReferenceException, AssignNullToNotNullAttribute and similar false alarms
    [ContractAnnotation("=> halt")]
    internal static void ArgumentException(Argument arg, string message) => throw new...
    

    不幸的是,对于 FxCop,我没有找到类似的解决方案([DoesNotReturn] 属性显然不起作用)所以你应该使用#pragma warning disableSuppressMessage 属性来抑制 CA1031、CA1062 和他们的朋友。

    【讨论】:

      【解决方案3】:

      看看 ThrowHelper 做了什么。它为错误消息获取资源和内容。在这个特定的例子中,没有错误文本,所以它看起来没用,但他们的模式可能需要它,所以编写它的开发人员遵循了他/她应该的模式。

      【讨论】:

        猜你喜欢
        • 2010-12-31
        • 1970-01-01
        • 2014-07-08
        • 1970-01-01
        • 2017-08-15
        • 1970-01-01
        • 2021-06-25
        • 2017-01-26
        • 2020-04-24
        相关资源
        最近更新 更多