Проверка экземпляра параметра ограниченного типа не для класса на null в универсальном методе

В настоящее время у меня есть общий метод, в котором я хочу выполнить некоторую проверку параметров, прежде чем работать с ними. В частности, если экземпляр параметра типа T это ссылочный тип, я хочу проверить, если это 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 ограничение

Я подумал, что мог бы использовать ответ Марка Гравелла на "Как сравнить универсальный тип со значением по умолчанию?" и использовать 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 против null когда T не ограничен быть значением или ссылочным типом?

1 ответ

Решение

Есть несколько способов сделать это. Часто в фреймворке (если вы посмотрите на исходный код через Reflector), вы увидите приведение экземпляра параметра типа к object а затем проверяя это против null, вот так:

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

И по большей части это нормально. Тем не менее, есть проблема.

Рассмотрим пять основных случаев, когда неограниченный случай 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

Обратите внимание, в случае ненулевого 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) в object чтобы увидеть, может ли тип иметь null назначен на это. Это нормально, потому что вызывается только один раз для каждого типа.
  • Если значение по умолчанию для T не является null тогда предполагается null не может быть назначен на экземпляр T, В этом случае нет никаких оснований для создания лямбда-выражения с использованием Expression класс, так как условие всегда ложно.
  • Если тип может иметь 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

Эти цифры намного лучше в двух вышеупомянутых случаях и в целом лучше (хотя и незначительны) в других. Там нет бокса, а Nullable<T> копируется в стек, что является гораздо более быстрой операцией, чем создание нового объекта в куче (что делал предыдущий тест).

Можно пойти дальше и использовать Reflection Emit для генерации реализации интерфейса на лету, но я обнаружил, что результаты незначительны, если не хуже, чем при использовании скомпилированной лямбды. Код также сложнее поддерживать, так как вам нужно создать новые компоновщики для типа, а также, возможно, сборку и модуль.

Другие вопросы по тегам