正在检查泛型方法中非类约束类型参数的实例是否为null



我目前有一个通用方法,在处理参数之前,我想对它们进行一些验证。具体来说,如果类型参数T的实例是引用类型,我想检查它是否是null,如果它为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的实例?

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

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

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

考虑以下五种主要情况,其中T的无约束实例可以针对null进行检查:

  • 不是Nullable<T>的值类型的实例
  • Nullable<T>但不是null的值类型的实例
  • Nullable<T>但为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();
}

需要说明的是,指出这一点需要1000万次迭代。

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

也就是说,以下是针对铸造方法的测试:

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

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

原因是拳击。对于传入的每个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)的结果的实例强制转换为object,以查看类型是否可以为其分配null。在这里可以这样做,因为它只被调用一次
  • 如果T的默认值不是null,则假定null不能分配给T的实例。在这种情况下,没有理由实际使用Expression类生成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

在上述两种情况下,这些数字要好得多,在其他情况下总体更好(尽管可以忽略不计)。没有装箱,Nullable<T>被复制到堆栈上,这是一个比在堆上创建新对象(之前的测试是这样做的)快得多的操作。

一个可以更进一步,使用Reflection Emit动态生成接口实现,但我发现结果可以忽略不计,甚至比使用编译后的lambda更糟糕。代码也更难维护,因为您必须为类型创建新的生成器,以及可能的程序集和模块。

最新更新