Інвертування твердження IF


39

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

Скажімо, у мене є цей код:

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
    }
}

І ReSharper запропонує зробити це:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

Здається, що ReSharper ВЖЕ завжди запропонує мені інвертувати ІФ, незалежно від того, скільки гніздування відбувається. Ця проблема полягає в тому, що мені подобається гніздування принаймні в деяких випадках. Мені здається легше прочитати і зрозуміти, що відбувається в певних місцях. Це не завжди так, але я відчуваю себе комфортніше, коли гніздяться.

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


1
Можливий дублікат рівня Тримати відступ низьким
gnat

24
Ключовим тут є "Пропозиції Resharper". Погляньте на пропозицію, якщо це полегшує читання коду та є послідовним, використовуйте його. Це робиться так само, як для / для кожної петлі.
Джон Рейнор

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

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

2
Чи не пропонує ReSharper також перенести умовний запит? foreach (someObject in someObjectList.Where (object => object! = null)) {...}
Roman Reiner

Відповіді:


63

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

Ми є лише людьми, і тримати в голові кілька речей одночасно важко.

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


64
Я люблю бачити приплив думки розробників таким чином. Там так багато вкладеності в коді просто тому , що надзвичайний поширена помилка , що break, continueі кілька returnне структуровані.
JimmyJames

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

2
@Ewan: Яка різниця між "це не може бути x, або він би вийшов з циклу вгорі" і "це не може бути x, або він би не ввійшов до цього блоку"?
Collin

ніщо не є складністю та значенням x, що важливо
Ewan

4
@Ewan: Я думаю break/ continue/ returnмаю реальну перевагу перед elseблоком. З ранніми виходами існують 2 блоки : блок виходу та блок перебування там; з elseє 3 блоків : якщо, ще, і все , що приходить після того, як (який використовується незалежно від гілки прийнято). Я вважаю за краще менше блоків.
Матьє М.

33

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

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

foo != null, на жаль, став ідіомою. Я майже не помічаю окремих частин. Я дивлюся на це і просто думаю: "о, це безпечно робити fooзараз".

Якщо ви хочете перевернути це (о, колись, будь ласка), вам потрібна справді вагома справа. Вам потрібно щось, що дозволить моїм очам без особливих зусиль зісковзнути з нього і зрозуміти це в одну мить.

Однак, що ви нам дали:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

Що навіть структурно не однакове!

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    if(someGlobalObject == null) continue;
    someSideEffectObject = someGlobalObject.SomeProperty;
}

Це не те, що мій ледачий мозок очікував цього. Я думав, що можу продовжувати додавати незалежний код, але не можу. Це змушує мене думати про речі, про які зараз не хочу думати. Ти порушив мою ідіому, але не дав мені кращого. Все тому, що ти хотів захистити мене від того негативу, який спалив це неприємне маленьке «я» в моїй душі.

Тож вибачте, можливо, ReSharper має рацію запропонувати це 90% часу, але я вважаю, що ви знайшли кутовий випадок, коли це найкраще ігнорувати. Інструменти просто непогані в навчанні того, що таке читабельний код. Запитайте у людини. Зробіть експертну перевірку. Але не слід сліпо слідувати інструменту.


1
Добре, якщо у вас є більше коду в циклі, то, як він працює, залежить від того, як все це з’єднується. Це не незалежний код. Реконструйований код у запитанні був правильним для прикладу, але може бути невірним, коли він не знаходиться в ізоляції - це був пункт, який ви збираєтеся? (+1, однак, не уникайте негативів)
Балдрік

@Baldrickk if (foo != null){ foo.something() }дозволяє мені продовжувати додавати код, не піклуючись про те, чи fooбув нуль. Це не так. Навіть якщо мені не потрібно цього робити, я не відчуваю мотивації викручувати майбутнє, кажучи: "ну вони можуть просто переробляти це, якщо їм це потрібно". Feh. Програмне забезпечення є лише м'яким, якщо його легко змінити.
candied_orange

4
"продовжуйте додавати код без піклування" Або код повинен бути пов'язаний (інакше він, мабуть, повинен бути окремим) або слід бути обережним, щоб зрозуміти функціональність функції / блоку, який ви змінюєте, оскільки додавання будь-якого коду може потенційно змінити поведінку понад призначене. Для таких простих випадків, я не думаю, що це має значення в будь-якому випадку. Для більш складних випадків я б очікував, що розуміння коду стане переважним, тому я вибрав би те, що, на мою думку, є найбільш зрозумілим, який може бути будь-яким стилем.
Балдрікк

пов'язане та переплетене - це не одне і те ж.
candied_orange

1
Не уникайте негативів: D
VitalyB

20

Це старий аргумент про те, чи зламається гірше, ніж гніздування.

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

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

foreach (someObject in someObjectList.where(i=>i != null))
{
    someOtherObject = someObject.SomeProperty;
}

Очевидно, що гніздування дуже важко дотримується, коли у вас багато шарів

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
            }
        }
    }
}

Найгірше, коли у вас є обоє

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
                return;
            }
            continue;
        }
        someObject.z --;
        if(myFunctionWithSideEffects(someObject) > 6) break;
    }
}

11
Любіть цю лінію: foreach (subObject in someObjectList.where(i=>i != null))Не знаю, чому я ніколи не думав про щось подібне.
Баррі Франклін

@BarryFranklin ReSharper повинен мати підкреслену зелену крапку на передньому плані з написом "Перетворити у LINQ-вираз" як пропозицію, яка б зробила це для вас. Залежно від операції, вона може також використовувати інші методи linq, такі як Sum або Max.
Кевін Плата

@BarryFranklin Це, мабуть, пропозиція, яку ви хочете встановити на низькому рівні та використовувати лише дискреційно. Хоча це добре спрацьовує для тривіальних випадків, як цей приклад, я в більш складному коді знаходжу те, як він вибирає складнішу частину умовних умов у біти, які є Linqifiable, а решта тіла має тенденцію створювати речі, які важче зрозуміти, ніж оригінал. Я звичайно принаймні спробую пропозицію, оскільки коли вона може перетворити цілий цикл у Linq, результати часто є розумними; але половина та половина результатів, як правило, отримують некрасиві швидкі ІМО.
Ден Нілі

Як не дивно, редагування resharper має точно такий же рівень вкладеності, оскільки воно також має, якщо слідує однолінійний.
Пітер

хе, жодних брекетів, хоча! що, очевидно, дозволено: /
Ewan

6

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

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    ...  imagine that there is some code here ...

    // added later by a Junior Developer
    doSomeOtherFunction(someObject);
}

Ви хочете , щоб зателефонувати doSomeOtherFunction()за всьому SomeObject, або тільки якщо не нуль? З оригінальним кодом у вас є можливість, і він зрозуміліший. З тим, що continueможна забути "о, так, там ми пропустили нулі", і ви навіть не маєте можливості називати це для всіх деяких об'єктів, якщо ви не ставите цей виклик на початку методу, який може бути неможливим або правильним з інших причин.

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

Питання про бонус: Чому в цьому списку потрібні нулі? Можливо, буде краще ніколи не додавати нуль в першу чергу, і в цьому випадку логіка обробки набагато простіша.


12
Всередині циклу не повинно бути достатньо коду, щоб ви не бачили нульової перевірки вгорі, не прокручуючи.
Брайан Ботчер

@Bryan Boettcher погодився, але не весь код написаний так добре. І навіть кілька рядків складного коду можуть змусити забути про (або неправильну причину) продовження. На практиці я все гаразд return, тому що вони утворюють "чистий перерву", але ІМО breakі continueпризводять до плутанини, і в більшості випадків їх краще уникати.
user949300

4
@ user949300 Після десяти років розвитку я ніколи не знаходив перерви або продовжував викликати плутанину. У мене вони були заплутані, але вони ніколи не були причиною цього. Зазвичай причиною були погані рішення розробника, і вони могли б викликати розгубленість незалежно від того, якою зброєю вони користувалися.
corsiKa

1
@corsiKa Помилка Apple SSL - це фактично помилка goto, але вона могла бути просто перервою чи продовженням помилки. Однак код жахливий ...
user949300

3
@BryanBoettcher проблема здається мені, що ті ж розробники, які думають, що тривати чи кілька разів повертаються, також вважають, що закопувати їх у великі органи методу теж добре. Тож кожен досвід, який я маю з продовженням / багаторазовим поверненням, був у величезних помилках коду.
Енді,

3

Ідея полягає у якнайшвидшому поверненні. Рано returnв петлі стає рано continue.

Багато хороших відповідей на це питання: Чи слід повернутися з функції рано чи використовувати оператор if?

Зауважте, що поки питання закрите на основі думки, 430+ голосів виступають за дострокове повернення, ~ 50 голосів - це "це залежить", а 8 - проти дострокового повернення. Тож існує виключно сильний консенсус (або це, або я погано рахую, що правдоподібно).

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


2

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

Ми часто інвертуємось ifsу моїй роботі та використовуємо інший фантом. Раніше це було досить часто, коли під час перегляду PR я міг зазначити, як мій колега міг зняти три рівні гніздування! Це трапляється рідше, оскільки ми розгортаємо свої ifs.

Ось іграшковий приклад того, що можна розгорнути

function foobar(x, y, z) {
    if (x > 0) {
        if (z != null && isGamma(y)) {
            // ~10 lines of code
            // return something
        }
        else {
           return y
        }
    }
    else {
      return y + z
    }
}

Такий код, де є ОДИН цікавий випадок (де решта - це просто повернення або невеликі блоки операторів). Банально переписати вищезгадане як

function foobar(x, y, z) {
    if (x <=0) {
        return y + z
    }
    if (z == null || !isGamma(y) {
       return y
    }
    // ~ 10 lines of code
    // return something
}

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


У відповідь на зразок коду ОП, я погоджуюсь, що це великий обвал. Тим більше, що в 1 ТБ, стилі, який я використовую, інверсія викликає збільшення коду в розмірі.


0

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

За інших рівних умов краще вкладати менше гнізд, саме тому ReSharper запропонує трансформацію, яка зменшить гніздування. Але всі інші речі не є рівними: у цьому випадку для трансформації потрібне введення а continue, що означає, що за результатом коду насправді важче слідувати.

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

У вашому випадку пропозиція ReSharper погіршить код. Тож просто ігноруйте це. Або ще краще: не використовуйте запропоновану трансформацію, але трохи подумайте, як можна вдосконалити код. Зверніть увагу, що ви можете призначити одну і ту ж змінну кілька разів, але тільки останнє призначення має ефект, оскільки попереднє було б просто перезаписане. Це робить вигляд , як помилка. Пропозиція ReSharpers не виправляє помилку, але це робить трохи очевиднішим, що відбувається якась сумнівна логіка.


0

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

for(obj in objectList) {
    if(obj == null) continue;
    if(obj.getColour().equals(Color.BLUE)) {
        // Handle blue objects
    } else {
        // Handle non-blue objects
    }
}

Зауважте, що в Java Streams або інших функціональних ідіомах ви можете відокремити фільтрування від циклу, використовуючи:

items.stream()
    .filter(i -> (i != null))
    .filter(i -> i.getColour().equals(Color.BLUE))
    .forEach(i -> {
        // Process blue objects
    });

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

foreach my $item (@items) {
    next unless defined $item;
    carp "Only blue items are supported! Failed on item $item" 
       unless ($item->get_colour() eq 'blue');
    ...
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.