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


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.