繁体   English   中英

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

[英]Checking instance of non-class constrained type parameter for null in generic method

我目前有一个通用方法,在此之前我想对参数进行一些验证。 具体来说,如果类型参数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不受值或引用类型的约束时,如何检查T的实例是否为null

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

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

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

考虑以下五种主要情况,其中可以针对null检查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

注意,在非null Nullable<T>以及null 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 ,以查看类型是否可以分配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();

输出为:

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还差。 该代码也更难以维护,因为您必须为该类型以及可能的程序集和模块创建新的生成器。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM