Якщо функції повинні зробити нульові перевірки перед тим, як виконати задумане поведінку, це погана конструкція?


67

Тому я не знаю, чи це добре, чи погано дизайн коду, тому я подумав, що краще запитати.

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

Для дуже базового прикладу:

// 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
Що станеться, якщо я дійсно хочу, щоб деякийEntity був Null? Ви вирішуєте, що хочете, або someEntity може бути Null, або він не може, а потім відповідно пишете свій код. Якщо ви хочете, щоб це ніколи не було Null, ви можете замінити чеки на твердження.
gnasher729

5
Ви можете спостерігати, як Межі Гері Бернарда говорять про чіткий поділ між санітарними даними (тобто перевірка наявності нулей тощо) у зовнішній оболонці та наявністю внутрішньої ядерної системи, яка не повинна піклуватися про ці речі - і просто робити це робота.
mgarciaisaia

1
З C # 8 вам потенційно ніколи не потрібно турбуватися про нульові винятки посилань із увімкненими правильними налаштуваннями: blogs.msdn.microsoft.com/dotnet/2017/11/15/…
JᴀʏMᴇᴇ

3
Це одна з причин, чому мовна команда C # хоче ввести нульові посилання (та нерегульовані
Mafii

Відповіді:


168

Проблема з вашим основним прикладом - це не нульова перевірка, це тиха помилка .

Помилки нуля вказівника / посилання, частіше за все, є помилками програміста. Помилки програміста найчастіше найкраще усунути, якщо негайно і голосно виходять з ладу. У вас є три загальних способи вирішити проблему в цій ситуації:

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

Третє рішення - це більше роботи, але набагато надійніше:

  1. Структуруйте свій клас таким чином, щоб практично неможливо _someEntityбуло колись перебувати в недійсному стані. Один із способів зробити це - позбутися AssignEntityі вимагати цього як параметра для інстанції. Інші методи ін'єкційних залежностей можуть бути також корисними.

У деяких ситуаціях є сенс перевірити всі аргументи функції на достовірність перед будь-якою роботою, яку ви виконуєте, а в інших має сенс сказати абоненту, що він несе відповідальність за те, щоб їх введення були дійсними, а не перевіряли. Який кінець спектра ви будете залежати від вашої проблемної області. Третій варіант має істотну перевагу в тому, що ви можете, в певній мірі, змусити компілятор домогтися того, щоб абонент все робив правильно.

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


10
@aroth: Будь-яка конструкція будь-чого, включаючи програмне забезпечення, матиме обмеження. Сам акт проектування - це вибір того, куди йдуть ці обмеження, і це не моя відповідальність, а також не те, що я приймаю будь-який внесок і від нього потрібно очікувати, щоб зробити щось корисне. Я точно знаю null, неправильно чи недійсно, тому що в своїй гіпотетичній бібліотеці я визначив типи даних і я визначив, що є дійсним, а що ні. Ви називаєте це "примушуючи припущення", але вгадайте, що: саме так робить кожна існуюча бібліотека програмного забезпечення. Бувають випадки, коли null є дійсним значенням, але це не "завжди".
whatsisname

4
"що ваш код порушений, оскільки він не працює nullналежним чином" - Це нерозуміння того, що означає правильне поводження. Для більшості Java-програмістів це означає викид для будь-якого недійсного введення. Використовуючи @ParametersAreNonnullByDefault, це означає також для кожного null, якщо аргумент не позначений як @Nullable. Робити що-небудь інше означає подовжувати шлях, поки він нарешті не здує в іншому місці, і, таким чином, також подовжується витрачений час на налагодження. Ну, ігноруючи проблеми, ви можете домогтися того, що вона ніколи не дме, але неправильна поведінка тоді досить гарантована.
maaartinus

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

3
"Ваш код не повинен виганяти припущення у світ абонента" - Не можна не вимушувати припущення для абонента. Ось чому нам потрібно зробити ці припущення максимально явними.
Діого Коллросс

3
@aroth ваш код порушений, оскільки його передача мого коду є нульовою, коли він повинен передавати те, що має ім'я як властивість. Будь-яка поведінка, окрім вибуху, є якоюсь задньою дивною версією динамічного набору IMHO.
mbrig

56

Оскільки _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як значення параметра, воно вибухне прямо у вашому обличчі ". Іншими частинами цього договору можуть бути " о, BTW: кожного разу, коли дві нитки одночасно виконують цей метод, кошеня гине ".

Який тип контракту ви хочете скласти, залежить від вас. Важливі речі - це

  • створити корисний та послідовний 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) ви навчаєте своїх абонентів, що нуль є хорошим значенням, і (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
Я люблю натрапляти на ваші відповіді / коментарі по всій тематиці, вони завжди проникливі. Щойно я побачив, що стверджує про внутрішні / приватні методи, я прокрутився вниз, очікуючи, що це буде відповідь Еріка Ліпперта, не розчарувався :)
bob 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 може бути нульовим, і ви не зможете дізнатися, який із сліду стека. За допомогою чеків ви точно дізнаєтесь, який об’єкт був нульовим, що може бути надзвичайно корисно при спробі відібрати дефект.

Зауважу, що шаблон у вашому оригінальному коді:

if (x == null) return;

Може використовувати його за певних обставин, це може також (можливо частіше) дати вам дуже хороший спосіб маскування основних проблем у вашій базі коду.


4

Ні взагалі, але це залежить.

Ви робите нульову перевірку, якщо ви хочете на неї реагувати.

Якщо ви робите нульову перевірку довідок і кидаєте перевірений виняток , ви змушуєте користувача методу реагувати на нього і дозволяєте йому відновитись. Програма як і раніше працює як очікувалося.

Якщо ви не зробите нульову перевірку посилань (або викинете неперевірений виняток), вона може викинути невірно NullReferenceException, що зазвичай не обробляється користувачем методу і навіть може закрити програму. Це означає, що функція, очевидно, повністю не зрозуміла, а тому програма несправна.


4

Щось розширення для відповіді @ null, але, на моєму досвіді, робити недійсні перевірки незалежно від того.

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

Тепер, як ви насправді маєте справу з nullцінністю, багато що залежить від вашої справи використання.

Якщо nullє прийнятним значенням, наприклад, будь-який з другого по п'ятий параметри для MSVC звичайного _splitpath (), то мовчки мати справу з ним - це правильна поведінка.

Якщо nullніколи не трапляється під час виклику відповідної рутини, тобто у випадку з ОП, контракт API AssignEntity()повинен бути викликаний дійсним, Entityтоді викиньте виняток, або іншим чином не вдасться забезпечити багато шуму в процесі.

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


Або уникайте цього взагалі (48 хвилин 29 секунд: "Ніколи ніколи не використовуйте та не дозволяйте ніде біля вашої програми нульовий покажчик" ).
Пітер Мортенсен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.