0

私は現在、パラメーターを操作する前にパラメーターの検証を行いたい汎用メソッドを持っています。具体的には、型パラメーターのインスタンスがT参照型の場合、それが参照型かどうかを確認し、nullnullの場合はスローします。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か?nullT

4

1 に答える 1

7

これを行うにはいくつかの方法があります。object多くの場合、フレームワークでは (Reflector を介してソース コードを見ると)、次のように、型パラメーターのインスタンスが にキャストされ、それを に対してチェックすることがわかりますnull

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

そして、ほとんどの場合、これで問題ありません。ただし、問題があります。

の制約のないインスタンスがTnull に対してチェックされる可能性がある 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を使用してラムダを実際に生成する理由はありません。nullTExpression
  • 割り当てられている場合は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を使用してその場でインターフェイスの実装を生成することもできますが、コンパイルされたラムダを使用するよりも悪くないにしても、結果は無視できるものであることがわかりました。また、アセンブリやモジュールだけでなく、型の新しいビルダーを作成する必要があるため、コードの保守もより困難になります。

于 2012-09-12T21:30:31.360 に答える