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

时间:2012-09-12 21:30:31

标签: c# .net null iequalitycomparer equals-operator

我目前有一个通用方法,我希望在处理它们之前对参数进行一些验证。具体来说,如果类型参数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 constraint约束我的类型参数。

我以为我可以在Marc Gravell's answer上使用"How do I compare a generic type to its default value?",并像{{}}一样使用EqualityComparer<T> class

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的实例?

1 个答案:

答案 0 :(得分:7)

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

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

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

考虑可以检查T的无约束实例的五种主要情况:

  • 不是 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();
}

应该说一下,需要一千万次迭代来指出这一点。

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

那就是说,这是针对铸造方法的测试:

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>;第一个比检查非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)的结果实例转换为object的相同技巧,以便查看类型的类型是否具有{{1}分配给它。可以在这里做,因为它只被称为一次每个类型,这是被要求的。
  • 如果null的默认值不是T,则假定null无法分配给null的实例。在这种情况下,没有理由使用Expression class实际生成lambda,因为条件总是为假。
  • 如果类型可以分配T,那么创建一个lambda表达式就足够了,该表达式与null进行比较,然后即时编译它。

现在,运行此测试:

null

输出结果为:

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 被复制到堆栈上,这比在堆上创建一个新对象(先前的测试正在进行)更快 更快。

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