При заходе в метод мы часто выполняемым проверку на 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;
public void ThrowIfNullGenericArg<T>(T arg)
where T : class
{
if (arg == null)
{
throw new ArgumentNullException();
}
}
public void ThrowIfNullGenericArg<T>(Nullable<T> arg) // Nullable argument with struct constraint
where T : struct
{
if(arg == null)
{
throw new ArgumentNullException();
}
}
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 | 0.0001 ns | 0.0003 ns | 0.0003 ns |
CallMethodWithObjArgNullableInt | 121.1810 ns | 0.1218 ns | 0.1017 ns |
CallMethodWithGenericArgString | 0.0000 ns | 0.0000 ns | 0.0000 ns |
CallMethodWithGenericArgNullableInt | 0.0667 ns | 0.0112 ns | 0.0105 ns |
Наш generic на nullable типе отработал в 2000 раз быстрее! А всё из-за пресловутой упаковки(boxing). Когда мы вызываем CallMethodWithObjArgNullableInt, то наш nullable-int "упаковывается" и размещается в куче. Упаковка очень дорогая операция, от того метод и проседает по производительности. Таким образом использую generic мы можем избежать упаковки.
Итак, generic аргумент лучше object потому что:
- Спасает от упаковки
- Позволяет улучшить сигнатуру метода, при использовании ограничений
Автор: крепыш