これを行うにはいくつかの方法があります。object
多くの場合、フレームワークでは (Reflector を介してソース コードを見ると)、次のように、型パラメーターのインスタンスが にキャストされ、それを に対してチェックすることがわかりますnull
。
if (((object) instance) == null)
throw new ArgumentNullException("instance");
そして、ほとんどの場合、これで問題ありません。ただし、問題があります。
の制約のないインスタンスがT
null に対してチェックされる可能性がある 5 つの主なケースを考えてみましょう。
- ではない値型のインスタンス
Nullable<T>
- である
Nullable<T>
がそうではない値型のインスタンスnull
- ある
Nullable<T>
が、ある値型のインスタンスnull
- ではない参照型のインスタンス
null
- 参照型のインスタンス
null
これらのケースのほとんどでは、パフォーマンスは問題ありませんが、 と比較しているケースでは、あるケースでNullable<T>
は 1 桁以上、別のケースでは少なくとも 5 倍の深刻なパフォーマンス ヒットがあります。
まず、メソッドを定義しましょう。
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>
nullだけでなく非 null の場合にも注意してくださいNullable<T>
。最初のものはそうでない値型に対してチェックするよりも 15 倍以上遅くNullable<T>
、2 番目のものは少なくとも 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();
}
いくつかの注意事項:
default(T)
実際には、 toの結果のインスタンスをキャストするという同じトリックを使用してobject
、型がnull
割り当てられているかどうかを確認しています。これが呼び出されている型ごとに 1回だけ呼び出されるため、ここで実行しても問題ありません。
- のデフォルト値が
T
でない場合、 のインスタンスに割り当てることができないとnull
見なされます。この場合、条件は常に false であるため、 classを使用してラムダを実際に生成する理由はありません。null
T
Expression
- 型が割り当てられている場合は
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
これらの数値は、上記の 2 つのケースでははるかに優れており、他のケースでは (無視できる程度ではありますが) 全体的に優れています。ボックス化はなくNullable<T>
、スタックにコピーされます。これは、ヒープ上に新しいオブジェクトを作成するよりもはるかに高速な操作です (以前のテストで行っていました)。
さらに進んで、Reflection Emitを使用してその場でインターフェイスの実装を生成することもできますが、コンパイルされたラムダを使用するよりも悪くないにしても、結果は無視できるものであることがわかりました。また、アセンブリやモジュールだけでなく、型の新しいビルダーを作成する必要があるため、コードの保守もより困難になります。