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