При заходе в метод мы часто выполняемым проверку на 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 потому что:


  1. Спасает от упаковки
  2. Позволяет улучшить сигнатуру метода, при использовании ограничений

Upd. Спасибо хабраюзеру zelyony за замечание. Методы инлайнились, для более точных замеров добавил атрибут MethodImpl(MethodImplOptions.NoInlining).

Поделиться с друзьями
-->

Комментарии (30)


  1. yurij_volkov
    07.07.2017 22:26
    +3

    Хочется отметить вопрос терминологии. Value types переводятся как «значимые», а не вещественные. Этот вопрос принципиален, так как, например, тип Int32 (Value type, как вы понимаете) является целочисленным, не вещественным.


    1. unsafePtr
      07.07.2017 23:13
      +1

      Спасибо, поправил.


  1. insafonov
    07.07.2017 23:03

    Опыт работы с приложением для автоматизации ВУЗа показывает, что такие исключения (ArgumentNullException) характерны для недописанного софта… Ведь логика (если нет бананов, купи яблок) проще, чем (бананов нет, паника!)(если паника из-за отсутствия бананов, купи яблок).


    1. unsafePtr
      07.07.2017 23:08

      Проектируя библиотеку, вы например не сможете избежать неправильного использования функции, и кто-нибудь да подаст аргумент null. Когда бизнес-логика не позволяет продолжить поток выполнения, лучше выбросить исключение, ну и задокументировать в каком случае оно будет выброшено.


      1. AgentFire
        08.07.2017 15:32
        -6

        Только если эта библиотека плнируется к использованию ну совсем новичками


        1. playermet
          09.07.2017 19:37
          +2

          Новички тут не при чем, потому что null вместо валидного значения обычно попадает в аргументы не напрямую, а вследствие ошибки в коде. И лучше дать знать, что эта ошибка произошла, а не молча проглотить неверное значение с непредсказуемым для приложения и данных результатом.


          1. AgentFire
            10.07.2017 00:33
            -5

            Сама библиотека должна быть написана так, чтобы это было ОЧЕВИДНО, когда можно послать null, а когда нет.


      1. insafonov
        08.07.2017 21:18
        -4

        В защиту AgentFire. Функции предъявляют некоторые требования к входным параметрам. Очевидно, что передача неправильного параметра — проблема не библиотеки, а вызвавшего функцию. И исключительная ситуация возникла еще до входа в эту функцию.


        1. mayorovp
          10.07.2017 08:57

          А после входа в функцию исключительная ситуация становится ошибочной (= отказом).


          1. AgentFire
            10.07.2017 11:36
            -3

            Сама библиотека должна быть написана так, чтобы это было ОЧЕВИДНО, когда можно послать null, а когда нет,


            Но почему-то минусаторы не удосаживаются объяснить причину своего недовольства, видимо, любят тратить время на кучу проверок входных параметров у каждого метода своих классов.


    1. franzose
      08.07.2017 04:44

      А еще некоторые убежденно доказывают, что кидаться исключениями — моветон, т.к. исключительная ситуация по их мнению, это когда сервер отвалился, в остальных случаях можно просто вернуть null и написать if. Это не про Java, но тем не менее.


      1. insafonov
        08.07.2017 11:11

        Всегда кидать исключения и никогда не использовать исключения — это две крайности. Но если откуда-то появился пустой указатель — причина в источнике этого указателя, и ситуация должна обрабатываться после появления этого указателя, а не во вложенной функции или где-то еще, иначе логика размазывается, и поддерживать её сложнее (ведь последующий код кроме ненулевого указателя теперь должен учитывать и пустой).

        Вообще я пытаюсь сказать, что мне больше нравится так

        function getApple() {
            return null;
        }
        
        function bar(apple) {
            ...
        }
        
        function foo() {
            var apple = getApple();
        
            if (!apple) {
                throw new Error('apple is empty!');
            }
        
            bar(apple);
            ...
        }
        

        чем так
        function getApple() {
            return null;
        }
        
        function bar(apple) {
            if (!apple) {
                throw new Error('apple is empty!');
            }
        
            ...
        }
        
        function foo() {
            var apple = getApple();
        
            bar(apple);
            ...
        }
        

        Но, может, я заблуждаюсь, т. к. разрабатывать большие программы мне не доводилось.


        1. franzose
          08.07.2017 15:37

          А еще лучше, как мне кажется, вот так:


          function getApple(): Apple
          {
               $apple = <...>;
          
               if (!$apple) {
                    throw new AppleNotFoundException();
               }
          
              return $apple;
          }


        1. AgentFire
          08.07.2017 23:43
          -6

          Имхо, такой подход гораздо лучше. Разработчик ХОРОШЕЙ библиотеки не станет заморачиваться проверкой каждого угла на наличие null'ов. Его задача — правильно составить документацию, а в идеале — даже без нее, чтобы из названий классов и методов было ОЧЕВИДНО, возможен ли тут null или нет. А задача не пускать нулл должна быть на плечах пользователя библиотеки.


          1. Naglec
            09.07.2017 11:37
            +2

            Давно я таких глупостей не читал. Пользователь библиотеки/любого другого публичного контракта пихнет туда все что угодно и во всех возможных комбинациях. Если ваш код это не учитывает, то это лично ваша проблема, а не клиента.
            Проверка входных параметров для любых публичных методов необходима. Как и для методов, которые могут быть переопределены. Я искренне не понимаю что здесь можно обсуждать.


            1. insafonov
              09.07.2017 13:53

              Пользовательский ввод необходимо фильтровать. Пользователь может на вход подать что угодно. А вот если программа подает что попало на вход библиотечной функции — это вообще правильная программа?
              И в этом случае параметры проверяются дважды — до вызова метода и внутри него. Ясно, что два-три лишних if'a погоды не сделают, но зачем?


              1. playermet
                09.07.2017 19:47

                > это вообще правильная программа?
                Может и неправильная, только она об этом не узнает. Пока данные не навернутся от побочных эффектов передачи чего попало.


        1. mayorovp
          10.07.2017 09:01
          +1

          Это две разные проверки, и обе проверки — важны.


          Ваша проверка в функции foo — это часть алгоритма. Ситуация, когда яблоко было не найдено, является исключительной и требует особой обработки.


          Проверку же в функции bar требует принцип защитного программирования. Ее задача — остановить распространение возможной ошибки по программе для упрощения диагностики. Одновременно, эту проверку можно считать частью контракта функции, потому что она легко видна любому кто решит посмотреть на функцию по-внимательнее.


          Ошибка, брошенная в функции bar, по-хорошему не должна нигде ловиться (принцип "Fail Fast"). От хорошего программиста ожидается, что увидев трассировку стека с этой ошибкой, он хлопнет себя по лбу и допишет случайно пропущенную проверку в функции foo.


      1. Interreto
        08.07.2017 15:33
        +2

        Исключение это хорошо, вешаем глобальный логер, и получаем полную картину мира продакшена, и как бонус фингерприн попыток взлома ...


    1. petuhov_k
      08.07.2017 04:56
      +2

      Т.е. по-вашему, по мере дописывания софта, эти проверки убираются? И, вообще, что такое дописанный софт? Примерчик бы.

      Из моего опыта, проверка аргументов появляется либо сразу в методе публичного API, либо после того, как кто-то словил NullReferenceException.


      1. insafonov
        08.07.2017 11:53
        +1

        Нужно различать проверку входных данных и проверку аргументов функции/метода.
        Проверка входных данных должна быть сразу же (если, конечно, это не прототип), и о неправильном вводе пользователь должен получать вменяемое сообщение. А внутренние методы, по хорошему, не должны вызываться с пустыми (= NULL) аргументами (хотя здесь много исключений).

        после того, как кто-то словил NullReferenceException

        Вот это и есть — недописанный софт. Приходится писать руководству выше, они пишут разработчикам, разработчики исправляют, высылают(!) новую версию программы, ну а пользователь теряет день-два-неделю.


  1. kogoia
    08.07.2017 15:34
    -2

    Использование проверок на Null, нужно стараться вообще исключить из кода. Не передавать ни в качестве аргумента в методы ни ожидать в параметрах конструктора. Если такое случается это повод задуматься об дизаине кода. Скорее всего проблема в нём. Вот как я обхажусь без всего этого:

    В принципе это Maybe, но чуть изменённая под себя.

    Я определяю два интерфейса и наследую их от IEnumerable:

    public interface IOptional<T> : IEnumerable<T> { }
    public interface IMandatory<T> : IEnumerable<T> { }
    

    Это может показатся странным, но даст нам возможность делать прекрасные вещи :)

    Далее я деляю два класса и наследую их от этих интерфеисов:

    public class Some<T> : IOptional<T>
    {
        private readonly IEnumerable<T> _element;
        public Some(T element)
            : this(new T[1] { element }){}
        public Some()
            : this(new T[0]) {}
        private Some(T[] element)
        {
            _element = element;
        }
        public IEnumerator<T> GetEnumerator()
        {
            return _element.GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    
    public class Just<T> : IMandatory<T>
    {
        private readonly T _element;
    
        public Just(T element)
        {
            _element = element;
        }
        public IEnumerator<T> GetEnumerator()
        {
            yield return _element;
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    


    Несколько вспомогательных статических методов как Exstensions к этим интерфеисом:

    public static class LinqExtensions
    {
        public static IMandatory<TOutput> Match<TInput, TOutput>(
            this IEnumerable<TInput> maybe,
            Func<TInput, TOutput> some, Func<TOutput> nothing)
        {
            if (maybe.Any())
            {
                return new Just<TOutput>(
                            some(
                                maybe.First()
                            )
                        );
            }
            else
            {
                return new Just<TOutput>(
                            nothing()
                        );
            }
        }
        public static T Fold<T>(this IMandatory<T> maybe)
        {
            return maybe.First();
        }
    }
    

    Их можно било определить в самих интерфеисах и сделать имплементацию в соответственныйх классах, но здесь показываю как пример, а так можно ещё пару интересных методов добавить.

    А теперь можно творить такие вещи:

    var five = new Just<int>(5);
    var @null = new Some<int>();
    
    Console.WriteLine(
                five
                    .SelectMany(f => @null.Select(n => f * n))
                    .Match(
                        some: r => $"Result: {r}",
                        nothing: () => "Ups"
                    )
                    .Fold()
            );
    


    Вот так, немного фукциональной парадигмы здорого выручает, даже не нужно объястять Монады, а она здесь присутствует. (SelectMany)
    Всё легко и просто :)


    1. mayorovp
      10.07.2017 09:07
      -1

      Чего только люди не колхозят, лишь бы контракты не использовать...


      1. kogoia
        10.07.2017 11:49

        Вам ещё расти:)


    1. AgentFire
      10.07.2017 12:22
      +1

      Использование проверок на Null, нужно стараться вообще исключить из кода. Не передавать ни в качестве аргумента в методы ни ожидать в параметрах конструктора. Если такое случается это повод задуматься об дизаине кода. Скорее всего проблема в нём.

      Эта фраза полностью самодостаточно, все остальное после нее — лишнее.


      1. kogoia
        10.07.2017 13:44

        Просто хотел поделиться каким методом я добиваюсь такого


        1. AgentFire
          10.07.2017 15:23

          Минус не мой, если что.


          1. kogoia
            10.07.2017 16:16

            Понятно что люди во нениях не часто сходятся, но просто выражать не довольство без мнении, как то странно. Может я в праде чушь несу и есть решения иные, по лучше.

            Пусть высказываются сначало хотябы :)


  1. unsafePtr
    08.07.2017 15:59
    -2

    По моему тема ушла в перекос о том как спастись/избавится/не использовать исключения. Я хотел показать лишь, что вместо использования object аргумента лучше всегда использовать generic, так как это спасает от упаковки.
    Что касается самих исключений, то я придерживаюсь мнения, что исключения надо использовать только по необходимости, и где это возможно избегать. Это и в стандартной библиотеке встречается, например класс ReadOnlyCollection. Если вы его откроете и посмотрите на строку его объявления то увидите, что он наследует IList, у IList есть метод Add, однако ReadOnlyCollection, использует трюк который называется Explicit Interface Implementation. Получается вы защищены от вызова метода на инстанции класса, однако не защищены от вызова этого метода на интерфейсе.

    IList<int> collection = new ReadOnlyCollection<int>(new int[] { 1, 2, 3 });
    collection.Add(10); // throws exception
    


    1. kogoia
      10.07.2017 12:11
      +1

      Да правда в перекос, я согласен что вместо использования object аргумента лучше использовать generic, тот коментарий так для разнообразия.