Оцінка короткого замикання, чи це погана практика?


88

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

if (smartphone != null && smartphone.GetSignal() > 50)
{
   // Do stuff
}

У цьому випадку код призведе до того, щоб спочатку перевірити, що об’єкт не є нульовим, а потім використовувати цей об'єкт, знаючи, що він існує. Мова розумна, тому що вона знає, що якщо перше твердження, якщо помилкове, то немає сенсу навіть оцінювати друге твердження, і тому нульове посилання на виняток ніколи не кидається. Це працює так само andі для orоператорів.

Це може бути корисно і в інших ситуаціях, наприклад, перевірка того, що індекс знаходиться в межах масиву, і така методика, наскільки мені відомо, може бути виконана різними мовами: Java, C #, C ++, Python та Matlab.

Моє запитання: чи цей код є репрезентацією поганої практики? Чи виникає ця погана практика з якихось прихованих технічних проблем (тобто це в кінцевому підсумку може призвести до помилки) чи це призводить до проблеми читабельності для інших програмістів? Чи може це бути заплутано?


69
Технічний термін - це оцінка короткого замикання . Це добре відома техніка реалізації, і там, де мовний стандарт гарантує це, покладатися на неї - це добре. (Якщо це не так, це не багато мови, ІМО.)
Кіліан Фот

3
Більшість мов дозволяють вам вибрати, яку поведінку ви хочете. У C # оператора ||коротке замикання, оператор |(одна труба) не робить ( &&і &поводиться так само). Оскільки оператори короткого замикання використовуються набагато частіше, я рекомендую позначити звичаї не короткого замикання за допомогою коментаря, щоб пояснити, навіщо потрібна їх поведінка (переважно такі речі, як виклики методів, які мають відбуватися).
linac

14
@linac тепер покласти щось праворуч &або |переконатися, що побічний ефект трапиться - це те, що я б сказав, звичайно, є поганою практикою: це не коштуватиме вам нічого, щоб перевести цей виклик методу у свою лінію.
Джон Ханна

14
@linac: У C і C ++ &&є коротке замикання і &його немає, але вони дуже різні оператори. &&є логічним "і"; &є побітним "і". Це може x && yбути правдою, а x & yнеправдою.
Кіт Томпсон

3
Ваш конкретний приклад може бути поганою практикою і з іншої причини: що , якщо це є нульовим? Чи розумно це для цього бути нульовим? Чому це недійсне? Чи повинна виконуватися решта функції поза цим блоком, якщо вона є недійсною, чи вся справа нічого не робить, або ваша програма зупиняється? Оформлення "if null" над кожним доступом (можливо, через виключення з нульовою посиланням ви поспішали позбутися) означає, що ви можете досить далеко потрапити в решту програми, не помічаючи ніколи, що ініціалізація об'єкта телефону накручена вгору І в кінці кінців, коли ви , нарешті , отримати трохи коду , який робить НЕ
Random832

Відповіді:


125

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

Ваш приклад коду досить зрозумілий і, дійсно, це найчастіше найкращий спосіб його написати. Альтернативи (такі як вкладені ifвисловлювання) були б м'яснішими, складнішими і тому важче зрозуміти.

Пара застережень:

Використовуйте здоровий глузд щодо складності та читабельності

Далі не така гарна ідея:

if ((text_string != null && replace(text_string,"$")) || (text_string = get_new_string()) != null)

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

Якщо вам потрібно впоратися з помилками, часто є кращий спосіб

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

Уникайте повторних перевірок того ж стану

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

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


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

1
@Neil, хороший момент; Я оновив відповідь.

2
Єдине, що я можу додати, - це зазначити ще один необов'язковий спосіб поводження з ним, який дійсно бажано, якщо ви збираєтеся перевіряти щось у кількох різних умовах: перевірте, чи є щось недійсним у if (), то повернути / кинути - інакше просто продовжуйте йти. Це виключає вкладення і часто може зробити код більш явним і короткочасним, оскільки "в цьому" не повинно бути недійсним, і якщо це, то я перебуваю тут "- розміщується якомога раніше в коді. Чудово, коли ви кодуєте щось, що перевіряє конкретний стан більше, ніж в одному місці методу.
БрайанХ

1
@ dan1111 Я б узагальнив приклад, який ви наводили як "Умова не повинна містити побічного ефекту (наприклад, присвоєння змінної вище). Оцінка короткого замикання цього не змінює і не повинна використовуватися як короткочасне для ефект, який міг би так само легко розміститися в тілі, якщо краще представити відносини "якщо / тоді".
AaronLS

@AaronLS, я категорично не згоден з твердженням, що "умова не повинна містити побічних ефектів". Стан не повинен бути зайвим заплутаним, але, поки це чіткий код, я добре з побічним ефектом (або навіть більше ніж один побічний ефект) у стані.

26

Скажімо, ви використовували мову в стилі С без &&і потрібно було робити еквівалентний код, як у вашому запитанні.

Ваш код буде:

if(smartphone != null)
{
  if(smartphone.GetSignal() > 50)
  {
    // Do stuff
  }
}

Ця картина виявиться багато.

Тепер уявімо версію 2.0 нашої гіпотетичної мови &&. Подумайте, як класно ви могли б подумати, що це було!

&&є визнаним, ідіоматичним засобом робити те, що робить мій приклад вище. Це не погана практика використовувати його, дійсно погана практика не використовувати його у випадках, як описано вище: Досвідчений кодер такою мовою буде цікаво, де elseбула чи інша причина, щоб не робити речі звичайним способом.

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

Ні, ви розумні, бо ви знали, що якщо перше твердження було помилковим, то немає сенсу навіть оцінювати друге твердження. Мова німий, як коробка скель, і робив те, що ти йому сказав. (Джон Маккарті був ще розумнішим, і зрозумів, що оцінка короткого замикання буде корисною для мов).

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

Поміркуйте:

if(smartphone != null && smartphone.GetSignal() > ((range = (range != null ? range : GetRange()) != null && range.Valid ? range.MinSignal : 50)

Це розширює ваш код, щоб перевірити range. Якщо значення rangenull, воно намагається встановити його за допомогою виклику, GetRange()хоча це може бути невдалим, тому rangeвсе ще може бути null. Якщо діапазон після цього не є нульовим, і це Validйого MinSignalвластивість, інакше використовується за замовчуванням 50.

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

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

Також:

if(smartphone != null && (smartphone.GetSignal() == 2 || smartphone.GetSignal() == 5 || smartphone.GetSignal() == 8 || smartPhone.GetSignal() == 34))
{
  // do something
}

Тут я поєдную використання використання &&з перевіркою на певні значення. Що стосується сигналів телефону, це не реально, але в інших випадках. Ось, це приклад того, що я недостатньо розумний. Якби я зробив таке:

if(smartphone != null)
{
  switch (smartphone.GetSignal())
  {
    case 2: case 5: case 8: case 34:
      // Do something
      break;
  }
}

Я отримав би як читабельність, так і ймовірну ефективність (кілька викликів GetSignal(), можливо, не було б оптимізовано).

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

Заключний випадок, який відходить від найкращої практики, це:

if(a && b)
{
  //do something
}

У порівнянні з:

if(a & b)
{
  //do something
}

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

Однак з точки зору ефективності будь-який з двох, ймовірно, буде кращим. Перший, очевидно, буде виконувати менше коду (зовсім не оцінюючи bза один шлях коду), що може економити нам скільки завгодно часу, необхідного для оцінки b.

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

if(a)
{
  if(b)
  {
    // Do something
  }
}

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

З цієї причини if(a & b)код може бути більш ефективним у деяких випадках.

Тут я б сказав, що if(a && b)це все-таки найкращий підхід для початку: Поширеніший, єдиний життєздатний в деяких випадках (де bпомилка, якщо aпомилковий), і швидше частіше, ніж ні. Варто відзначити, що if(a & b)часто корисна оптимізація щодо неї в деяких випадках.


1
Якщо у вас є tryметоди, які повертаються trueу випадку успіху, що ви думаєте if (TryThis || TryThat) { success } else { failure }? Це менш поширена ідіома, але я не знаю, як виконати те ж саме, без дублювання коду чи додавання прапора. Хоча пам'ять, необхідна для прапора, не є проблемою, з точки зору аналізу, додаючи прапор, ефективно розбивається код на два паралельних всесвіту - один, що містить заяви, які доступні, коли прапор, trueа інший твердження доступні, коли він є false. Іноді добре з сухих позицій, але примхливе.
supercat

5
Я нічого не бачу в цьому if(TryThis || TryThat).
Джон Ханна

3
Проклята прекрасна відповідь, сер.
єпископ

FWIW, відмінні програмісти, серед яких Kernighan, Plauger & Pike з Bell Labs, рекомендують для наочності використовувати неглибокі керуючі структури, if {} else if {} else if {} else {}а не вкладені структури управління на зразок if { if {} else {} } else {}, навіть якщо це означає оцінювати один і той же вираз не один раз. Лінус Торвальдс сказав щось подібне: "якщо тобі потрібно більше 3-х рівнів відступу, ти все одно накрутився". Візьмемо це як голосування за операторів короткого замикання.
Сем Уоткінс

if (&& b) заява; досить легко замінити. if (a && b&& c && d) заява1; else твердження2; - це справжній біль.
gnasher729

10

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

open($handle, "<", "filename.txt") or die "Couldn't open file!";

є ідіоматичним. openповертає щось ненулеве на успіх (так, що dieчастина після цього orніколи не буває), але невизначена при відмові, і тоді відбувається заклик до дій die(це спосіб Перла зібрати виняток).

Це добре в мовах, де це роблять усі.


17
Найбільший внесок Perl у мовний дизайн - це надання декількох прикладів того, як цього не зробити.
Джек Едлі

@JackAidley: Хоча я сумую за unlessумовами Perl та Postfix ( send_mail($address) if defined $address).
RemcoGerlich

6
@JackAidley, чи можете ви вказати, чому "або померти" це погано? Це просто наївний спосіб поводження з винятками?
bigstones

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

1
@RemcoGerlich Рубі успадкував деякі з цього стилю:send_mail(address) if address
бон

6

Хоча я загалом погоджуюся з відповіддю dan1111 , є один особливо важливий випадок, який він не охоплює: випадок, коли smartphoneвикористовується одночасно. У такому сценарії подібний зразок є добре відомим джерелом важко знайти помилок.

Проблема полягає в тому, що оцінка короткого замикання не є атомною. Ваш потік може перевірити, що smartphoneє null, інша нитка може увійти і звести нанівець її, і тоді ваша нитка негайно намагається GetSignal()і підірветься.

Але, звичайно, це тільки робить, коли зірки вирівнюють і час ваших потоків є тільки правом.

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


3

Існує безліч ситуацій, коли я хочу перевірити одну умову першою, і лише хочу перевірити другу умову, якщо перша умова вдалася. Іноді виключно для ефективності (тому що немає сенсу перевіряти другу умову, якщо перша вже не вдалася), іноді тому, що в іншому випадку моя програма вийде з ладу (ваша перевірка на NULL), іноді тому, що в іншому випадку результати були б жахливими. (ЯКЩО (я хочу надрукувати документ) ТА (друк документа не вдалося)) ТОГО відображається повідомлення про помилку - ви не хочете роздрукувати документ і перевірити, чи не вдалося надрукувати, якщо ви не хотіли надрукувати документ у першість!

Таким чином, виникла велика потреба у гарантованому короткому замиканні логічних виразів. Він не був у Паскаля (який мав негарантовану оцінку короткого замикання), що було головним болем; Я вперше побачив це в Ада та С.

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


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

2

Одне врахування, яке не згадується в інших відповідях: іноді ці перевірки можуть натякати на можливе рефакторинг на модель дизайну Null Object . Наприклад:

if (currentUser && currentUser.isAdministrator()) 
  doSomething();

Можна спростити просто:

if (currentUser.isAdministrator())
  doSomething ();

Якщо currentUserза замовчуванням для якогось "анонімного користувача" або "нульового користувача" є резервна реалізація, якщо користувач не входить у систему.

Не завжди кодовий запах, але щось врахувати.


-4

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

тобто

 if(quickLogicA && slowLogicB) {doSomething();}

добре, але

if(logicA && doSomething()) {andAlsoDoSomethingElse();}

погано, і напевно ніхто не погодився б ?!

if(doSomething() || somethingElseIfItFailed() && anotherThingIfEitherWorked() & andAlwaysThisThing())
{/* do nothing */}

Обґрунтування:

Помилки вашого коду, якщо обидві сторони оцінюються, а не просто не виконують логіку. Стверджувалося, що це не проблема, оператор 'і' визначено в c #, тому сенс зрозумілий. Однак я стверджую, що це неправда.

Причина 1. Історична

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

Хоча в c # мовна специфікація гарантує булеві "та" оператор робить коротке замикання. Це не стосується всіх мов та компіляторів.

wikipeda перелічує різні мови та їхнє коротке замикання http://en.wikipedia.org/wiki/Short-circuit_evaluation, оскільки, можливо, існує багато підходів. І ще кілька додаткових проблем я тут не вникаю.

Зокрема, FORTRAN, Pascal та Delphi мають прапори компілятора, які перемикають таку поведінку.

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

Причина 2. Мовні відмінності

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

Зокрема, оператор VB.Net 'і' не має короткого замикання, а TSQL має деякі особливості.

Зважаючи на те, що сучасне "рішення", ймовірно, включає декілька мов, таких як javascript, regex, xpath і т. Д., Ви повинні це врахувати, щоб зробити чіткіше функціональність вашого коду.

Причина 3. Відмінності мови

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

Причина 4. Ваш конкретний приклад нульової перевірки параметра функції

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

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

Однак він не вдається кращої практики з кількох причин!

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

    if (smartphone == null)
    {
        //throw an exception
    }
    // perform functionality
    
  2. Ви викликаєте функцію в умовному операторі. Хоча назва вашої функції означає, що вона просто обчислює значення, вона може приховувати побічні ефекти. напр

    Smartphone.GetSignal() { this.signal++; return this.signal; }
    
    else if (smartphone.GetSignal() < 1)
    {
        // Do stuff 
    }
    else if (smartphone.GetSignal() < 2)
    {
        // Do stuff 
    }
    

    найкраща практика може запропонувати:

    var s = smartphone.GetSignal();
    if(s < 50)
    {
        //do something
    }
    

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

Я думаю, ви виявите, що це стосується і інших звичних звичаїв. Ви повинні пам’ятати, що у нас також є:

рядкові умови

var s = smartphone!=null ? smartphone.GetSignal() : 0;

нульова злиття

var s = smartphoneConnectionStatus ?? "No Connection";

які забезпечують подібну функцію короткої довжини коду з більшою чіткістю

численні посилання на підтримку всіх моїх пунктів:

https://stackoverflow.com/questions/1158614/what-is-the-best-practice-in-case-one-argument-is-null

Перевірка параметрів конструктора в C # - Найкращі практики

Чи слід перевіряти на null, якщо він не очікує нуля?

https://msdn.microsoft.com/en-us/library/seyhszts%28v=vs.110%29.aspx

https://stackoverflow.com/questions/1445867/why-would-a-language-not-use-short-circuit-evaluation

http://orcharddojo.net/orchard-resources/Library/DevelopmentGuidelines/BestPractices/CSharp

приклади прапорів компілятора, які впливають на коротке замикання:

Fortran: "sce | nosce За замовчуванням компілятор виконує оцінку короткого замикання у вибраних логічних виразах, використовуючи правила XL Fortran. Вказівка ​​sce дозволяє компілятору використовувати правила, які не є XL Fortran. Компілятор буде виконувати оцінку короткого замикання, якщо поточні правила дозволяють це . За замовчуванням - NACE. "

Паскаль: "В - Директива прапора, яка контролює оцінку короткого замикання. Для отримання додаткової інформації про оцінку короткого замикання див. Порядок оцінки операндів діадичних операторів. Для отримання додаткової інформації див. Також опцію компілятора -sc для компілятора командного рядка ( задокументовано в Посібнику користувача). "

Delphi: "Delphi підтримує оцінку короткого замикання за замовчуванням. Її можна вимкнути за допомогою директиви компілятора {$ BOOLEVAL OFF}."


Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
Світовий інженер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.