podczas gdy (prawda) i łamanie pętli - anty-wzór?

32

Rozważ następujący kod:

public void doSomething(int input)
{
   while(true)
   {
      TransformInSomeWay(input);

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Załóżmy, że proces ten obejmuje skończoną, ale zależną od danych wejściowych liczbę kroków; pętla została zaprojektowana tak, aby zakończyć się sama w wyniku działania algorytmu i nie jest zaprojektowana do działania w nieskończoność (dopóki nie zostanie anulowana przez zdarzenie zewnętrzne). Ponieważ test sprawdzający, czy pętla powinna się zakończyć, znajduje się w środku logicznego zestawu kroków, sama pętla while obecnie nie sprawdza niczego znaczącego; zamiast tego sprawdzanie odbywa się w „właściwym” miejscu algorytmu koncepcyjnego.

Powiedziano mi, że jest to zły kod, ponieważ jest bardziej podatny na błędy ze względu na to, że warunek końcowy nie jest sprawdzany przez strukturę pętli. Trudniej jest wymyślić, jak wyjść z pętli, i może zapraszać błędy, ponieważ warunek przerwania może zostać ominięty lub pominięty przypadkowo, biorąc pod uwagę przyszłe zmiany.

Teraz kod może mieć następującą strukturę:

public void doSomething(int input)
{
   TransformInSomeWay(input);

   while(!ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
      TransformInSomeWay(input);
   }
}

Powoduje to jednak duplikowanie wywołania metody w kodzie, co narusza DRY; gdyby TransformInSomeWayzostały później zastąpione inną metodą, oba wywołania musiałyby zostać znalezione i zmienione (a fakt, że są dwa, może być mniej oczywisty w bardziej złożonym fragmencie kodu).

Możesz również napisać to tak:

public void doSomething(int input)
{
   var complete = false;
   while(!complete)
   {
      TransformInSomeWay(input);

      complete = ProcessingComplete(input);

      if(!complete) 
      {
         DoSomethingElseTo(input);          
      }
   }
}

... ale masz teraz zmienną, której jedynym celem jest przeniesienie sprawdzania warunków do struktury pętli, a także musi być sprawdzana wiele razy, aby zapewnić takie samo zachowanie jak oryginalna logika.

Ze swojej strony mówię, że biorąc pod uwagę algorytm, jaki ten kod implementuje w świecie rzeczywistym, oryginalny kod jest najbardziej czytelny. Gdybyś sam to przeszedł, tak byś o tym pomyślał, dlatego byłoby to intuicyjne dla osób zaznajomionych z algorytmem.

Więc co jest „lepsze”? czy lepiej jest powierzyć odpowiedzialność za sprawdzanie stanu pętli while, konstruując logikę wokół pętli? Czy może lepiej jest ustrukturyzować logikę w „naturalny” sposób, na co wskazują wymagania lub opis koncepcyjny algorytmu, nawet jeśli może to oznaczać ominięcie wbudowanych możliwości pętli?

KeithS
źródło
12
Być może, ale pomimo próbek kodu, zadawane pytanie jest bardziej koncepcyjne przez IMO, co jest bardziej zgodne z tym obszarem SE. O tym, co jest wzorem lub anty-wzorem, często się tutaj dyskutuje i to jest prawdziwe pytanie; czy tworzenie pętli tak, by działała nieskończenie, a następnie zerwanie z nią jest czymś złym?
KeithS
3
do ... untilKonstrukt jest również przydatna dla tych rodzajów pętli, ponieważ nie muszą być „zalane”.
Blrfl
2
Półtorej skrzyni wciąż nie ma żadnej naprawdę dobrej odpowiedzi.
Loren Pechtel
1
„Jednak to powiela wywołanie metody w kodzie, co narusza DRY” Nie zgadzam się z tym stwierdzeniem i wydaje się, że to nieporozumienie z zasadą.
Andy
1
Oprócz tego, że wolę styl C dla (;;), co wyraźnie oznacza „Mam pętlę i nie sprawdzam instrukcji pętli”, twoje pierwsze podejście jest absolutnie właściwe. Masz pętlę. Zanim zdecydujesz, czy wyjść, musisz zrobić jeszcze trochę pracy. Następnie wyjdziesz, jeśli zostaną spełnione pewne warunki. Następnie wykonujesz właściwą pracę i zaczynasz wszystko od nowa. To absolutnie w porządku, łatwe do zrozumienia, logiczne, niepotrzebne i łatwe do rozwiązania. Drugi wariant jest po prostu okropny, podczas gdy trzeci jest po prostu bezcelowy; zmieniając się w okropne, jeśli reszta pętli, która przechodzi do if, jest bardzo długa.
gnasher729,

Odpowiedzi:

14

Trzymaj się pierwszego rozwiązania. Pętle mogą być bardzo zaangażowane, a jedna lub więcej czystych przerw może być dla ciebie o wiele łatwiejsze, każdy inny, kto patrzy na twój kod, optymalizator i wydajność programu, niż opracowywać instrukcje if i dodawać zmienne. Moje zwykłe podejście polega na skonfigurowaniu pętli z czymś w rodzaju „while (true)” i uzyskaniu najlepszego kodowania, jakie mogę. Często mogę, a następnie zastępuję przerwę (s) standardową kontrolą pętli; w końcu większość pętli jest dość prosta. Warunek końcowy powinien zostać sprawdzony przez strukturę pętli z podanych powodów, ale nie kosztem dodawania całych nowych zmiennych, dodawania instrukcji if i powtarzania kodu, z których wszystkie są jeszcze bardziej podatne na błędy.

RalphChapin
źródło
„instrukcje if i powtarzający się kod, z których wszystkie są jeszcze bardziej podatne na błędy”. - tak, próbując ludzi, których nazywam „przyszłymi błędami”
Pat
13

Jest to znaczący problem, jeśli ciało pętli nie jest trywialne. Jeśli ciało jest tak małe, wówczas analizowanie go w ten sposób jest trywialne i wcale nie stanowi problemu.

DeadMG
źródło
7

Mam problem ze znalezieniem innych rozwiązań lepszych niż rozwiązanie z przerwą. Cóż, wolę sposób Ada, który pozwala na to

loop
  TransformInSomeWay(input);
exit when ProcessingComplete(input);
  DoSomethingElseTo(input);
end loop;

ale rozwiązanie break jest najczystszym renderingiem.

Moim POV jest to, że gdy brakuje struktury kontrolnej, najczystszym rozwiązaniem jest emulacja jej za pomocą innych struktur kontrolnych, nie wykorzystujących przepływu danych. (I podobnie, gdy mam problem z przepływem danych, wolę rozwiązania o czystym przepływie danych niż te obejmujące struktury kontrolne *).

Trudniej jest dowiedzieć się, jak wyjść z pętli,

Nie pomyl się, dyskusja nie odbyłaby się, gdyby trudność nie była w większości taka sama: warunek jest taki sam i występuje w tym samym miejscu.

Które z

while (true) {
   XXXX
if (YYY) break;
   ZZZZ;
}

i

while (TTTT) {
    XXXX
    if (YYYY) {
        ZZZZ
    }
}

lepiej renderować zamierzoną strukturę? Głosuję za pierwszym, nie musisz sprawdzać, czy warunki się zgadzają. (Ale zobacz moje wcięcie).

AProgrammer
źródło
1
Och, spójrz, język, który bezpośrednio obsługuje pętle mid-exit! :)
mjfgates
Podoba mi się twój punkt widzenia na temat emulacji brakujących struktur kontrolnych za pomocą struktur kontrolnych. To prawdopodobnie dobra odpowiedź również na pytania związane z GOTO. Należy używać struktur kontrolnych języka, gdy tylko spełniają one wymagania semantyczne, ale jeśli algorytm wymaga czegoś innego, należy użyć struktury kontrolnej wymaganej przez algorytm.
supercat
3

while (true) i break to powszechny idiom. To kanoniczny sposób implementacji pętli mid-exit w językach podobnych do C. Dodanie zmiennej do przechowywania warunku wyjścia i if () wokół drugiej połowy pętli jest rozsądną alternatywą. Niektóre sklepy mogą oficjalnie ustandaryzować się w jednym lub drugim, ale żaden z nich nie jest lepszy; są równoważne.

Dobry programista pracujący w tych językach powinien na pierwszy rzut oka wiedzieć, co się dzieje. To może być dobre pytanie do rozmowy kwalifikacyjnej; podaj komuś dwie pętle, z których każda używa każdego idiomu, i zapytaj ich, jaka jest różnica (poprawna odpowiedź: „nic”, może z dodatkiem „ale zwykle tego używam”).

mjfgates
źródło
2

Twoje alternatywy nie są tak złe, jak myślisz. Dobry kod powinien:

  1. Wyraźnie wskaż za pomocą dobrych nazw / komentarzy zmiennych, w jakich warunkach pętla powinna zostać zakończona. (Warunki zerwania nie powinny być ukryte)

  2. Wyraźnie wskaż kolejność wykonywania operacji. W tym miejscu DoSomethingElseTo(input)dobrym pomysłem jest umieszczenie opcjonalnej trzeciej operacji ( ) wewnątrz if.

To powiedziawszy, łatwo jest pozwolić, aby perfekcjonistyczne, mosiężne instynkty pochłaniały dużo czasu. Po prostu poprawiłbym, jeśli, jak wspomniano powyżej; skomentuj kod i przejdź dalej.

Poklepać
źródło
Myślę, że perfekcjonista, instynkt polerowania mosiądzu na dłuższą metę oszczędza czas. Mam nadzieję, że w praktyce rozwiniesz takie nawyki, aby Twój kod był doskonały, a jego mosiądz dopracowany bez zajmowania dużo czasu. Ale w przeciwieństwie do budowy fizycznej oprogramowanie może trwać wiecznie i być używane w nieskończonej liczbie miejsc. Oszczędności z kodu, które są nawet 0,00000001% szybsze, bardziej niezawodne, użyteczne i łatwe w utrzymaniu, mogą być duże. (Oczywiście, większość mojego kodu dużo polerowana jest w językach dawno przestarzałych lub dokonujących pieniądze dla kogoś innego te dni, ale udało mi pomóc rozwijać dobre nawyki.)
RalphChapin
Cóż, nie mogę cię źle nazwać :-) To jest pytanie opinii i jeśli dobre umiejętności wyszły z polerowania mosiądzu: świetnie.
Pat
W każdym razie myślę, że można to rozważyć. Na pewno nie chciałbym udzielać żadnych gwarancji. Czuję się lepiej, gdy myślę, że polerowanie mosiądzu poprawiło moje umiejętności, więc mogę być w tej sprawie uprzedzony.
RalphChapin
2

Ludzie twierdzą, że jest to zła praktyka while (true)i często mają rację. Jeżeli twójwhile instrukcja zawiera dużo skomplikowanego kodu i nie wiadomo, kiedy się z niego wyrwiesz, wtedy masz trudny do utrzymania kod, a prosty błąd może spowodować nieskończoną pętlę.

W twoim przykładzie możesz używać poprawnie, while(true)ponieważ kod jest przejrzysty i łatwy do zrozumienia. Nie ma sensu tworzyć nieefektywnego i trudniejszego do odczytania kodu, po prostu dlatego, że niektórzy uważają, że zawsze jest to zła praktyka while(true).

Miałem podobny problem:

$i = 0
$key = $str.'0';
$key1 = $str1.'0';
while (isset($array][$key] || isset($array][$key1])
{
    if (isset($array][$key]))
    {
        // Code
    }
    else
    {
        // Code
    }
    $key = $str.++$i;
    $key1 = $str1.$i;
}

Użycie do ... while(true)pozwala uniknąć isset($array][$key]niepotrzebnego podwójnego wywoływania, a kod jest krótszy, czystszy i łatwiejszy do odczytania. Nie ma absolutnie żadnego ryzyka, że else break;na końcu zostanie wprowadzony błąd nieskończonej pętli .

$i = -1;
do
{
    $key = $str.++$i;
    $key1 = $str1.$i;
    if (isset($array[$key]))
    {
        // Code
    }
    elseif (isset($array[$key1]))
    {
        // Code
    }
    else
        break;
} while (true);

Dobrze jest przestrzegać dobrych praktyk i unikać złych praktyk, ale zawsze są wyjątki i ważne jest, aby zachować otwarty umysł i zdawać sobie sprawę z tych wyjątków.

Dan Bray
źródło
0

Jeśli określony przepływ sterowania jest naturalnie wyrażony jako instrukcja break, zasadniczo nigdy nie jest lepszym wyborem, aby użyć innych mechanizmów kontroli przepływu do emulacji instrukcji break.

(zauważ, że obejmuje to częściowe rozwinięcie pętli i zsunięcie korpusu pętli w dół, aby pętla zaczęła się pokrywać z testem warunkowym)

Jeśli możesz zreorganizować swoją pętlę, aby przepływ kontroli był naturalnie wyrażony poprzez sprawdzenie warunkowe na początku pętli, świetnie. Warto nawet poświęcić trochę czasu na zastanowienie się, czy to możliwe. Ale nie, nie próbuj wpasowywać kwadratowego kołka w okrągły otwór.


źródło
0

Możesz również iść z tym:

public void doSomething(int input)
{
   while(TransformInSomeWay(input), !ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
   }
}

Lub mniej myląco:

bool TransformAndCheckCompletion(int &input)
{
   TransformInSomeWay(input);
   return ProcessingComplete(input))
}

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}
Zaćmienie
źródło
1
Chociaż niektórym się to podoba, twierdzę, że warunek pętli powinien być zasadniczo zarezerwowany dla testów warunkowych, a nie instrukcji, które robią różne rzeczy.
5
Wyrażenie przecinka w pętli ... To okropne. Jesteś zbyt sprytny. I działa tylko w tym trywialnym przypadku, w którym TransformInSomeWay może być wyrażony jako wyrażenie.
gnasher729,
0

Kiedy złożoność logiki staje się niewygodna, wówczas użycie kontroli nieograniczonej pętli z przerwami w środkowej pętli do kontroli przepływu ma naprawdę sens. Mam projekt z kodem, który wygląda następująco. (Zwróć uwagę na wyjątek na końcu pętli).

L: for (;;) {
    if (someBool) {
        someOtherBool = doSomeWork();
        if (someOtherBool) {
            doFinalWork();
            break L;
        }
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
            break L;
        }
        someOtherBool3 = doSomeWork3();
        if (someOtherBool3) {
            doFinalWork3();
            break L;
        }
    }
    bitOfData = getTheData();
    throw new SomeException("Unable to do something for some reason.", bitOfData);
}
progressOntoOtherThings();

Aby wykonać tę logikę bez nieograniczonego przerywania pętli, musiałbym uzyskać coś takiego:

void method() {
    if (!someBool) {
        throwingMethod();
    }
    someOtherBool = doSomeWork();
    if (someOtherBool) {
        doFinalWork();
    } else {
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
        } else {
            someOtherBool3 = doSomeWork3();
            if (someOtherBool3) {
                doFinalWork3();
            } else {
                throwingMethod();
            }
        }
    }
    progressOntoOtherThings();
}
void throwingMethod() throws SomeException {
        bitOfData = getTheData();
        throw new SomeException("Unable to do something for some reason.", bitOfData);
}

Tak więc wyjątek jest teraz zgłaszany metodą pomocniczą. Ma też całkiem sporo if ... elsezagnieżdżania. Nie jestem zadowolony z tego podejścia. Dlatego uważam, że pierwszy wzór jest najlepszy.

intrepidis
źródło
Rzeczywiście, zadałem to pytanie na SO i zestrzelono mnie w płomieniach ... stackoverflow.com/questions/47253486/…
intrepidis