При заходе в метод мы часто выполняемым проверку на null. Кто-то выносит проверку в отдельный метод, что бы код выглядел чище, и получается что то-такое:
public void ThrowIfNull(object obj)
{
if(obj == null)
{
throw new ArgumentNullException();
}
}
И что интересно при такой проверке, я массово вижу использование именно object атрибута, можно ведь воспользоватся generic-ом. Давайте попробуем заменить наш метод на generic и сравнить производительность.
Перед тестированием нужно учесть ещё один недостаток object аргумента. Значимые типы(value types) никогда не могут быть равны null(Nullable тип не в счёт). Вызов метода, вроде ThrowIfNull(5), бессмыслен, однако, поскольку тип аргумента у нас object, компилятор позволит вызвать метод. Как по мне, это снижает качество кода, что в некоторых ситуациях гораздо важнее производительности. Для того что бы избавится от такого поведения, и улучшить сигнатуру метода, generic метод придётся разделить на два, с указанием ограничений(constraints). Беда в том что нельзя указать Nullable ограничение, однако, можно указать nullable аргумент, с ограничением struct.
Приступаем к тестированию производительности, и воспользуемся библиотекой BenchmarkDotNet. Навешиваем атрибуты, запускаем, и смотрим на результаты.
public class ObjectArgVsGenericArg
{
public string str = "some string";
public Nullable<int> num = 5;
[MethodImpl(MethodImplOptions.NoInlining)]
public void ThrowIfNullGenericArg<T>(T arg)
where T : class
{
if (arg == null)
{
throw new ArgumentNullException();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void ThrowIfNullGenericArg<T>(Nullable<T> arg)
where T : struct
{
if(arg == null)
{
throw new ArgumentNullException();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void ThrowIfNullObjectArg(object arg)
{
if(arg == null)
{
throw new ArgumentNullException();
}
}
[Benchmark]
public void CallMethodWithObjArgString()
{
ThrowIfNullObjectArg(str);
}
[Benchmark]
public void CallMethodWithObjArgNullableInt()
{
ThrowIfNullObjectArg(num);
}
[Benchmark]
public void CallMethodWithGenericArgString()
{
ThrowIfNullGenericArg(str);
}
[Benchmark]
public void CallMethodWithGenericArgNullableInt()
{
ThrowIfNullGenericArg(num);
}
}
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<ObjectArgVsGenericArg>();
}
}
Method | Mean | Error | StdDev |
---|---|---|---|
CallMethodWithObjArgString | 1.784 ns | 0.0166 ns | 0.0138 ns |
CallMethodWithObjArgNullableInt | 124.335 ns | 0.2480 ns | 0.2320 ns |
CallMethodWithGenericArgString | 1.746 ns | 0.0290 ns | 0.0271 ns |
CallMethodWithGenericArgNullableInt | 2.158 ns | 0.0089 ns | 0.0083 ns |
Наш generic на nullable типе отработал в 2000 раз быстрее! А всё из-за пресловутой упаковки(boxing). Когда мы вызываем CallMethodWithObjArgNullableInt, то наш nullable-int "упаковывается" и размещается в куче. Упаковка очень дорогая операция, от того метод и проседает по производительности. Таким образом использую generic мы можем избежать упаковки.
Итак, generic аргумент лучше object потому что:
- Спасает от упаковки
- Позволяет улучшить сигнатуру метода, при использовании ограничений
Upd. Спасибо хабраюзеру zelyony за замечание. Методы инлайнились, для более точных замеров добавил атрибут MethodImpl(MethodImplOptions.NoInlining).