0

我目前有一个通用方法,我想在处理参数之前对参数进行一些验证。具体来说,如果类型参数的实例T是引用类型,我想检查它是否为空,如果为空则null抛出一个ArgumentNullException

类似于以下内容:

// This can be a method on a generic class, it does not matter.
public void DoSomething<T>(T instance)
{
    if (instance == null) throw new ArgumentNullException("instance");

请注意,我不希望使用class约束来约束我的类型参数。

我想我可以使用Marc Gravell关于“如何将泛型类型与其默认值进行比较?”的回答。,并像这样使用EqualityComparer<T>

static void DoSomething<T>(T instance)
{
    if (EqualityComparer<T>.Default.Equals(instance, null))
        throw new ArgumentNullException("instance");

但它在调用时给出了一个非常模棱两可的错误Equals

成员 'object.Equals(object, object)' 不能通过实例引用访问;改为使用类型名称来限定它

如何检查T不受值或引用类型约束null的实例?T

4

1 回答 1

7

有几种方法可以做到这一点。通常,在框架中(如果您通过 Reflector 查看源代码),您会看到类型参数实例的转换object,然后检查它是否与null,如下所示:

if (((object) instance) == null)
    throw new ArgumentNullException("instance");

在大多数情况下,这很好。但是,有一个问题。

T考虑可以针对 null 检查不受约束的实例的五个主要情况:

  • 值类型的实例不是 Nullable<T>
  • Nullable<T>但不是的值类型的实例null
  • is Nullable<T> but is的值类型的实例null
  • 引用类型的实例不是null
  • 引用类型的实例是null

在大多数情况下,性能都很好,但在与 进行比较的情况下,Nullable<T>性能受到严重影响,在一种情况下超过一个数量级,在另一种情况下至少是五倍。

首先,让我们定义方法:

static bool IsNullCast<T>(T instance)
{
    return ((object) instance == null);
}

以及测试工具方法:

private const int Iterations = 100000000;

static void Test(Action a)
{
    // Start the stopwatch.
    Stopwatch s = Stopwatch.StartNew();

    // Loop
    for (int i = 0; i < Iterations; ++i)
    {
        // Perform the action.
        a();
    }

    // Write the time.
    Console.WriteLine("Time: {0} ms", s.ElapsedMilliseconds);

    // Collect garbage to not interfere with other tests.
    GC.Collect();
}

关于需要一千万次迭代才能指出这一点的事实,应该说点什么。

肯定有人认为这无关紧要,通常情况下,我会同意。然而,我在一个紧密循环中迭代大量数据的过程中发现了这一点(为数以万计的项目构建决策树,每个项目都有数百个属性),这是一个明确的因素。

也就是说,这里是针对强制转换方法的测试:

Console.WriteLine("Value type");
Test(() => IsNullCast(1));
Console.WriteLine();

Console.WriteLine("Non-null nullable value type");
Test(() => IsNullCast((int?)1));
Console.WriteLine();

Console.WriteLine("Null nullable value type");
Test(() => IsNullCast((int?)null));
Console.WriteLine();

// The object.
var o = new object();

Console.WriteLine("Not null reference type.");
Test(() => IsNullCast(o));
Console.WriteLine();

// Set to null.
o = null;

Console.WriteLine("Not null reference type.");
Test(() => IsNullCast<object>(null));
Console.WriteLine();

这输出:

Value type
Time: 1171 ms

Non-null nullable value type
Time: 18779 ms

Null nullable value type
Time: 9757 ms

Not null reference type.
Time: 812 ms

Null reference type.
Time: 849 ms

注意在非 null 和 null 的情况Nullable<T>Nullable<T>;第一个比检查不是的值类型慢十五倍以上,Nullable<T>而第二个至少慢八倍。

原因是拳击。对于Nullable<T>传入的每个实例,在转换为object进行比较时,必须将值类型装箱,这意味着在堆上进行分配等。

然而,这可以通过动态编译代码来改进。可以定义一个帮助类,它将提供对 的调用的实现IsNull,在创建类型时动态分配,如下所示:

static class IsNullHelper<T>
{
    private static Predicate<T> CreatePredicate()
    {
        // If the default is not null, then
        // set to false.
        if (((object) default(T)) != null) return t => false;

        // Create the expression that checks and return.
        ParameterExpression p = Expression.Parameter(typeof (T), "t");

        // Compare to null.
        BinaryExpression equals = Expression.Equal(p, 
            Expression.Constant(null, typeof(T)));

        // Create the lambda and return.
        return Expression.Lambda<Predicate<T>>(equals, p).Compile();
    }

    internal static readonly Predicate<T> IsNull = CreatePredicate();
}

需要注意的几点:

  • 我们实际上使用了相同的技巧,将结果实例转换为default(T)toobject以查看类型是否可以分配null给它。可以在这里做,因为它只被调用一次
  • 如果 的默认值T不是null,则假定null不能将其分配给 的实例T。在这种情况下,没有理由使用Expressionclass实际生成 lambda ,因为条件始终为 false。
  • 如果类型可以分配给它,那么很null容易创建一个与 null 进行比较的 lambda 表达式,然后即时编译它。

现在,运行这个测试:

Console.WriteLine("Value type");
Test(() => IsNullHelper<int>.IsNull(1));
Console.WriteLine();

Console.WriteLine("Non-null nullable value type");
Test(() => IsNullHelper<int?>.IsNull(1));
Console.WriteLine();

Console.WriteLine("Null nullable value type");
Test(() => IsNullHelper<int?>.IsNull(null));
Console.WriteLine();

// The object.
var o = new object();

Console.WriteLine("Not null reference type.");
Test(() => IsNullHelper<object>.IsNull(o));
Console.WriteLine();

Console.WriteLine("Null reference type.");
Test(() => IsNullHelper<object>.IsNull(null));
Console.WriteLine();

输出是:

Value type
Time: 959 ms

Non-null nullable value type
Time: 1365 ms

Null nullable value type
Time: 788 ms

Not null reference type.
Time: 604 ms

Null reference type.
Time: 646 ms

These numbers are much better in the two cases above, and overall better (although negligible) in the others. There's no boxing, and the Nullable<T> is copied onto the stack, which is a much faster operation than creating a new object on the heap (which the prior test was doing).

One could go further and use Reflection Emit to generate an interface implementation on the fly, but I've found the results to be negligible, if not worse than using a compiled lambda. The code is also more difficult to maintain, as you have to create new builders for the type, as well as possibly an assembly and module.

于 2012-09-12T21:30:31.360 回答