Если функции должны выполнить нулевые проверки перед выполнением намеченного поведения, это плохой дизайн?


66

Так что я не знаю, хороший это или плохой дизайн кода, поэтому я подумал, что лучше спросить.

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

Для очень простого примера:

// fields and properties
private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}
public void SetName(string name)
{
    if (_someEntity == null) return; //check to avoid null ref
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Так что, как вы можете видеть, я проверяю нулевое значение каждый раз. Но должен ли метод не иметь эту проверку?

Например, внешний код должен очищать данные перед использованием, чтобы методы не проверялись, как показано ниже:

 if(entity != null) // this makes the null checks redundant in the methods
 {
     Manager.AssignEntity(entity);
     Manager.SetName("Test");
 }

Таким образом, если методы «проверяют данные», а затем выполняют их обработку данных, или это должно быть гарантировано перед вызовом метода, и если вам не удалось проверить перед вызовом метода, он должен выдать ошибку (или перехватить ошибка)?


7
Что произойдет, если кто-то не выполнит нулевую проверку и SetName все равно выдаст исключение?
Роберт Харви

5
Что произойдет, если я на самом деле хочу, чтобы someEntity был нулевым? Вы сами решаете, что хотите, либо someEntity может иметь значение Null, либо нет, и затем вы соответственно пишете свой код. Если вы хотите, чтобы он никогда не был нулевым, вы можете заменить проверки на утверждения.
gnasher729

5
Вы можете посмотреть выступление Гэри Бернхардта «Границы» о четком разделении между очисткой данных (т. Е. Проверкой на нулевые значения и т. Д.) Во внешней оболочке и наличием внутренней базовой системы, которой не нужно заботиться об этом, - и просто выполните ее работай.
марта

1
С C # 8 вам, возможно, никогда не придется беспокоиться об исключениях нулевых ссылок при включенных
JᴀʏMᴇᴇ

3
Это одна из причин, по которой языковая команда C # хочет ввести обнуляемые ссылочные типы (и не обнуляемые) -> github.com/dotnet/csharplang/issues/36
Mafii

Ответы:


167

Проблема с вашим базовым примером не в нулевой проверке, а в тихом сбое .

Ошибки нулевого указателя / ссылки чаще всего являются ошибками программиста. Ошибки программиста часто лучше всего устраняются путем немедленного и громкого сбоя. У вас есть три основных способа решения проблемы в этой ситуации:

  1. Не беспокойтесь о проверке, а просто дайте среде выполнения сгенерировать исключение nullpointer.
  2. Проверьте и сгенерируйте исключение с сообщением лучше, чем базовое сообщение NPE.

Третье решение - больше работы, но гораздо надежнее:

  1. Создайте свой класс таким образом, чтобы он _someEntityникогда не был в недопустимом состоянии. Один из способов сделать это - избавиться от него AssignEntityи потребовать его в качестве параметра для создания экземпляра. Другие методы внедрения зависимости также могут быть полезны.

В некоторых ситуациях имеет смысл проверить правильность всех аргументов функции перед любой работой, которую вы делаете, а в других имеет смысл сообщить вызывающей стороне, что они несут ответственность за достоверность своих входных данных, а не проверку. На каком конце спектра вы находитесь, зависит от вашей проблемной области. Третий вариант имеет существенное преимущество в том, что вы можете, в некоторой степени, заставить компилятор принудительно выполнять все действия вызывающей стороны.

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


10
@aroth: Любой дизайн чего-либо, включая программное обеспечение, будет иметь ограничения. Сам акт проектирования - выбор того, куда идут эти ограничения, и я не обязан и не должен принимать какие-либо предложения и ожидать, что они сделают что-то полезное с ним. Я знаю, nullявляется ли он неправильным или недействительным, потому что в моей гипотетической библиотеке я определил типы данных и определил, что является действительным, а что нет. Вы называете это «принуждением к предположениям», но угадайте, что: это то, что делает каждая существующая библиотека программного обеспечения. Есть моменты, когда null является допустимым значением, но это не «всегда».
whatsisname

4
«что ваш код сломан, потому что он не обрабатывает nullдолжным образом» - это неправильное понимание того, что означает правильная обработка. Для большинства Java-программистов это означает создание исключения для любого неверного ввода. Используя @ParametersAreNonnullByDefault, это означает также для каждого null, если аргумент не помечен как @Nullable. Делать что-либо еще означает удлинять путь до тех пор, пока он, наконец, не уйдет в другое место, и, таким образом, также увеличить время, потраченное на отладку. Что ж, игнорируя проблемы, вы можете достичь того, что это никогда не случится, но неправильное поведение тогда довольно гарантировано.
Маартинус

4
@aroth Дорогой Господь, я бы не хотел работать с любым кодом, который ты пишешь. Взрывы бесконечно лучше, чем делать что-то бессмысленное, что является единственным вариантом для неправильного ввода. Исключения вынуждают меня, вызывающего абонента, решать, что делать в тех случаях, когда метод не может угадать, что я имел в виду. Проверенные исключения и неоправданное расширение Exceptionявляются совершенно не связанными дебатами. (Я согласен, что оба хлопотны.) Для этого конкретного примера здесь, nullочевидно, нет смысла, если целью класса является вызов методов для объекта.
jpmc26

3
«Ваш код не должен выдвигать предположения в мир вызывающего» - невозможно не навязывать исходящее предположения вызывающему. Вот почему мы должны сделать эти предположения как можно более явными.
Диого Коллросс

3
@aroth ваш код не работает, потому что он передает мой код ноль, когда он должен передавать то, что имеет имя в качестве свойства. Любое поведение, кроме взрыва, является некой странной версией динамической типизации IMHO.
Мбриг

55

Поскольку _someEntityего можно изменить на любом этапе, имеет смысл проверять его каждый раз, когда SetNameвызывается. В конце концов, это могло измениться с момента последнего вызова этого метода. Но имейте в виду, что код SetNameне является потокобезопасным, поэтому вы можете выполнить эту проверку в одном потоке, _someEntityустановить в nullдругой, и тогда код все NullReferenceExceptionравно будет заблокирован .

Поэтому один из подходов к этой проблеме - стать еще более оборонительным и выполнить одно из следующих действий:

  1. сделать локальную копию _someEntityи ссылку на эту копию с помощью метода,
  2. использовать блокировку вокруг, _someEntityчтобы убедиться, что она не изменяется при выполнении метода,
  3. используйте a try/catchи выполняйте те же действия с любыми NullReferenceExceptionобъектами, которые встречаются в методе, что вы выполняете при первоначальной проверке нуля (в данном случае nopдействие).

Но что вам действительно нужно сделать, так это остановиться и задать себе вопрос: нужно ли вам на самом деле разрешить _someEntityперезапись? Почему бы не установить его один раз, через конструктор. Таким образом, вам нужно всего лишь сделать эту нулевую проверку один раз:

private readonly Entity _someEntity;

public Constructor(Entity someEntity)
{
    if (someEntity == null) throw new ArgumentNullException(nameof(someEntity));
    _someEntity = someEntity;
}

public void SetName(string name)
{
    _someEntity.Name = name;
    label.SetText(_someEntity.Name);
}

Если это вообще возможно, я бы рекомендовал такой подход в таких ситуациях. Если вы не можете помнить о безопасности потоков при выборе способа обработки возможных нулей.

В качестве бонуса я чувствую, что ваш код странный и может быть улучшен. Все:

private Entity _someEntity;
public Entity SomeEntity => _someEntity;

public void AssignEntity(Entity entity){
    _someEntity = entity;
}

можно заменить на:

public Entity SomeEntity { get; set; }

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


5
Почему это отвечает на вопрос? Вопрос касается проверки на нуль, и это в значительной степени обзор кода.
IEatBagels

17
@TopinFrassi объясняет, что текущий подход к проверке нулей не является потокобезопасным. Затем он затрагивает другие методы защитного программирования, чтобы обойти это. Затем оно заканчивается предложением об использовании неизменяемости, чтобы избежать необходимости иметь это защитное программирование в первую очередь. И в качестве дополнительного бонуса, он предлагает советы о том, как лучше структурировать код.
Дэвид Арно

23

Но должен ли метод не иметь эту проверку?

Это твой выбор.

Создавая publicметод, вы предлагаете общественности возможность его вызвать. Это всегда сопровождается неявным договором о том, как вызывать этот метод и что ожидать при этом. Этот контракт может (или не может) включать « да, хм, если вы передадите nullв качестве значения параметра, он взорвется прямо на вашем лице ». Другие части этого контракта могут быть « о, кстати: каждый раз, когда два метода выполняют этот метод одновременно, котенок умирает ».

Какой тип контракта вы хотите разработать, зависит от вас. Важные вещи должны

  • создать API, который является полезным и последовательным и

  • хорошо документировать это, особенно для тех крайних случаев.

Каковы вероятные случаи, когда ваш метод вызывается?


Что ж, безопасность потоков является исключением, а не правилом, когда существует одновременный доступ. Таким образом, использование скрытого / глобального состояния задокументировано, так как в любом случае параллелизм в любом случае безопасен.
Дедупликатор

14

Другие ответы хороши; Я хотел бы расширить их, сделав несколько комментариев о модификаторах доступности. Во-первых, что делать, если nullникогда не действует:

  • публичные и защищенные методы - это те, где вы не контролируете вызывающего. Они должны бросить, когда прошло ноль. Таким образом вы научите своих абонентов никогда не пропускать вас в ноль, потому что они хотят, чтобы их программа не вылетала.

  • внутренние и частные методы являются те , где вы действительно контролировать абонент. Они должны утверждать, когда передано нуль; потом они, вероятно, вылетят позже, но, по крайней мере, вы сначала получили утверждение и возможность взломать отладчик. Опять же, вы хотите научить своих абонентов правильно звонить вам, делая это больно, когда они этого не делают.

Теперь, что вы делаете, если это может быть допустимым для передачи нулевого значения? Я бы избежал этой ситуации с шаблоном нулевого объекта . То есть: создайте специальный экземпляр типа и потребуйте, чтобы он использовался каждый раз, когда требуется недопустимый объект . Теперь вы снова в приятном мире бросания / утверждения при каждом использовании нуля.

Например, вы не хотите быть в такой ситуации:

class Person { ... }
...
class ExpenseReport { 
  public void SubmitReport(Person approver) { 
    if (approver == null) ... do something ...

и на сайте вызова:

// I guess this works but I don't know what it means
expenses.SubmitReport(null); 

Потому что (1) вы обучаете своих вызывающих, что null является хорошим значением, и (2) неясно, какова семантика этого значения. Скорее сделайте это:

class ExpenseReport { 
  public static readonly Person ApproverNotRequired = new Person();
  public static readonly Person ApproverUnknown = new Person();
  ...
  public void SubmitReport(Person approver) { 
    if (approver == null) throw ...
    if (approver == ApproverNotRequired) ... do something ...
    if (approver == ApproverUnknown) ... do something ...

И теперь сайт вызова выглядит так:

expenses.SubmitReport(ExpenseReport.ApproverNotRequired);

и теперь читатель кода знает, что это значит.


Еще лучше, не используйте цепочку ifs для нулевых объектов, а используйте полиморфизм, чтобы заставить их вести себя правильно.
Конрад Рудольф

@KonradRudolph: Действительно, это часто хорошая техника. Мне нравится использовать это для структур данных, где я делаю EmptyQueueтип, который выбрасывает при удалении из очереди, и так далее.
Эрик Липперт

@EricLippert - простите меня, потому что я все еще учусь здесь, но предположим, что если Personкласс был неизменным и не содержал конструктор с нулевым аргументом, как бы вы сконструировали ваши ApproverNotRequiredили ApproverUnknownэкземпляры? Другими словами, какие значения вы бы передали конструктору?

2
Мне нравится сталкиваться с вашими ответами / комментариями по всей SE, они всегда проницательны. Как только я увидел утверждение о внутренних / частных методах, я прокрутил вниз, ожидая, что это будет ответ Эрика Липперта, не разочаровался :)
Боб esponja

4
Вы, ребята, обдумываете конкретный пример, который я привел. Это было предназначено, чтобы быстро понять идею нулевого объекта. Он не был предназначен для того, чтобы быть примером хорошего дизайна в системе автоматизации бумажных и зарплатных чеков. В реальной системе создание «утверждающего» типа «человек», вероятно, является плохой идеей, поскольку оно не соответствует бизнес-процессу; может не быть утверждающего, вам может понадобиться несколько и так далее. Как я часто отмечаю, отвечая на вопросы в этой области, мы действительно хотим получить объект политики . Не зацикливайтесь на примере.
Эрик Липперт,

8

Другие ответы указывают, что ваш код может быть очищен, чтобы не нуждаться в нулевой проверке, где он у вас есть, однако, для общего ответа о том, что полезная нулевая проверка может быть для рассмотрения следующего примера кода:

public static class Foo {
    public static void Frob(Bar a, Bar b) {
        if (a == null) throw new ArgumentNullException(nameof(a));
        if (b == null) throw new ArgumentNullException(nameof(b));

        a.Something = b.Something;
    }
}

Это дает вам преимущество в обслуживании. Если в вашем коде когда-либо возникает дефект, когда что-то передает нулевой объект методу, вы получите полезную информацию от метода относительно того, какой параметр был нулевым. Без проверок вы получите NullReferenceException в строке:

a.Something = b.Something;

И (учитывая, что свойство Something является типом значения), a или b могут иметь значение null, и вы не сможете узнать, какой из трассировки стека. С помощью проверок вы будете точно знать, какой объект был нулевым, что может быть чрезвычайно полезно при попытке выявить дефект.

Я хотел бы отметить, что шаблон в вашем исходном коде:

if (x == null) return;

Может использоваться в определенных обстоятельствах, а также (возможно, чаще) дает вам очень хороший способ маскировки основных проблем в вашей кодовой базе.


4

Нет, совсем нет, но это зависит.

Вы только делаете нулевую проверку ссылки, если хотите реагировать на это.

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

Если вы не выполните проверку нулевой ссылки (или сгенерируете непроверенное исключение), он может выдать непроверенную проверкуNullReferenceException , которая обычно не обрабатывается пользователем метода и даже может завершить работу приложения. Это означает, что функция, очевидно, полностью неправильно понята и, следовательно, приложение неисправно.


4

Что-то вроде расширения ответа @ null, но, по моему опыту, делать нулевые проверки, несмотря ни на что.

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

Теперь, как вы на самом деле имеете дело со nullзначением, это во многом зависит от вашего варианта использования.

Если nullэто допустимое значение, например, любой из параметров со второго по пятый для подпрограммы MSVC _splitpath (), то правильное поведение в нем.

Если nullэтого не произойдет при вызове рассматриваемой подпрограммы, т. Е. В случае OP, контракт API требует, AssignEntity()чтобы он был вызван с допустимым значением, а Entityзатем выдает исключение или иным образом завершается неудачей, гарантируя, что вы создадите много шума в процессе.

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


Или вообще избегайте этого (48 минут 29 секунд: «Никогда не используйте и не допускайте, чтобы рядом с вашей программой был нулевой указатель» ).
Питер Мортенсен
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.