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

cas*_*One 0 .net c# null iequalitycomparer equals-operator

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

有点像:

// 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");
Run Code Online (Sandbox Code Playgroud)

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

我以为我可以用马克Gravell的答案"我怎么比一般类型为默认值?" ,并像这样使用EqualityComparer<T>:

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

但它在调用时给出了一个非常模糊的错误Equals:

无法使用实例引用访问成员'object.Equals(object,object)'; 用类型名称来限定它

如何在不限制为值或引用类型的情况下检查T反对的实例?nullT

cas*_*One 7

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

if (((object) instance) == null)
    throw new ArgumentNullException("instance");
Run Code Online (Sandbox Code Playgroud)

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

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

  • 值类型不是的实例 Nullable<T>
  • 一个值类型的实例 Nullable<T>,但不是null
  • 值类型的实例 Nullable<T>,但null
  • 不是引用类型的实例 null
  • 引用类型的实例 null

在大多数情况下,性能都很好,但在您进行比较的情况下Nullable<T>,会出现严重的性能损失,在一种情况下会超过一个数量级,在另一种情况下至少会达到五倍.

首先,让我们定义方法:

static bool IsNullCast<T>(T instance)
{
    return ((object) instance == null);
}
Run Code Online (Sandbox Code Playgroud)

以及测试工具方法:

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();
}
Run Code Online (Sandbox Code Playgroud)

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

肯定存在一个无关紧要的论点,通常,我同意.然而,我发现这个在迭代一个的过程中非常大的数据集在紧凑循环(建筑决策树与数以百计的每个属性的数万项),这是一个明确的因素.

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

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();
Run Code Online (Sandbox Code Playgroud)

这输出:

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
Run Code Online (Sandbox Code Playgroud)

注意在非null Nullable<T>和null的情况下Nullable<T>; 第一个比检查值类型慢15倍以上,Nullable<T>而第二个值至少慢8倍.

原因是拳击.对于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();
}
Run Code Online (Sandbox Code Playgroud)

有几点需要注意:

  • 我们实际上正在使用铸造结果的实例相同伎俩default(T)object才能看到,如果该类型可以null分配给它.这里可以做到,因为每个类型只需要调用一次.
  • 如果默认值T不是null,则假定null无法将其分配给实例T.在这种情况下,没有理由使用Expression该类实际生成lambda ,因为条件始终为false.
  • 如果类型可以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();
Run Code Online (Sandbox Code Playgroud)

输出是:

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
Run Code Online (Sandbox Code Playgroud)

在上述两种情况下,这些数字好得多,而在其他情况下总体上更好(尽管可以忽略不计).没有装箱,并且Nullable<T>被复制到堆栈上,这比在堆上创建新对象(先前的测试正在进行)快得多.

一个走得更远,使用反射发出来实时生成的接口实现,但我发现效果可以忽略不计,如果不是使用编译拉姆达更糟.代码也更难维护,因为您必须为类型创建新的构建器,以及可能的组件和模块.