Jak uniknąć łańcuchów „jeśli”?

266

Zakładając, że mam ten pseudo-kod:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

Funkcje executeStepXpowinny być wykonywane tylko wtedy, gdy poprzednie zakończyły się powodzeniem. W każdym razie executeThisFunctionInAnyCasefunkcja powinna zostać wywołana na końcu. Jestem początkującym programistą, przepraszam za bardzo podstawowe pytanie: czy istnieje sposób (na przykład w C / C ++), aby uniknąć tego długiego ifłańcucha produkującego coś w rodzaju „piramidy kodu” kosztem czytelności kodu ?

Wiem, że gdybyśmy mogli pominąć executeThisFunctionInAnyCasewywołanie funkcji, kod mógłby zostać uproszczony jako:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Ale ograniczeniem jest executeThisFunctionInAnyCasewywołanie funkcji. Czy breakmożna w jakiś sposób wykorzystać to oświadczenie?

ABCplus
źródło
254
@ FrédéricHamidi źle źle źle! Nigdy nie mów, że kierowanie przebiegiem programu z wyjątkami jest dobre! Wyjątki zdecydowanie NIE nadają się do tego celu z zbyt wielu powodów.
Piotr Zierhoffer
26
@Piotr, zostałem zepsuty przez Pythona (który tak naprawdę zachęca). Wiem, że wyjątków nie należy używać do kontroli przepływu w C ++, ale czy to naprawdę kontrola przepływu tutaj? Czy powracającej funkcji nie falsemożna uznać za coś wyjątkowego?
Frédéric Hamidi
13
To zależy od semantyki programu. falsePowrót może być całkiem normalne.
dornhege
28
Cofnęłam twoje pytanie do jego pierwszej wersji. Nie powinieneś radykalnie zmieniać pytania po otrzymaniu określonej liczby pytań (> 0), ponieważ spowoduje to unieważnienie wszystkich odpowiedzi udzielonych do tego momentu i spowoduje zamieszanie. Zamiast tego otwórz nowe pytanie.
But
13
Chciałbym, aby wszyscy „nowi programiści” zadawali takie pytania projektowe.
Jezen Thomas

Odpowiedzi:

486

Możesz użyć &&(logika AND):

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

spełni to oba Twoje wymagania:

  • executeStep<X>()powinien oceniać tylko, jeśli poprzedni się powiódł (nazywa się to oceną zwarcia )
  • executeThisFunctionInAnyCase() zostanie wykonane w każdym przypadku
But
źródło
29
Jest to zarówno semantycznie poprawne (rzeczywiście chcemy, aby WSZYSTKIE warunki były prawdziwe), jak również bardzo dobra technika programowania (ocena zwarcia). Ponadto można tego użyć w każdej złożonej sytuacji, w której tworzenie funkcji zepsułoby kod.
Sanchises
58
@RobAu: Wtedy młodym programistom dobrze będzie w końcu zobaczyć kod oparty na ewaluacji skrótów i może poprosić ich o ponowne zajęcie się tym tematem, co z kolei pomoże im w końcu zostać starszymi programistami. Tak wyraźnie wygrana-wygrana: porządny kod i ktoś nauczył się czegoś z jego czytania.
x4u
24
To powinna być najlepsza odpowiedź
Nazwa wyświetlana
61
@RobAu: To nie jest hack wykorzystujący jakąś niejasną sztuczkę składni, jest bardzo idiomatyczny w prawie każdym języku programowania, do tego stopnia, że ​​jest niewątpliwie standardową praktyką.
BlueRaja - Danny Pflughoeft
38
To rozwiązanie ma zastosowanie tylko wtedy, gdy warunki to tak naprawdę proste wywołania funkcji. W prawdziwym kodzie warunki te mogą mieć długość 2-5 wierszy (i same mogą być kombinacjami wielu innych &&i ||), więc nie ma sposobu, aby połączyć je w jedną ifinstrukcję bez pomniejszenia czytelności. Przeniesienie tych warunków do funkcji zewnętrznych nie zawsze jest łatwe, ponieważ mogą one zależeć od wielu wcześniej obliczonych zmiennych lokalnych, co spowodowałoby straszny bałagan, gdybyś spróbował przekazać każdy z nich jako osobny argument.
hamstergene
358

Wystarczy użyć dodatkowej funkcji, aby uruchomić drugą wersję:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

Użycie głęboko zagnieżdżonego ifs (twój pierwszy wariant) lub chęć wyjścia z „części funkcji” zwykle oznacza, że ​​potrzebujesz dodatkowej funkcji.

ltjax
źródło
51
+1 w końcu ktoś opublikował rozsądną odpowiedź. Moim zdaniem jest to najbardziej poprawny, bezpieczny i czytelny sposób.
Lundin
31
+1 To dobra ilustracja „zasady pojedynczej odpowiedzialności”. Funkcja foodziała poprzez sekwencję powiązanych warunków i działań. Funkcja barjest wyraźnie oddzielona od decyzji. Gdybyśmy zobaczyli szczegóły warunków i działań, może się okazać, że foowciąż robią za dużo, ale na razie jest to dobre rozwiązanie.
GraniteRobert
13
Minusem jest to, że C nie ma funkcji zagnieżdżonych, więc jeśli te 3 kroki potrzebne do użycia zmiennych od barCiebie, będziesz musiał ręcznie przekazać je foojako parametry. Jeśli tak jest i jeśli foozostanie wywołane tylko raz, popełniłbym błąd w używaniu wersji goto, aby uniknąć zdefiniowania dwóch ściśle powiązanych funkcji, które ostatecznie nie byłyby bardzo przydatne.
hugomg
7
Nie jestem pewien składni zwarcia dla C, ale w C # foo () można zapisać jakoif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis
6
@ user1598390 Goto są używane przez cały czas, szczególnie w programowaniu systemów, gdy trzeba odpocząć dużo kodu konfiguracji.
Scotty Bauer
166

gotoW tym przypadku używają programiści starej szkoły C. Jest to jedyne użycie, gotoktóre tak naprawdę zachęca przewodnik po stylach Linuksa, zwane scentralizowanym wyjściem funkcji:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

Niektórzy ludzie pracują przy użyciu goto, zawijając ciało w pętlę i odrywając się od niego, ale skutecznie oba podejścia robią to samo. gotoPodejście jest lepsze, jeśli potrzebujesz jakieś inne porządki tylko jeśli executeStepA()był udany:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

Przy podejściu pętli uzyskałbyś wtedy dwa poziomy pętli.

cmaster - przywróć monikę
źródło
108
+1. Wiele osób widzi gotoi od razu myśli „To okropny kod”, ale ma swoje prawidłowe zastosowania.
Graeme Perrow
46
Z wyjątkiem tego, że jest to właściwie dość niechlujny kod, z punktu widzenia konserwacji. Zwłaszcza, gdy istnieje wiele etykiet, w których kod staje się trudniejszy do odczytania. Istnieją bardziej eleganckie sposoby na osiągnięcie tego: użyj funkcji.
Lundin
29
-1 Widzę gotoi nawet nie muszę myśleć, że to okropny kod. Kiedyś musiałem takie utrzymywać, to bardzo paskudne. OP sugeruje rozsądną alternatywę dla języka C na końcu pytania i uwzględniłem to w mojej odpowiedzi.
Pozdrawiam i hth. - Alf
56
Nie ma nic złego w tym ograniczonym, samodzielnym użyciu goto. Ale uwaga, goto jest lekiem na bramę, a jeśli nie będziesz ostrożny, pewnego dnia zdasz sobie sprawę, że nikt inny nie je spaghetti z nogami, a robisz to od 15 lat, zaczynając od myśli: „Mogę to naprawić inaczej koszmarny błąd z etykietą ... ”
Tim Post
66
Napisałem prawdopodobnie sto tysięcy wierszy niezwykle przejrzystego, łatwego w utrzymaniu kodu w tym stylu. Dwie kluczowe rzeczy do zrozumienia to (1) dyscyplina! ustanowić jasne wytyczne dotyczące układu każdej funkcji i naruszać ją tylko w skrajnych przypadkach oraz (2) zrozumieć, że to, co tutaj robimy, symuluje wyjątki w języku, w którym ich nie ma . throwjest pod wieloma względami gorsze niż gotodlatego, throwże nie jest nawet jasne z lokalnego kontekstu, w którym skończysz! Zastosuj te same zasady projektowania dla przepływu sterowania w stylu goto, jak w przypadku wyjątków.
Eric Lippert
131

Jest to powszechna sytuacja i istnieje wiele typowych sposobów radzenia sobie z nią. Oto moja próba kanonicznej odpowiedzi. Jeśli coś przeoczyłem, proszę o komentarz, a ten post będę aktualizowany na bieżąco.

To jest strzała

To, co omawiasz, nazywa się anty-wzorem strzałki . Nazywa się to strzałką, ponieważ łańcuch zagnieżdżonych ifs tworzy bloki kodu, które rozszerzają się coraz bardziej w prawo, a następnie z powrotem w lewo, tworząc strzałkę wizualną, która „wskazuje” na prawą stronę panelu edytora kodu.

Spłaszcz strzałę strażnikiem

Omówiono tutaj niektóre typowe sposoby unikania Strzałki . Najpopularniejszą metodą jest użycie wzorca zabezpieczającego , w którym kod obsługuje najpierw przepływy wyjątków, a następnie obsługuje przepływ podstawowy, np. Zamiast

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... użyłbyś ...

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

Gdy jest długa seria strażników, spłaszcza to znacznie kod, ponieważ wszyscy strażnicy pojawiają się aż do lewej, a twoje nie są zagnieżdżone. Ponadto wizualnie parujesz warunek logiczny z powiązanym błędem, co znacznie ułatwia stwierdzenie, co się dzieje:

Strzałka:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

Strzec:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

Jest to obiektywnie i ilościowo łatwiejsze do odczytania, ponieważ

  1. Znaki {i} dla danego bloku logicznego są bliżej siebie
  2. Ilość kontekstu mentalnego potrzebnego do zrozumienia określonej linii jest mniejsza
  3. Całość logiki powiązanej z warunkiem if jest bardziej prawdopodobne na jednej stronie
  4. Konieczność przewijania kodera przez stronę / ścieżkę oka jest znacznie zmniejszona

Jak dodać wspólny kod na końcu

Problem z wzorcem ochronnym polega na tym, że polega on na tak zwanym „oportunistycznym powrocie” lub „oportunistycznym wyjściu”. Innymi słowy, łamie to wzór, że każda funkcja powinna mieć dokładnie jeden punkt wyjścia. Jest to problem z dwóch powodów:

  1. Pociera niektóre osoby w niewłaściwy sposób, np. Osoby, które nauczyły się kodować na Pascalu, nauczyły się, że jedna funkcja = jeden punkt wyjścia.
  2. Nie zawiera części kodu, która jest wykonywana przy wyjściu bez względu na to , co jest przedmiotem.

Poniżej przedstawiłem kilka opcji obejścia tego ograniczenia albo za pomocą funkcji językowych, albo całkowicie unikając problemu.

Opcja 1. Nie możesz tego zrobić: użyj finally

Niestety, jako programista c ++, nie możesz tego zrobić. Jest to jednak odpowiedź numer jeden w przypadku języków, które zawierają słowo kluczowe, ponieważ właśnie po to jest.

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

Opcja 2. Uniknij problemu: zrestrukturyzuj swoje funkcje

Możesz uniknąć problemu, dzieląc kod na dwie funkcje. Rozwiązanie to ma tę zaletę, że działa w dowolnym języku, a dodatkowo może zmniejszyć złożoność cykliczną , co jest sprawdzonym sposobem na zmniejszenie liczby defektów i poprawia specyfikę zautomatyzowanych testów jednostkowych.

Oto przykład:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Opcja 3. Sztuczka językowa: użyj fałszywej pętli

Inną częstą sztuczką, którą widzę, jest używanie while (true) i break, jak pokazano w innych odpowiedziach.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

Chociaż jest to mniej „uczciwe” niż używanie goto, jest mniej podatne na pomieszanie podczas refaktoryzacji, ponieważ wyraźnie wyznacza granice zakresu logiki. Naiwny programista, który wycina i wkleja etykiety lub gotooświadczenia, może powodować poważne problemy! (I szczerze mówiąc, ten wzór jest teraz tak powszechny, że myślę, że wyraźnie przekazuje intencję i dlatego nie jest wcale „nieuczciwy”).

Istnieją inne warianty tej opcji. Na przykład można użyć switchzamiast while. breakPrawdopodobnie działałby każdy konstrukt języka ze słowem kluczowym.

Opcja 4. Wykorzystaj cykl życia obiektu

Jedno inne podejście wykorzystuje cykl życia obiektu. Użyj obiektu kontekstowego, aby przenieść parametry (coś, czego podejrzanie brakuje w naszym naiwnym przykładzie) i usuń je, gdy skończysz.

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Uwaga: Upewnij się, że rozumiesz cykl życia obiektu w wybranym języku. Aby to zadziałało, potrzebujesz pewnego rodzaju deterministycznego wyrzucania elementów bezużytecznych, tzn. Musisz wiedzieć, kiedy zostanie wywołany destruktor. W niektórych językach będziesz musiał użyć Disposezamiast destruktora.

Opcja 4.1. Wykorzystaj cykl życia obiektu (wzór opakowania)

Jeśli zamierzasz zastosować podejście obiektowe, równie dobrze możesz to zrobić poprawnie. Ta opcja używa klasy do „zawijania” zasobów, które wymagają czyszczenia, a także innych operacji.

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Ponownie upewnij się, że rozumiesz swój cykl życia obiektu.

Opcja 5. Sztuczka językowa: Użyj oceny zwarcia

Inną techniką jest skorzystanie z oceny zwarcia .

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

To rozwiązanie wykorzystuje sposób działania operatora &&. Kiedy lewa strona && ma wartość false, prawa strona nigdy nie jest oceniana.

Ta sztuczka jest najbardziej przydatna, gdy wymagany jest zwarty kod i gdy kod prawdopodobnie nie wymaga wiele konserwacji, np. Wdrażasz dobrze znany algorytm. Dla bardziej ogólnego kodowania struktura tego kodu jest zbyt krucha; nawet niewielka zmiana w logice może spowodować całkowite przepisanie.

John Wu
źródło
12
Wreszcie? C ++ nie ma klauzuli wreszcie. Obiekt z destruktorem jest używany w sytuacjach, w których uważasz, że potrzebujesz klauzuli wreszcie.
Bill Door
1
Edytowane, aby uwzględnić dwa powyższe komentarze.
John Wu
2
W przypadku trywialnego kodu (np. W moim przykładzie) wzory zagnieżdżone są prawdopodobnie bardziej zrozumiałe. W kodzie świata rzeczywistego (który może obejmować kilka stron) wzór osłony jest obiektywnie łatwiejszy do odczytania, ponieważ będzie wymagał mniej przewijania i śledzenia wzroku, np. Średnia odległość od {do} jest mniejsza.
John Wu
1
Widziałem zagnieżdżony wzór, w którym kod nie był już widoczny na ekranie 1920 x 1080 ... Spróbuj dowiedzieć się, jaki kod obsługi błędów zostanie wykonany, jeśli trzecia akcja się nie powiedzie ... Użyłem do {...} podczas gdy (0) zamiast tego, więc nie potrzebujesz ostatniej przerwy (z drugiej strony, podczas gdy (prawda) {...} pozwala „kontynuować”, aby zacząć wszystko od nowa.
gnasher729
2
Twoja opcja 4 to przeciek pamięci w C ++ (ignorowanie drobnego błędu składniowego). W tym przypadku nie używa się słowa „nowy”, wystarczy powiedzieć „MyContext myContext;”.
Sumudu Fernando
60

Po prostu zrób

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

To takie proste.


Z powodu trzech edycji, z których każda zasadniczo zmieniła pytanie (cztery, jeśli liczy się zmiana z powrotem do wersji 1), dołączam przykład kodu, na który odpowiadam:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();
Pozdrawiam i hth. - Alf
źródło
14
Odpowiedziałem na to (bardziej zwięźle) w odpowiedzi na pierwszą wersję pytania i uzyskało 20 głosów pozytywnych, zanim zostało usunięte przez Billa Jaszczura, po komentarzach i edycji przez Darkness Races in Orbit.
Pozdrawiam i hth. - Alf
1
@ Cheersandhth-Alf: Nie mogę uwierzyć, że został usunięty mod. Po prostu szkoda. (+1)
Paramagnetyczny rogalik
2
Mój +1 do twojej odwagi! (3 razy ma znaczenie: D).
haccks
Jest bardzo ważne, aby programiści początkujący poznawali takie rzeczy, jak realizacja zamówień z wieloma logicznymi „i”, jak to jest różne w różnych językach itp. I ta odpowiedź jest świetnym wskaźnikiem do tego. Ale to po prostu „non-starter” w normalnym programowaniu komercyjnym. To nie jest proste, nie jest możliwe do utrzymania, nie można go modyfikować. Jasne, jeśli piszesz dla siebie szybki „kod kowboja”, zrób to. Ale to po prostu „brak powiązania z codzienną inżynierią oprogramowania, tak jak jest to praktykowane dzisiaj”. BTW przepraszam za absurdalne „zamieszanie podczas edycji”, które musieliście znosić tutaj, @cheers :)
Fattie
1
@JoeBlow: Jestem z Alfem w tej kwestii - uważam, że &&lista jest znacznie jaśniejsza. Zwykle dzielę warunki na osobne linie i dodam // explanationdo nich każdy znak końcowy ... W końcu jest o wiele mniej kodu do oglądania, a kiedy zrozumiesz, jak &&działa, nie ma ciągłego wysiłku umysłowego. Mam wrażenie, że większość profesjonalnych programistów C ++ byłaby z tym zaznajomiona, ale, jak mówisz w różnych branżach / projektach, nacisk i doświadczenie są różne.
Tony Delroy,
35

W C ++ istnieje sposób na odroczenie działań: użycie destruktora obiektu.

Zakładając, że masz dostęp do C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

A następnie za pomocą tego narzędzia:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo
Matthieu M.
źródło
67
To dla mnie kompletne zaciemnienie. Naprawdę nie rozumiem, dlaczego tak wielu programistów C ++ lubi rozwiązywać problemy, podrzucając możliwie jak najwięcej funkcji językowych, aż do momentu, gdy wszyscy zapomnieli o tym, jaki problem faktycznie rozwiązałeś: już ich to nie obchodzi, ponieważ są teraz bardzo zainteresowany korzystaniem z tych wszystkich egzotycznych funkcji językowych. Od tego momentu możesz być zajęty przez wiele dni i tygodni, pisząc meta kod, a następnie utrzymując meta kod, a następnie napisz meta kod do obsługi meta kodu.
Lundin
24
@Lundin: Cóż, nie rozumiem, jak można zadowolić się kruchym kodem, który zepsuje się, gdy tylko zostanie wprowadzone wczesne kontynuowanie / przerwa / powrót lub zostanie zgłoszony wyjątek. Z drugiej strony, to rozwiązanie jest odporne na przyszłe utrzymanie i opiera się tylko na tym, że podczas rozwijania działają niszczarki, co jest jedną z najważniejszych cech C ++. Kwalifikowanie tego egzotycznego, chociaż jest to podstawowa zasada, która zasila wszystkie standardowe pojemniki, jest co najmniej zabawne.
Matthieu M.
7
@Lundin: Zaletą kodu Matthieu jest to, że executeThisFunctionInAnyCase();zostanie wykonany, nawet jeśli foo();zgłosi wyjątek. Podczas pisania kodu bezpiecznego dla wyjątków dobrą praktyką jest umieszczenie wszystkich takich funkcji czyszczenia w destruktorze.
Brian
6
@Brian Więc nie wrzucaj żadnych wyjątków foo(). A jeśli to zrobisz, złap go. Problem rozwiązany. Napraw błędy, naprawiając je, a nie pisząc obejścia.
Lundin
18
@Lundin: Deferklasa to mały fragment kodu wielokrotnego użytku, który pozwala na wykonanie dowolnego czyszczenia na końcu bloku, w wyjątkowo bezpieczny sposób. jest bardziej znany jako osłona zakresu . tak, każde użycie osłony zakresu może być wyrażone na inne, bardziej ręczne sposoby, tak jak każda forpętla może być wyrażona jako blok i whilepętla, które z kolei mogą być wyrażone za pomocą ifi goto, które można wyrazić w języku asemblera, jeśli chcesz, lub dla tych, którzy są prawdziwymi mistrzami, poprzez zmianę bitów w pamięci za pomocą promieni kosmicznych kierowanych przez efekt motyla specjalnych krótkich pomruków i śpiewów. ale dlaczego to zrobić.
Pozdrawiam i hth. - Alf
34

Jest fajna technika, która nie wymaga dodatkowej funkcji otoki z instrukcjami return (metoda zalecana przez Itjax). Wykorzystuje while(0)pseudo-pętlę do. W while (0)zapewnia, że nie jest to faktycznie pętli ale wykonywane tylko raz. Jednak składnia pętli pozwala na użycie instrukcji break.

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}
Debasis
źródło
11
Używanie funkcji z wieloma zwrotami jest moim zdaniem podobne, ale bardziej czytelne.
Lundin
4
Tak, zdecydowanie jest bardziej czytelny ... jednak z punktu widzenia wydajności można uniknąć narzutu wywołania funkcji dodatkowej (przekazywanie i powrót parametrów) dzięki konstrukcji do {} while (0).
Debasis
5
Nadal możesz wykonać tę funkcję inline. W każdym razie jest to miła technika do poznania, ponieważ pomaga ona bardziej niż w tym problemie.
Ruslan
2
@Lundin Musisz wziąć pod uwagę lokalizację kodu, rozkładanie kodu na zbyt wiele miejsc ma swoje problemy.
API-Beast
3
Z mojego doświadczenia wynika, że ​​jest to bardzo niezwykły idiom. Zajmie mi trochę czasu, aby dowiedzieć się, co się do cholery dzieje, a to zły znak, gdy przeglądam kod. Biorąc pod uwagę, że wydaje się, że nie ma przewagi nad innymi, bardziej powszechnymi, a zatem bardziej czytelnymi podejściami, nie mogłem się na to zgodzić.
Cody Gray
19

Możesz także to zrobić:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

W ten sposób masz minimalny rozmiar liniowego wzrostu, +1 linię na połączenie i jest łatwy do utrzymania.


EDYCJA : (Dzięki @Unda) Nie jest wielkim fanem, ponieważ tracisz widoczność IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

źródło
1
Chodziło o przypadkowe wywołania funkcji w push_back (), ale i tak to naprawiłeś :)
Quentin
4
Chciałbym wiedzieć, dlaczego otrzymało to głosowanie negatywne. Zakładając, że kroki wykonania są rzeczywiście symetryczne, jak się wydaje, to są czyste i łatwe do utrzymania.
ClickRick
1
Chociaż może to wyglądać na czystsze. Może być trudniejszy do zrozumienia dla ludzi i zdecydowanie trudniejszy do zrozumienia dla kompilatora!
Roy T.
1
Lepsze traktowanie funkcji jako danych jest często dobrym pomysłem - gdy to zrobisz, zauważysz także kolejne refaktoryzacje. Jeszcze lepsze jest to, że każdy obsługiwany element jest odniesieniem do obiektu, a nie tylko odniesieniem do funkcji - da to jeszcze więcej możliwości ulepszenia kodu w dalszej linii.
Bill K
3
Nieco zbyt skomplikowane w przypadku trywialnego przypadku, ale ta technika zdecydowanie ma fajną funkcję, której inni nie mają: Możesz zmienić kolejność wykonywania i [liczbę] funkcji wywoływanych w czasie wykonywania, i to jest fajne :)
Xocoatzin
18

Czy to zadziała? Myślę, że jest to równoważne z twoim kodem.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();
sampathsris
źródło
3
Zwykle wywołuję zmienną, okgdy używam tej samej zmiennej jak ta.
Macke
1
Byłbym bardzo zainteresowany, aby dowiedzieć się, dlaczego opinie negatywne. Co tu idzie nie tak?
ABCplus
1
porównaj swoją odpowiedź z podejściem zwarciowym dla złożoności cyklicznej.
AlphaGoku,
14

Zakładając, że pożądany kod jest taki, jaki obecnie go widzę:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

Powiedziałbym, że prawidłowe podejście, ponieważ jest najłatwiejsze do odczytania i najłatwiejsze w utrzymaniu, miałoby mniej poziomów wcięć, co jest (obecnie) podanym celem pytania.

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

W ten sposób unika jakichkolwiek potrzebę gotos, wyjątki, atrapy whilepętlach lub innych trudnych konstrukcjach i po prostu wsiada z prostego zadania pod ręką.

ClickRick
źródło
Podczas korzystania z pętli zwykle dopuszczalne jest używanie returni breakwyskakiwanie z pętli bez konieczności wprowadzania dodatkowych zmiennych „flag”. W takim przypadku użycie goto byłoby podobnie niewinne - pamiętaj, że handlujesz dodatkową złożonością goto za dodatkową zmienną złożoność zmiennych.
hugomg
2
@hugomg Zmienne były w pierwotnym pytaniu. Tutaj nie ma dodatkowej złożoności. Przyjęto założenia dotyczące pytania (np. Że zmienne są potrzebne w zredagowanym kodzie), więc zostały zachowane. Jeśli nie są potrzebne, kod można uprościć, ale biorąc pod uwagę niepełny charakter pytania, nie można przyjąć żadnego innego założenia.
ClickRick
Bardzo przydatne podejście, szczególnie do użytku przez samozwańczego newbie, zapewnia czystsze rozwiązanie bez wad. Zauważam, że również nie polega na tym, stepsże ma taką samą sygnaturę, a nawet jest funkcją, a nie blokiem. Widzę, że jest to stosowane jako refaktor pierwszego przejścia, nawet gdy bardziej wyrafinowane podejście jest ważne.
Keith
12

Czy można w jakiś sposób użyć instrukcji break?

Może nie jest to najlepsze rozwiązanie, ale możesz umieścić swoje instrukcje w do .. while (0)pętli i użyć breakinstrukcji zamiast return.

ouah
źródło
2
To nie ja głosowałem za tym, ale byłoby to nadużycie zapętlonego konstruktu dla czegoś, w którym efekt jest tym, czego obecnie pragniemy, ale nieuchronnie spowodowałby ból. Prawdopodobnie dla następnego programisty, który będzie musiał go utrzymać za 2 lata po przejściu do innego projektu.
ClickRick
3
Używanie @ClickRick do .. while (0)do definicji makr również nadużywa pętli, ale jest uważane za OK.
ouah
1
Być może, ale istnieją czystsze sposoby na osiągnięcie tego.
ClickRick
4
@ClickRick jedynym celem mojej odpowiedzi jest udzielenie odpowiedzi. Można złamać instrukcję, która może być użyta w jakiś sposób, a odpowiedź brzmi tak, pierwsze słowa w mojej odpowiedzi sugerują, że nie jest to rozwiązanie, którego należy użyć.
ouah
2
Ta odpowiedź powinna być tylko komentarzem
msmucker0527
12

Możesz umieścić wszystkie ifwarunki, sformatowane tak, jak chcesz, w ich własnych funkcjach, a po powrocie wykonać executeThisFunctionInAnyCase()funkcję.

Z podstawowego przykładu w PO, testowanie i wykonanie warunków można oddzielić jako takie;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

A następnie nazywany jako taki;

InitialSteps();
executeThisFunctionInAnyCase();

Jeśli dostępne są lambdy C ++ 11 (w OP nie było tagu C ++ 11, ale nadal mogą być opcją), możemy zrezygnować z oddzielnej funkcji i zawinąć ją w lambda.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();
Niall
źródło
10

Jeśli nie lubisz gotoi nie do { } while (0);lubisz pętli i lubisz używać C ++, możesz również użyć tymczasowej lambda, aby uzyskać ten sam efekt.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();
Alex
źródło
1
if nie lubisz goto && nie lubisz robić {}, podczas gdy (0) && lubisz C ++ ... Przepraszam, nie mogłem się oprzeć, ale ten ostatni warunek nie udaje się, ponieważ pytanie jest oznaczone jako c oraz c ++
ClickRick
@ClickRick zawsze trudno jest zadowolić wszystkich. W moim zdaniem nie ma czegoś takiego jak C / C ++ zwykle kod na jeden z tych, a korzystanie z drugą jest mile widziana.
Alex
9

Łańcuchy IF / ELSE w twoim kodzie nie są problemem językowym, ale projektem twojego programu. Jeśli jesteś w stanie zmienić lub ponownie napisać swój program, chciałbym zasugerować, aby zajrzeć do Wzorów projektowych ( http://sourcemaking.com/design_patterns ), aby znaleźć lepsze rozwiązanie.

Zwykle, gdy widzisz w kodzie wiele IF i innych, jest to okazja do zaimplementowania wzorca projektowania strategii ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) lub może kombinacji innych wzorów.

Jestem pewien, że istnieją alternatywy, aby napisać długą listę if / else, ale wątpię, aby coś zmieniły, z wyjątkiem tego, że łańcuch będzie wyglądał ładnie dla ciebie (Jednak piękno jest w oku patrzącego nadal dotyczy kodu też:-) ) . Powinieneś martwić się o takie rzeczy (czy w ciągu 6 miesięcy, kiedy mam nowy stan i nie pamiętam nic o tym kodzie, czy będę w stanie łatwo go dodać? Albo co, jeśli łańcuch się zmieni, jak szybko i bezbłędnie czy będę go wdrażać)

Roman Mik
źródło
9

Po prostu to zrób ...

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 razy na 100, to jedyny sposób, aby to zrobić.

Nigdy, nigdy, nigdy nie próbuj robić czegoś „podstępnego” w kodzie komputerowym.


Nawiasem mówiąc, jestem prawie pewien, że poniższe rozwiązanie jest właściwym rozwiązaniem, o którym myślisz ...

Instrukcja kontynuacji ma kluczowe znaczenie w programowaniu algorytmicznym. (O ile instrukcja goto ma kluczowe znaczenie w programowaniu algorytmicznym).

W wielu językach programowania możesz to zrobić:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(Uwaga: przede wszystkim: nagie bloki jak ten przykład są kluczową i ważną częścią pisania pięknego kodu, szczególnie jeśli masz do czynienia z programowaniem „algorytmicznym”).

Znów dokładnie to miałeś w głowie, prawda? I to jest piękny sposób na napisanie tego, więc masz dobry instynkt.

Jednak tragicznie w obecnej wersji celu-c (na bok - nie wiem o Swift, przepraszam) istnieje pewna sensowna funkcja, która sprawdza, czy blok otaczający jest pętlą.

wprowadź opis zdjęcia tutaj

Oto jak obejść ten problem ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

Więc nie zapominaj o tym ...

do {} while (false);

oznacza po prostu „zrób ten blok raz”.

tzn. nie ma żadnej różnicy między pisaniem do{}while(false);a zwykłym pisaniem {}.

To działa teraz idealnie, jak chciałeś ... oto wynik ...

wprowadź opis zdjęcia tutaj

Możliwe więc, że tak właśnie widzisz algorytm w swojej głowie. Zawsze powinieneś próbować pisać to, co masz w głowie. (Zwłaszcza jeśli nie jesteś trzeźwy, bo wtedy pojawia się śliczna! :))

W projektach „algorytmicznych”, w których często się to zdarza, w celu-c zawsze mamy makro takie jak ...

#define RUNONCE while(false)

... więc możesz to zrobić ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

Istnieją dwa punkty:

a, chociaż to głupie, że cel-c sprawdza rodzaj bloku, w którym znajduje się instrukcja kontynuacji, trudno jest z tym „walczyć”. To trudna decyzja.

b, jest pytanie, czy powinieneś wciąć w tym przykładzie blok? Nie mogę zasnąć z powodu takich pytań, więc nie mogę doradzić.

Mam nadzieję, że to pomoże.

Fattie
źródło
Czy umieściłeś nagrodę, aby uzyskać więcej przedstawicieli? :)
TMS
Zamiast umieszczać wszystkie te komentarze w if, możesz również użyć bardziej opisowych nazw funkcji i umieścić komentarze w funkcjach.
Thomas Ahle,
Fuj Przyjmę rozwiązanie 1-liniowe, które czyta zwięźle z oceną zwarcia (które było w językach od ponad 20 lat i jest dobrze znane) w tym bałaganie każdego dnia. Myślę, że oboje możemy się zgodzić, że jesteśmy szczęśliwi, że nie współpracujemy ze sobą.
M2tM
8

Niech twoje funkcje wykonywania zgłoszą wyjątek, jeśli zawiodą, zamiast zwracać wartość false. Wówczas Twój kod telefoniczny może wyglądać następująco:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

Oczywiście zakładam, że w twoim oryginalnym przykładzie krok wykonania zwróci fałsz tylko w przypadku wystąpienia błędu w kroku?

Rik
źródło
3
stosowanie wyjątku do kontroli przepływu jest często uważane za złą praktykę i śmierdzący kod
user902383
8

Wiele dobrych odpowiedzi już, ale wydaje się, że większość z nich kompromituje niektóre (co prawda bardzo mało) elastyczności. Powszechnym podejściem, które nie wymaga tego kompromisu, jest dodawanie zmiennej status / keep-go . Cena jest oczywiście jedną dodatkową wartością do śledzenia:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;
blgt
źródło
Dlaczego ok &= conditionX;nie po prostu ok = conditionX;?
ABCplus
@ user3253359 W wielu przypadkach tak, możesz to zrobić. To jest demo koncepcyjne; w działającym kodzie staramy się go maksymalnie uprościć
blgt
+1 Jedna z niewielu czystych i łatwych do utrzymania odpowiedzi, które działają w c , jak określono w pytaniu.
ClickRick
6

W C ++ (pytanie jest oznaczone zarówno C, jak i C ++), jeśli nie możesz zmienić funkcji w celu użycia wyjątków, nadal możesz użyć mechanizmu wyjątku, jeśli napiszesz małą funkcję pomocniczą, taką jak

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Wówczas Twój kod mógłby brzmieć następująco:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Jeśli lubisz fantazyjną składnię, możesz zamiast tego sprawić, by działała za pomocą jawnej obsady:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Następnie możesz napisać kod jako

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();
celtschk
źródło
Refaktoryzacja kontroli wartości w wyjątki niekoniecznie jest dobrą drogą, istnieją znaczne wyjątki związane z odwijaniem.
John Wu
4
-1 Używanie wyjątków dla normalnego przepływu w C ++ w ten sposób jest słabą praktyką programowania. W C ++ wyjątki powinny być zastrzeżone na wyjątkowe okoliczności.
Jack Aidley,
1
Z tekstu pytania (moje podkreślenie): „Funkcje executeStepX powinny być wykonywane tylko wtedy, gdy poprzednie zakończyły się powodzeniem. ” Innymi słowy, wartość zwracana jest używana do wskazania niepowodzenia. Oznacza to obsługę błędów (i można mieć nadzieję, że awarie wyjątkowe). Obsługa błędów jest dokładnie tym , dla czego wymyślono wyjątki.
celtschk
1
Nie. Po pierwsze stworzono wyjątki, aby umożliwić propagację błędów , a nie obsługę błędów ; po drugie: „Funkcje executeStepX powinny być wykonywane tylko wtedy, gdy poprzednie zakończyły się powodzeniem”. nie oznacza, że ​​wartość logiczna false zwrócona przez poprzednią funkcję wskazuje na oczywiście wyjątkową / błędną wielkość liter. Twoje oświadczenie nie jest zatem sekwencyjne . Obsługa błędów i odkażanie przepływu mogą być realizowane na wiele różnych sposobów, wyjątki są narzędziem umożliwiającym propagację błędów i obsługę błędów poza miejscem , i wyróżniają się tym.
6

Jeśli Twój kod jest tak prosty jak Twój przykład, a Twój język obsługuje oceny zwarć, możesz spróbować:

StepA() && StepB() && StepC() && StepD();
DoAlways();

Jeśli przekazujesz argumenty do swoich funkcji i odzyskujesz inne wyniki, aby Twój kod nie mógł zostać zapisany w poprzedni sposób, wiele innych odpowiedzi lepiej pasowałoby do problemu.

Noctis Skytower
źródło
W rzeczywistości zredagowałem moje pytanie, aby lepiej wyjaśnić temat, ale zostało odrzucone, aby nie unieważniać większości odpowiedzi. : \
ABCplus
Jestem nowym użytkownikiem w SO i początkującym programistą. 2 pytania: czy istnieje ryzyko, że inne pytanie, takie jak to, które powiedziałeś, zostanie oznaczone jako zduplikowane z powodu TEGO pytania? Kolejna kwestia to: w jaki sposób początkujący użytkownik / programista SO może wybrać najlepszą odpowiedź spośród wszystkich (chyba prawie dobry ...)?
ABCplus
6

W przypadku C ++ 11 i nowszych fajnym podejściem może być wdrożenie systemu wyjścia zakresu podobnego do mechanizmu zasięgu (wyjścia) D.

Jednym z możliwych sposobów jego implementacji jest użycie lambda C ++ 11 i niektórych makr pomocniczych:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

Pozwoli ci to wrócić wcześniej z funkcji i upewnić się, że kod czyszczenia, który zdefiniujesz, jest zawsze wykonywany po wyjściu z zakresu:

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Makra to tak naprawdę dekoracja. MakeScopeExit()może być używany bezpośrednio.

glampert
źródło
Aby makra działały, nie trzeba. I [=]zwykle jest źle w przypadku lambda z lunetą.
Yakk - Adam Nevraumont
Tak, makra służą tylko do dekoracji i można je wyrzucić. Ale czy nie powiedziałbyś, że wychwytywanie według wartości jest najbezpieczniejszym „ogólnym” podejściem?
glampert
1
nie: jeśli twoja lambda nie przetrwa poza bieżącym zakresem, w którym tworzona jest lambda, użyj [&]: jest bezpieczna i minimalnie zaskakująca. Przechwytywanie według wartości tylko wtedy, gdy lambda (lub kopie) może przetrwać dłużej niż zakres w punkcie deklaracji ...
Yakk - Adam Nevraumont
Tak, to ma sens. Zmienię to. Dzięki!
glampert
6

Dlaczego nikt nie podaje najprostszego rozwiązania? :RE

Jeśli wszystkie funkcje mają ten sam podpis, możesz to zrobić w ten sposób (dla języka C):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Aby uzyskać czyste rozwiązanie C ++, należy utworzyć klasę interfejsu, która zawiera metodę wykonania i zawija kroki w obiektach.
Następnie powyższe rozwiązanie będzie wyglądać następująco:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();
Benkoin
źródło
5

Zakładając, że nie potrzebujesz indywidualnych zmiennych warunkowych, odwrócenie testów i użycie opcji else-falthrough jako ścieżki „ok” pozwoliłoby uzyskać bardziej pionowy zestaw instrukcji if / else:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

Pominięcie nieudanej zmiennej powoduje, że kod jest zbyt niejasny dla IMO.

Zadeklarowanie zmiennych wewnątrz jest w porządku, nie martw się o = vs ==.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Jest to niejasne, ale kompaktowe:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();
Macke
źródło
5

Wygląda to na automat stanowy, co jest przydatne, ponieważ można go łatwo zaimplementować za pomocą wzorca stanu .

W Javie wyglądałoby to mniej więcej tak:

interface StepState{
public StepState performStep();
}

Implementacja będzie działać w następujący sposób:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

I tak dalej. Następnie możesz zastąpić duży warunek jeśli:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();
CarrKnight
źródło
5

Jak wspomniał Rommik, możesz zastosować do tego wzór projektowy, ale wolałbym użyć wzoru Dekorator niż Strategii, ponieważ chcesz łączyć połączenia. Jeśli kod jest prosty, wybrałbym jedną z ładnie ustrukturyzowanych odpowiedzi, aby zapobiec zagnieżdżeniu. Jednak jeśli jest złożony lub wymaga dynamicznego łączenia, to wzór Dekoratora jest dobrym wyborem. Oto schemat klas yUML :

Schemat klas yUML

Oto przykładowy program LinqPad C #:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

Najlepsza książka do przeczytania na temat wzorów projektowych, IMHO, to Head First Design Patterns .

Co by było fajne
źródło
Jaka jest z tego korzyść w porównaniu z odpowiedzią Jefffreya?
Dason,
znacznie bardziej odporna na zmiany, gdy zmieniają się wymagania, podejście to jest łatwiejsze do zarządzania bez dużej wiedzy na temat domen. Zwłaszcza gdy weźmiesz pod uwagę głębokość i długość niektórych odcinków zagnieżdżonych. Wszystko może stać się bardzo delikatne, a zatem wysokie ryzyko pracy. Nie zrozum mnie źle, niektóre scenariusze optymalizacji mogą spowodować, że wyrwiesz to i wrócisz do ifs, ale 99% czasu jest w porządku. Ale chodzi o to, że kiedy osiągniesz ten poziom, nie zależy ci na łatwości utrzymania, potrzebujesz wydajności.
John Nicholas
4

Kilka odpowiedzi wskazywało na wzór, który widziałem i stosowałem wiele razy, szczególnie w programowaniu sieciowym. W stosach sieciowych często występuje długa sekwencja żądań, z których każde może zakończyć się niepowodzeniem i zatrzymać proces.

Powszechnym wzorem było użycie do { } while (false);

Kiedyś makro dla while(false)aby do { } once;Wspólna wzór był następujący:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

Ten wzorzec był stosunkowo łatwy do odczytania i pozwolił na użycie obiektów, które odpowiednio zniszczyłyby, a także uniknęły wielokrotnych zwrotów, co nieco ułatwiło wchodzenie i debugowanie.

Bill Door
źródło
4

Aby ulepszyć odpowiedź Mathieu na C ++ 11 i uniknąć kosztów środowiska wykonawczego związanych z użyciem std::function, sugerowałbym użycie następujących

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Ta prosta klasa szablonów zaakceptuje dowolny funktor, który można wywołać bez żadnych parametrów, i robi to bez żadnych dynamicznych przydziałów pamięci, a zatem lepiej odpowiada celowi abstrakcji C ++ bez niepotrzebnego narzutu. Dodatkowy szablon funkcji ma na celu uproszczenie użycia przez odjęcie parametrów szablonu (które nie jest dostępne dla parametrów szablonu klasy)

Przykład użycia:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Podobnie jak odpowiedź Mathieu, to rozwiązanie jest w pełni bezpieczne i executeThisFunctionInAnyCasebędzie wywoływane we wszystkich przypadkach. Gdyby executeThisFunctionInAnyCasesię rzuciło, niszczyciele są domyślnie oznaczone noexcepti dlatego wywołane std::terminatezostanie polecenie zamiast wywoływać wyjątek podczas odwijania stosu.

Joe
źródło
+1 Szukałem tej odpowiedzi, więc nie musiałbym jej publikować. Powinieneś przejść do przodu functorw deferredkonstruktorze, bez potrzeby wymuszania move.
Yakk - Adam Nevraumont
@Yakk zmienił konstruktora na konstruktora przekazującego
Joe
3

Wygląda na to, że chcesz wykonywać wszystkie połączenia z jednego bloku. Jak inni to zaproponowali, powinieneś użyć whilepętli i wyjść używając breaklub nowej funkcji, którą możesz zostawić return(może być czystsza).

Osobiście wypędzam goto, nawet za wyjście z funkcji. Są trudniejsze do wykrycia podczas debugowania.

Elegancką alternatywą, która powinna działać w twoim przepływie pracy, jest zbudowanie tablicy funkcji i iteracja na niej.

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();
AxFab
źródło
Na szczęście debuger rozpoznaje je dla Ciebie. Jeśli debugujesz, a nie przeskakujesz kodu, robisz to źle.
Cody Gray
Nie rozumiem, co masz na myśli, ani dlaczego nie mogłem skorzystać z jednego kroku?
AxFab
3

Ponieważ masz również [... blok kodu ...] między wykonaniami, myślę, że masz alokację pamięci lub inicjalizację obiektu. W ten sposób musisz zadbać o wyczyszczenie wszystkiego, co już zainicjalizowałeś przy wyjściu, a także wyczyść go, jeśli napotkasz problem i którakolwiek z funkcji zwróci false.

W tym przypadku najlepsze, co miałem z mojego doświadczenia (kiedy pracowałem z CryptoAPI), to tworzenie małych klas, w konstruktorze inicjujesz swoje dane, w destruktorze - je inicjujesz. Każda następna klasa funkcji musi być potomkiem poprzedniej klasy funkcji. Jeśli coś poszło nie tak - wyrzuć wyjątek.

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

A następnie w kodzie wystarczy wywołać:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Myślę, że to najlepsze rozwiązanie, jeśli każde wywołanie ConditionX coś zainicjuje, przydzieli pamięć i tym podobne. Najlepiej mieć pewność, że wszystko zostanie wyczyszczone.

Arkady
źródło
3

ciekawym sposobem jest praca z wyjątkami.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

Jeśli napiszesz taki kod, idziesz w złym kierunku. Nie uważam tego za „problem” za taki kod, ale za taką bałaganiarską „architekturę”.

Wskazówka: przedyskutuj te sprawy z doświadczonym programistą, któremu ufasz ;-)

Karsten
źródło
Myślę, że ten pomysł nie może zastąpić każdego łańcucha if. W każdym razie w wielu przypadkach jest to bardzo dobre podejście!
WoIIe,
3

Inne podejście - do - whilenawet jeśli zostało wspomniane wcześniej, nie było takiego przykładu, który pokazałby, jak to wygląda:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(Cóż, istnieje już odpowiedź z whilepętlą, ale do - whilepętla nie sprawdza nadmiarowo prawdy (na początku), ale zamiast tego na końcu xD (można to jednak pominąć)).

Zaffy
źródło
Hej Zaffy - ta odpowiedź zawiera obszerne wyjaśnienie podejścia do {} while (false). stackoverflow.com/a/24588605/294884 Wspomniały o tym także dwie inne odpowiedzi.
Fattie
To fascynujące pytanie, czy bardziej elegancko jest użyć KONTYNUUJ, czy PRZERWA w tej sytuacji!
Fattie
Hej @JoeBlow Widziałem wszystkie odpowiedzi ... chciałem tylko pokazać, zamiast o tym mówić :)
Zaffy
Moja pierwsza odpowiedź tutaj brzmiała: „Nikt nie wspominał o TYM ...” i natychmiast ktoś uprzejmie zauważył, że była to druga najwyższa odpowiedź :)
Fattie
@JoeBlow Eh, masz rację. Spróbuję to naprawić. Czuję się jak ... xD W każdym razie dziękuję, następnym razem zapłacę trochę więcej :)
Zaffy