为什么SortedList实现使用ThrowHelper而不是直接抛出?

时间:2009-02-18 19:28:30

标签: c#

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)”,这是无法访问的。我必须得出结论,这种模式的好处足以证明这一点。

这些好处是什么?

3 个答案:

答案 0 :(得分:21)

根据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. 

答案 1 :(得分:4)

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

答案 2 :(得分:0)

另一个有趣的方面是性能。有趣的是,一个包含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毫无意义。但是,这对于要求短性能的成员很有用。

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

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

// 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语句中使用,可以在break块中保留case,从C#7.0开始,可以与throw表达式相同的方式使用:

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

另一个问题是ReSharper和FxCop无法识别投掷助手成员,并可能开始发出错误的肯定警告。对于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和他们的朋友。