instrukcja if - ocena zwarcia a czytelność


90

Czasami ifinstrukcja może być dość skomplikowana lub długa, dlatego ze względu na czytelność lepiej jest wyodrębnić skomplikowane wywołania przed rozszerzeniem if.

np. to:

if (SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall())
{
    // do stuff
}

zaangażowany w to

bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

if (b1 || b2)
{
    //do stuff
}

(podany przykład nie jest taki zły, to tylko dla ilustracji ... wyobraź sobie inne wywołania z wieloma argumentami itp.)

Ale z tym wydobyciem straciłem ocenę zwarcia (SCE).

  1. Czy naprawdę tracę SCE za każdym razem? Czy istnieje scenariusz, w którym kompilator może go „zoptymalizować” i nadal zapewniać SCE?
  2. Czy istnieją sposoby na utrzymanie lepszej czytelności drugiego fragmentu bez utraty SCE?

20
Praktyka pokazuje, że większość odpowiedzi na temat wydajności, które zobaczysz tutaj lub w innych miejscach, jest w większości przypadków błędnych (4 złe 1 poprawne). Radzę zawsze zrobić profilowanie i sprawdzić to samemu, unikniesz „przedwczesnej optymalizacji” i nauczysz się nowych rzeczy.
Marek R

25
@MarekR to nie tylko wydajność, chodzi o możliwe skutki uboczne w OtherCunctionCall ...
relaxxx

3
@David, odsyłając do innych witryn, często pomocne jest wskazanie, że publikowanie krzyżowe jest niezadowolone
gnat

7
Jeśli czytelność jest Twoim głównym problemem, nie wywołuj funkcji z efektami ubocznymi wewnątrz warunku warunkowego
Morgen

3
Potencjalni bliscy wyborcy: przeczytaj ponownie pytanie. Część (1) nie jest oparta na opiniach, podczas gdy część (2) może łatwo przestać być oparta na opiniach poprzez edycję, która usuwa odniesienie do jakiejkolwiek rzekomej „najlepszej praktyki”, co mam zamiar zrobić.
duplode

Odpowiedzi:


119

Jedno naturalne rozwiązanie wyglądałoby tak:

bool b1 = SomeCondition();
bool b2 = b1 || SomeOtherCondition();
bool b3 = b2 || SomeThirdCondition();
// any other condition
bool bn = bn_1 || SomeFinalCondition();

if (bn)
{
  // do stuff
}

Ma to tę zaletę, że jest łatwy do zrozumienia, ma zastosowanie we wszystkich przypadkach i wykazuje właściwości zwarciowe.


To było moje początkowe rozwiązanie: dobry wzorzec w wywołaniach metod i treściach pętli for jest następujący:

if (!SomeComplicatedFunctionCall())
   return; // or continue

if (!SomeOtherComplicatedFunctionCall())
   return; // or continue

// do stuff

Otrzymuje się te same niezłe korzyści wydajnościowe wynikające z oceny zwarć, ale kod wygląda na bardziej czytelny.


4
@relaxxx: Rozumiem, ale „więcej rzeczy do zrobienia po if” to także znak, że twoja funkcja lub metoda jest za duża i powinna zostać podzielona na mniejsze. Nie zawsze jest to najlepszy sposób, ale bardzo często jest!
mike3996

2
narusza to zasadę białej listy
JoulinRouge

13
@JoulinRouge: Interesujące, nigdy nie słyszałem o tej zasadzie. Osobiście wolę to podejście „zwarciowe” ze względu na korzyści związane z czytelnością: zmniejsza wcięcia i eliminuje możliwość, że coś się wydarzy po wciętym bloku.
Matthieu M.,

2
Czy jest bardziej czytelny? Nazwij b2poprawnie, a otrzymasz someConditionAndSomeotherConditionIsTrue, nie super znaczące. Poza tym podczas tego ćwiczenia muszę utrzymywać kilka zmiennych na swoim mentalnym stosie (i do czasu aż przestanę pracować w tym zakresie). Wybrałbym SJuan76rozwiązanie numer 2 lub po prostu umieściłbym całość w funkcji.
Nathan Cooper

2
Nie przeczytałem wszystkich komentarzy, ale po szybkim wyszukiwaniu nie znalazłem dużej zalety pierwszego fragmentu kodu, a mianowicie debugowania. Umieszczanie rzeczy bezpośrednio w instrukcji if zamiast wcześniejszego przypisywania ich do zmiennej, a następnie używanie tej zmiennej sprawia, że ​​debugowanie jest trudniejsze niż powinno. Używanie zmiennych pozwala również na semantyczne grupowanie wartości, co zwiększa czytelność.
rbaleksandar

30

Mam tendencję do rozkładania warunków na wiele linii, tj .:

if( SomeComplicatedFunctionCall()
 || OtherComplicatedFunctionCall()
  ) {

Nawet jeśli masz do czynienia z wieloma operatorami (&&), musisz po prostu przesuwać wcięcia z każdą parą nawiasów. SCE nadal działa - nie ma potrzeby używania zmiennych. Pisanie kodu w ten sposób już od lat sprawiło, że stał się on dla mnie dużo bardziej czytelny. Bardziej złożony przykład:

if( one()
 ||( two()> 1337
  &&( three()== 'foo'
   || four()
    )
   )
 || five()!= 3.1415
  ) {

28

Jeśli masz długie łańcuchy warunków i chcesz zachować niektóre z nich, możesz użyć zmiennych tymczasowych, aby połączyć wiele warunków. Biorąc twój przykład, można by zrobić np

bool b = SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
if (b && some_other_expression) { ... }

Jeśli masz kompilator obsługujący C ++ 11, możesz użyć wyrażeń lambda do łączenia wyrażeń w funkcje, podobnie jak powyżej:

auto e = []()
{
    return SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
};

if (e() && some_other_expression) { ... }

21

1) Tak, nie masz już SCE. W przeciwnym razie miałbyś to

bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

działa w jedną lub drugą stronę, w zależności od tego, czy ifpóźniej pojawi się instrukcja. Zbyt skomplikowane.

2) Jest to oparte na opiniach, ale w przypadku dość złożonych wyrażeń możesz zrobić:

if (SomeComplicatedFunctionCall()
    || OtherComplicatedFunctionCall()) {

Jeśli jest to zbyt skomplikowane, oczywistym rozwiązaniem jest utworzenie funkcji, która ocenia wyrażenie i wywołuje ją.


21

Możesz także użyć:

bool b = someComplicatedStuff();
b = b || otherComplicatedStuff(); // it has to be: b = b || ...;  b |= ...; is bitwise OR and SCE is not working then 

i SCE będzie działać.

Ale nie jest dużo bardziej czytelny niż na przykład:

if (
    someComplicatedStuff()
    ||
    otherComplicatedStuff()
   )

3
Nie przepadam za łączeniem wartości logicznych z operatorem bitowym, ale nie wydaje mi się to dobrze napisane. Generalnie używam tego, co wygląda na najbardziej czytelne, chyba że pracuję na bardzo niskim poziomie i liczbie cykli procesora.
Ant

3
Użyłem w szczególności b = b || otherComplicatedStuff();i @SargeBorsch dokonał edycji, aby usunąć SCE. Dziękuję za zwrócenie uwagi na tę zmianę @Ant.
KIIV

14

1) Czy naprawdę tracę SCE za każdym razem? Czy kompilator może „optymalizować” jakiś scenariusz i nadal zapewniać SCE?

Myślę, że taka optymalizacja nie jest dozwolona; zwłaszcza OtherComplicatedFunctionCall()może mieć pewne skutki uboczne.

2) Jaka jest najlepsza praktyka w takiej sytuacji? Czy jest tylko możliwość (gdy chcę SCE) mieć wszystko, czego potrzebuję bezpośrednio w środku, jeśli i „po prostu sformatuj go, aby był jak najbardziej czytelny”?

Wolę zamienić to na jedną funkcję lub jedną zmienną o opisowej nazwie; co pozwoli zachować zarówno ocenę zwarć, jak i czytelność:

bool getSomeResult() {
    return SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
}

...

if (getSomeResult())
{
    //do stuff
}

A ponieważ implementujemy w getSomeResult()oparciu o SomeComplicatedFunctionCall()i OtherComplicatedFunctionCall(), możemy je dekomponować rekurencyjnie, jeśli nadal są skomplikowane.


2
Podoba mi się to, ponieważ możesz zwiększyć czytelność, nadając funkcji opakowania opisową nazwę (choć prawdopodobnie nie getSomeResult), zbyt wiele innych odpowiedzi tak naprawdę nie dodaje nic wartościowego
aw04

9

1) Czy naprawdę tracę SCE za każdym razem? Czy kompilator może „optymalizować” jakiś scenariusz i nadal zapewniać SCE?

Nie, nie masz, ale jest to stosowane inaczej:

if (SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall())
{
    // do stuff
}

Tutaj kompilator nie będzie nawet działał, OtherComplicatedFunctionCall()jeśli SomeComplicatedFunctionCall()zwróci true.

bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

if (b1 || b2)
{
    //do stuff
}

Tutaj obie funkcje będą działać, ponieważ muszą być zapisane w b1i b2. Ff nie będzie b1 == truewtedy b2oceniana (SCE). Ale OtherComplicatedFunctionCall()już został uruchomiony.

Jeśli nie b2jest używane nigdzie indziej, kompilator może być wystarczająco inteligentny, aby wbudować wywołanie funkcji wewnątrz if, jeśli funkcja nie ma obserwowalnych skutków ubocznych.

2) Jaka jest najlepsza praktyka w takiej sytuacji? Czy jest tylko możliwość (gdy chcę SCE) mieć wszystko, czego potrzebuję bezpośrednio w środku, jeśli i „po prostu sformatuj go, aby był jak najbardziej czytelny”?

To zależy. Jeśli musisz OtherComplicatedFunctionCall() uruchomić z powodu skutków ubocznych lub wydajność funkcji jest minimalna, powinieneś użyć drugiego podejścia dla czytelności. W przeciwnym razie trzymaj się SCE przy pierwszym podejściu.


8

Inna możliwość zwarcia i ma warunki w jednym miejscu:

bool (* conditions [])()= {&a, &b, ...}; // list of conditions
bool conditionsHold = true;
for(int i= 0; i < sizeOf(conditions); i ++){
     if (!conditions[i]()){;
         conditionsHold = false;
         break;
     }
}
//conditionsHold is true if all conditions were met, otherwise false

Możesz umieścić pętlę w funkcji i pozwolić funkcji zaakceptować listę warunków i wyprowadzić wartość logiczną.


1
@Erbureth Nie, nie są. Elementy tablicy są wskaźnikami do funkcji, nie są wykonywane, dopóki funkcje nie zostaną wywołane w pętli.
Barmar

Dzięki Barmar, ale dokonałem edycji, Erbureth miał rację przed edycją (myślałem, że moja edycja będzie się bardziej bezpośrednio rozprzestrzeniać wizualnie).
levilime

4

Bardzo dziwne: mówisz o czytelności, gdy nikt nie wspomina o użyciu komentarza w kodzie:

if (somecomplicated_function() || // let me explain what this function does
    someother_function())         // this function does something else
...

Poza tym zawsze poprzedzam swoje funkcje komentarzami dotyczącymi samej funkcji, jej danych wejściowych i wyjściowych, a czasami umieszczam przykład, jak widać tutaj:

/*---------------------------*/
/*! interpolates between values
* @param[in] X_axis : contains X-values
* @param[in] Y_axis : contains Y-values
* @param[in] value  : X-value, input to the interpolation process
* @return[out]      : the interpolated value
* @example          : interpolate([2,0],[3,2],2.4) -> 0.8
*/
int interpolate(std::vector<int>& X_axis, std::vector<int>& Y_axis, int value)

Oczywiście formatowanie komentarzy może zależeć od środowiska programistycznego (Visual studio, JavaDoc w Eclipse, ...)

Jeśli chodzi o SCE, zakładam, że masz na myśli co następuje:

bool b1;
b1 = somecomplicated_function(); // let me explain what this function does
bool b2 = false;
if (!b1) {                       // SCE : if first function call is already true,
                                 // no need to spend resources executing second function.
  b2 = someother_function();     // this function does something else
}

if (b1 || b2) {
...
}

-7

Czytelność jest konieczna, jeśli pracujesz w firmie, a Twój kod będzie czytany przez kogoś innego. Jeśli piszesz program dla siebie, to od Ciebie zależy, czy chcesz poświęcić wydajność na rzecz zrozumiałego kodu.


23
Mając na uwadze, że „ty za sześć miesięcy” to zdecydowanie „ktoś inny”, a czasem „ty jutro”. Nigdy nie poświęciłbym czytelności na rzecz wydajności, dopóki nie miałbym solidnych dowodów na problem z wydajnością.
Martin Bonner wspiera Monikę
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.