簡體   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