有几种方法可以做到这一点。通常,在框架中(如果您通过 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
。在这种情况下,没有理由使用Expression
class实际生成 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.