Czy to usprawiedliwia stwierdzenia goto?

15

Natknąłem się na to pytanie przed chwilą i ściągam z tego trochę materiału: czy istnieje nazwa konstruktu „break n”?

Wydaje się to być niepotrzebnie złożonym sposobem na instruowanie programu, aby wyszedł z podwójnie zagnieżdżonej pętli for:

for (i = 0; i < 10; i++) {
    bool broken = false;
    for (j = 10; j > 0; j--) {
        if (j == i) {
            broken = true;
            break;
        }
    }
    if (broken)
        break;
}

Wiem, że podręczniki mówią, że stwierdzenia goto to diabeł, a ja wcale ich nie lubię, ale czy to uzasadnia wyjątek od reguły?

Szukam odpowiedzi, które dotyczą n-zagnieżdżonych pętli.

UWAGA: Niezależnie od tego, czy odpowiesz tak, nie, czy gdzieś pomiędzy, odpowiedzi całkowicie bliskie nie są mile widziane. Zwłaszcza jeśli odpowiedź brzmi „nie”, należy podać uzasadniony powód (i tak nie jest to zbyt daleko od przepisów Stack Exchange).

Panzercrisis
źródło
2
Jeśli możesz napisać warunek wewnętrznej pętli for (j = 10; j > 0 && !broken; j--), to nie potrzebujesz break;instrukcji. Jeśli przeniesiesz deklarację brokendo przed rozpoczęciem pętli zewnętrznej, to jeśli można to zapisać tak, for (i = 0; i < 10 && !broken; i++)to myślę, że nie break; są konieczne.
FrustratedWithFormsDesigner
8
Podany przykład pochodzi z C # - referencja C # na goto: goto (C # Reference) - „Instrukcja goto jest również przydatna, aby wydostać się z głęboko zagnieżdżonych pętli”.
OMG, nigdy nie wiedziałem, że c # ma oświadczenie goto! Rzeczy, których uczysz się na StackExchange. (Nie pamiętam, kiedy ostatni raz chciałem mieć zdolność goto, wydaje się, że nigdy nie pojawi się).
John Wu
6
IMHO to naprawdę zależy od twojej „religii programowania” i aktualnej fazy księżyca. Dla mnie (i większości ludzi wokół mnie) dobrze jest użyć goto, aby wydostać się z głębokich gniazd, jeśli pętla jest raczej niewielka, a alternatywą byłoby zbędne sprawdzenie zmiennej bool wprowadzonej tylko po to, by warunek pętli został przerwany. Tym bardziej, gdy chcesz wyrwać się w połowie ciała lub gdy po najbardziej wewnętrznej pętli jest jakiś kod, który musiałby również sprawdzić bool.
PlasmaHH
1
@JohnWu gotojest faktycznie wymagany w przełączniku C #, aby przejść do innej sprawy po wykonaniu dowolnego kodu w pierwszej sprawie. To jednak jedyny przypadek, w którym użyłem goto.
Dan Lyons,

Odpowiedzi:

33

Widoczna potrzeba wprowadzenia instrukcji wynika z tego, że wybrałeś słabe wyrażenia warunkowe dla pętli .

Stwierdzasz, że chciałeś, aby pętla zewnętrzna trwała tak długo, i < 10a wewnętrzna - tak długo, jak j > 0.

Ale w rzeczywistości nie było to, czego chciałeś , po prostu nie powiedziałeś pętlom o prawdziwym stanie, który chciałbyś, aby ocenili, a następnie chcesz go rozwiązać za pomocą break lub goto.

Jeśli powiesz pętlom swoje prawdziwe zamiary na początek , nie będziesz potrzebować przerw ani goto.

bool alsoThis = true;
for (i = 0; i < 10 && alsoThis; i++)
{    
    for (j = 10; j > 0 && alsoThis; j--)
    {
        if (j == i)
        {
            alsoThis = false;
        }
    }
}
Tulains Córdova
źródło
Zobacz moje komentarze do innych odpowiedzi. Oryginalny kod jest drukowany ina końcu zewnętrznej pętli, więc zwarcie przyrostu jest ważne, aby kod wyglądał w 100% równoważnie. Nie twierdzę, że cokolwiek w twojej odpowiedzi jest złe, chciałem tylko zaznaczyć, że natychmiastowe przerwanie pętli było ważnym aspektem kodu.
pswg
1
@pswg Nie widzę żadnego kodu drukującego się ina końcu zewnętrznej pętli w pytaniu OP.
Tulains Córdova
OP odwoływał się do kodu z mojego pytania tutaj , ale pominął tę część. Nie głosowałem cię, BTW. Odpowiedź jest całkowicie poprawna w odniesieniu do prawidłowego wyboru warunków pętli.
pswg
3
@pswg Wydaje mi się, że jeśli naprawdę naprawdę chcesz usprawiedliwić goto, możesz wymyślić problem, który go potrzebuje, z drugiej strony programuję profesjonalnie od dwóch dekad i nie mam ani jednego problemu z rzeczywistością, który wymagałby jeden.
Tulains Córdova
1
Celem tego kodu nie było podanie prawidłowego przypadku użycia goto(jestem pewien, że są lepsze), nawet jeśli zrodziło się pytanie, które wydaje się używać go jako takiego, ale aby zilustrować podstawowe działanie break(n)w językach, w których „ t wspierać to.
pswg
34

W tym przypadku można przefakturować kod na osobną procedurę, a następnie bezpośrednio returnz wewnętrznej pętli. To negowałoby potrzebę wyrwania się z zewnętrznej pętli:

bool isBroken() {
    for (i = 0; i < 10; i++)
        for (j = 10; j > 0; j--)
            if (j == i) return true;

    return false;
}
zrozumiałem
źródło
2
To znaczy, że tak, to, co pokazujesz, jest niepotrzebnie złożonym sposobem na zerwanie z zagnieżdżonymi pętlami, ale nie, istnieje sposób na refaktoryzację, który nie sprawia, że ​​goto jest bardziej atrakcyjny.
Roger
2
Tylko uwaga: w oryginalnym kodzie drukuje się ipo zakończeniu zewnętrznej pętli. Aby naprawdę odzwierciedlić, że ijest to ważna wartość, tutaj return true;i return false;powinny być obie return i;.
pswg
+1, właśnie miał opublikować tę odpowiedź i uważam, że powinna to być odpowiedź zaakceptowana. Nie mogę uwierzyć, że zaakceptowana odpowiedź została zaakceptowana, ponieważ ta metoda działa tylko dla określonego podzbioru algorytmów pętli zagnieżdżonej i niekoniecznie czyni kod znacznie czystszym. Metoda opisana w tej odpowiedzi działa ogólnie.
Ben Lee,
6

Nie, nie uważam, że jest gototo konieczne. Jeśli napiszesz to w ten sposób:

bool broken = false;
saved_i = 0;
for (i = 0; i < 10 && !broken; i++)
{
    broken = false;
    for (j = 10; j > 0 && !broken; j--)
    {
        if (j == i)
        {
            broken = true;
            saved_i = i;
        }
    }
}

Posiadanie &&!brokenobu pętli pozwala na wyrwanie się z obu pętli, kiedy i==j.

Dla mnie takie podejście jest lepsze. Warunki pętlowe są bardzo jasne: i<10 && !broken.

Działającą wersję można zobaczyć tutaj: http://rextester.com/KFMH48414

Kod:

public static void main(String args[])
{
    int i=0;
    int j=0;
    boolean broken = false;
    for (i = 0; i < 10 && !broken; i++)
    {
        broken = false;
        for (j = 10; j > 0 && !broken; j--)
        {
            if (j == i)
            {
                broken = true;
                System.out.println("loop breaks when i==j=="+i);
            }
        }
    }
    System.out.println(i);
    System.out.println(j);
}

Wynik:

pętla pęka, gdy i == j == 1
2)
0
FrustratedWithFormsDesigner
źródło
Dlaczego nawet użyć brokenw swoim pierwszym przykładzie w ogóle ? Po prostu użyj przerwy na wewnętrznej pętli. Ponadto, jeśli absolutnie musisz użyć broken, dlaczego deklarujesz to poza pętlami? Wystarczy zadeklarować je wewnątrz tej ipętli. Jeśli chodzi o whilepętlę, nie używaj tego. Szczerze mówiąc, myślę, że udaje mu się być mniej czytelnym niż wersja goto.
luiscubal
@luiscubal: Używa się zmiennej o nazwie, brokenponieważ nie lubię korzystać z breakinstrukcji (z wyjątkiem przypadku przełączania, ale o to chodzi). Jest zadeklarowany poza zewnętrzną pętlą, na wypadek, gdy chcesz przerwać WSZYSTKIE pętle i==j. Masz rację, ogólnie rzecz biorąc, musisz brokentylko zadeklarować przed przerwaniem najbardziej zewnętrznej pętli.
FrustratedWithFormsDesigner
W aktualizacji i==2na końcu pętli. Warunek powodzenia powinien również zwierać przyrost, jeśli ma on być w 100% równoważny a break 2.
pswg
2
Ja osobiście wolę broken = (j == i)bardziej niż if i, jeśli pozwala na to język, umieszczając złamaną deklarację w inicjalizacji zewnętrznej pętli. Chociaż trudno powiedzieć te rzeczy w tym konkretnym przykładzie, ponieważ cała pętla nie działa (nic nie zwraca i nie ma skutków ubocznych), a jeśli coś robi, prawdopodobnie robi to wewnątrz if (chociaż nadal lubię broken = (j ==i); if broken { something(); }lepiej osobiście)
Martijn
@Martijn W moim oryginalnym kodzie drukuje się ipo zakończeniu zewnętrznej pętli. Innymi słowy, jedyną naprawdę ważną myślą jest to, kiedy dokładnie wyłamuje się z zewnętrznej pętli.
pswg
3

Krótka odpowiedź brzmi „to zależy”. Opowiadam się za wyborem dotyczącym czytelności i zrozumiałości twojego kodu. Sposób na goto może być bardziej czytelny niż poprawianie warunku lub odwrotnie. Możesz także wziąć pod uwagę zasadę najmniejszego zaskoczenia, wytyczne stosowane w sklepie / projekcie oraz spójność bazy kodu. Na przykład goto do opuszczenia przełącznika lub obsługi błędów jest powszechną praktyką w bazie kodu jądra Linux. Zauważ też, że niektórzy programiści mogą mieć problemy z odczytaniem twojego kodu (albo wychowany z mantrą „goto is evil”, albo po prostu nie nauczył się, ponieważ ich nauczyciel tak uważa), a niektórzy z nich mogą nawet „osądzić cię”.

Ogólnie rzecz biorąc, goto idące dalej w tej samej funkcji jest często dopuszczalne, szczególnie jeśli skok nie jest zbyt długi. Przypadek użycia pozostawienia zagnieżdżonej pętli jest jednym ze znanych przykładów goto „not so evil”.

FabienAndre
źródło
2

W twoim przypadku goto wystarczy ulepszenia, aby wyglądało kusząco, ale nie jest to tak wiele ulepszenia, że ​​warto złamać regułę, aby nie używać ich w bazie kodu.

Pomyśl o swoim przyszłym recenzencie: on / ona będzie musiał pomyśleć: „Czy ta osoba jest złym programistą, czy też jest to przypadek, w którym warto było skorzystać z goto? Pozwól mi poświęcić 5 minut na samodzielne podjęcie decyzji lub zbadanie Internet." Dlaczego, do licha, miałbyś to zrobić dla swojego przyszłego programisty, skoro nie możesz całkowicie użyć goto?

Zasadniczo ta mentalność dotyczy całego „złego” kodu, a nawet go definiuje: jeśli teraz działa i jest dobry w tym przypadku, ale dokłada starań, aby sprawdzić, czy jest poprawny, co w tej erze będzie zawsze robić, wtedy nie Zrób to.

Biorąc to pod uwagę, tak, stworzyłeś jedną z nieco bardziej ostrych spraw. Możesz:

  • użyj bardziej złożonych struktur kontrolnych, takich jak flaga boolowska, aby wyrwać się z obu pętli
  • umieść wewnętrzną pętlę w jej własnej procedurze (prawdopodobnie idealnej)
  • umieść obie pętle w procedurze i wróć zamiast się zrywać

Bardzo trudno jest mi stworzyć przypadek, w którym podwójna pętla nie jest tak spójna, że ​​i tak nie powinna być własną rutyną.

Nawiasem mówiąc, najlepszą dyskusją na ten temat, jaką widziałem, jest Kod ukończony przez Steve'a McConnella . Jeśli lubisz krytycznie myśleć o tych problemach, zdecydowanie zalecamy lekturę, nawet od okładki do okładki, pomimo jej ogromu.

djechlin
źródło
1
Nasi programiści pracy czytają i rozumieją kod. Nie tylko uproszczony kod, chyba że taka jest natura Twojej bazy kodu. Istnieją i zawsze będą wyjątki od ogólności (nie reguły) przeciw używaniu gotos ze względu na rozumiany koszt. Ilekroć korzyść przewyższa koszt, należy zastosować goto; jak w przypadku wszystkich decyzji dotyczących kodowania w inżynierii oprogramowania.
dietbuddha
1
@dietbuddha naruszanie przyjętych konwencji kodowania oprogramowania wiąże się z pewnymi kosztami, które należy odpowiednio uwzględnić. Zwiększenie różnorodności struktur kontrolnych wykorzystywanych w programie kosztuje, nawet jeśli ta struktura kontrolna może być dobrze dopasowana do sytuacji. Przełamywanie statycznej analizy kodu jest kosztowne. Jeśli to wszystko wciąż jest tego warte, to kim jestem, aby się kłócić.
djechlin
1

TLD; DR: Nie konkretnie, tak ogólnie

W tym konkretnym przykładzie, którego użyłeś, powiedziałbym „nie” z tych samych powodów, które wskazało wiele innych. Możesz łatwo refaktoryzować kod do równorzędnej dobrej formy, co eliminuje potrzebę goto.

Ogólnie rzecz biorąc, zakazanie gotojest ogólnością opartą na zrozumiałej naturze gotoi kosztach jej używania. Część inżynierii oprogramowania podejmuje decyzje wdrożeniowe. Uzasadnienie tych decyzji następuje poprzez porównanie kosztów z korzyściami, a gdy korzyści przewyższają koszty, wdrożenie jest uzasadnione. Kontrowersje powstają z powodu różnych wycen, zarówno kosztów, jak i korzyści.

Chodzi o to, że zawsze będą wyjątki, w których gotouzasadnienie jest uzasadnione, ale tylko dla niektórych z powodu różnych wycen. Szkoda, że ​​wielu inżynierów po prostu wyklucza techniki celowej ignorancji, ponieważ czytają je w Internecie, a nie zastanawiają się dlaczego.

dietbuddha
źródło
Czy możesz polecić jakieś artykuły na wyciągu goto wyjaśniające koszty?
Thys,
-1

Nie sądzę, aby ten przypadek uzasadniał wyjątek, ponieważ można go zapisać jako:

def outerLoop(){
    for (i = 0; i < 10; i++) {
        if( broken?(i) )
          return; // broken
    }
}

def broken?(i){
   for (j = 10; j > 0; j--) {
        if (j == i) {
            return true; // broken
        }
   }
   return false; // not broken
}
ptyx
źródło