Czy używanie oznaczonej przerwy jest dobrą praktyką w Javie?

82

Wpatruję się w jakiś stary kod z 2001 roku i natknąłem się na to stwierdzenie:

   else {
     do {
       int c = XMLDocumentFragmentScannerImpl.this.scanContent();
       if (c == 60) {
         XMLDocumentFragmentScannerImpl.this.fEntityScanner.scanChar();
         XMLDocumentFragmentScannerImpl.this.setScannerState(1);
         break label913;
       }

Nigdy wcześniej tego nie widziałem i odkryłem tutaj oznaczone przerwy:

http://docs.oracle.com/javase/tutorial/java/nutsandbolts/branch.html

Czy to zasadniczo nie działa goto? Czy to w ogóle dobra praktyka? To mnie niepokoi.

avgvstvs
źródło
2
gdzie jest label913zdefiniowane?
Nikolay Kuznetsov
1
W tym przypadku około 200 linii później na końcu dość dużej instrukcji switch.
avgvstvs
kontynuuj i nazwa etykiety sprawią, że kod przetworzy tę część kodu, w której blok został oznaczony etykietą
user_CC
5
W takim przypadku problemem jest rozmiar instrukcji switch!
Cyrille Ka
8
To wygląda jak wygenerowany kod (a właściwie parser). Istnieje wiele sztuczek używanych z wygenerowanym kodem, które nie zostałyby użyte w kodzie pisanym odręcznie (głównie dlatego, że ułatwiają generowanie).
parsifal

Odpowiedzi:

120

Nie, to nie jest goto, ponieważ nie można „przejść” do innej części przepływu sterowania.

Ze strony, do której jesteś podłączony:

Instrukcja break kończy etykietowaną instrukcję; nie przenosi przepływu kontroli na etykietę. Przepływ kontrolny jest przenoszony do wyciągu bezpośrednio po oznaczonej (zakończonej) instrukcji.

Oznacza to, że możesz przerywać tylko pętle, które są aktualnie wykonywane.

Rozważmy ten przykład:

first:
for( int i = 0; i < 10; i++) {
  second:
  for(int j = 0; j < 5; j ++ )
  {
    break xxx;
  }
}

third:
for( int a = 0; a < 10; a++) {

}

Można wymienić xxxz firstlub second(złamać zewnętrznej lub wewnętrznej pętli), ponieważ obie pętle są wykonywane, gdy trafisz na breakoświadczenie, ale zastępując xxxze thirdnie będzie skompilować.

Tomasz
źródło
1
@user_CC Niezupełnie, nadal nie możesz wyskoczyć z przepływu sterowania. Oznaczona etykietą kontynuacja po prostu rozpocznie następną iterację oznaczonej pętli i będzie działać tylko wtedy, gdy ta pętla jest już wykonywana.
Thomas,
3
więc gdybym użył break first;, czy kompilator wróciłby do linii natychmiast po first:i ponownie wykonał te pętle?
Ungeheuer
15
@JohnnyCoder nie, break first;by złamać (koniec) pętla oznaczony first, tj byłoby kontynuować third. Z drugiej strony continue first;zakończy bieżącą iterację firsti zerwie secondz nią i będzie kontynuował z następną wartością iin first.
Thomas
1
@TheTechExpertGuy no cóż, nie bardzo. Jest tylko podobne do gotomniej więcej w tym samym sensie, co if-elsestwierdzenie. Goto są znacznie bardziej „elastyczne” jak w „idź gdziekolwiek”, np. Do pozycji przed pętlą (w rzeczywistości tak starsze języki używały pętli: „jeśli warunek nie zostanie spełniony, przejdź do początku treści pętli (ponownie)”) . Miałem z nimi ostatnio do czynienia i uwierz mi: gototo prawdziwy wrzód na tyłku, podczas gdy oznaczone jako przerwy nie.
Thomas
1
@TheTechExpertGuy, nie ma za co :) - Ponieważ jesteś początkującym, mam ochotę jeszcze raz udzielić rady: nie używaj gotow ogóle, nawet jeśli C ++ to obsługuje. Prawie zawsze jest lepszy sposób na rozwiązanie problemów, które później oszczędzą Ci wielu bólów głowy. Może nadejść pokusa ... oprzyj się!
Thomas
16

Nie jest tak straszne, jak gototylko dlatego, że wysyła sterowanie tylko na koniec oznaczonej instrukcji (zwykle jest to konstrukcja pętli). Rzeczą, która sprawia goto, że nie da się lubić, jest to, że jest to dowolna gałąź do dowolnego miejsca, w tym etykiety znalezione wyżej w źródle metody, dzięki czemu można uzyskać zachowanie zapętlone w domu. Funkcja dzielenia etykiet w Javie nie pozwala na tego rodzaju szaleństwa, ponieważ kontrola idzie tylko do przodu.

Użyłem go tylko raz, około 12 lat temu, w sytuacji, w której musiałem wyrwać się z zagnieżdżonych pętli, bardziej zorganizowana alternatywa mogłaby zrobić bardziej skomplikowane testy w pętlach. Nie radziłbym go często używać, ale nie oznaczałbym go jako automatyczny zapach złego kodu.

Nathan Hughes
źródło
6
@Boann: Nie chcę sugerować, że goto jest całkowicie zła we wszystkich kontekstach. to tylko jedna z tych rzeczy, takich jak arytmetyka wskaźników, przeciążanie operatorów i jawne zarządzanie pamięcią, które twórcy Javy uznali, że nie są warte zachodu. I po przeczytaniu programów COBOL, które wydawały się mieć ponad 90% błędów, nie jestem smutny, że pominęli je w Javie.
Nathan Hughes
1
Och, miałeś rację za pierwszym razem @NathanHughes, goto jest cholernie zło. Oczywiście nie we wszystkich sytuacjach, ale tak, nie tęsknię za tym.
Percepcja,
2
Goto może być bardziej „użyteczny” niż etykiety, @Boann, ale jest to całkowicie równoważone kosztami utrzymania. etykiety nie są magiczną różdżką, którą są, i to dobrze.
DavidS
9

Zawsze można zastąpić breaknową metodą.

Rozważmy kod sprawdzający wszelkie wspólne elementy na dwóch listach:

List list1, list2;

boolean foundCommonElement = false;
for (Object el : list1) {
    if (list2.contains(el)) {
        foundCommonElement = true;
        break;
    }
}

Możesz to przepisać w ten sposób:

boolean haveCommonElement(List list1, List list2) {
    for (Object el : list1) {
        if (list2.contains(el)) {
            return true;
        }
    }
    return false;
}

Oczywiście, aby sprawdzić wspólne elementy między dwiema listami, lepiej jest użyć list1.retainAll(new HashSet<>(list2))metody, aby to zrobić O(n)z O(n)dodatkową pamięcią, lub posortować obie listy w, O(n * log n)a następnie znaleźć wspólne elementy w O(n).

logicray
źródło
Moim zdaniem prawie zawsze jest to właściwe podejście. gotojest (potencjalnie) szkodliwy, ponieważ może wysłać tak wiele miejsc - wcale nie jest oczywiste, co zostało osiągnięte; tworzenie funkcji pomocniczej daje następnemu facetowi jasne granice co do tego, co próbujesz zrobić i gdzie zamierzasz to zrobić, a także daje szansę na nazwanie tego elementu funkcjonalności. W ten sposób wyraźniej.
ethanbustad
@ethanbustad Ale tu chodzi o oznaczone break, a nie goto. Jasne, pokazana tutaj refaktoryzacja jest dobrym pomysłem w obu przypadkach. Ale etykietowanie breaknie jest tak niemoderowane i podatne na spaghetti, jak gotojest.
underscore_d
Ten przykład jest okropny, przerwa nie działa, jeśli etykieta tutaj jest po prostu bezużyteczna.
Diracnote,
8

Przed przeczytaniem pozostałej części tej odpowiedzi przeczytaj Przejdź do oświadczenia uważanego za szkodliwy . Jeśli nie chcesz czytać go w całości, oto, co uważam za kluczowy punkt:

Nieokiełznane użycie polecenia go to ma natychmiastową konsekwencję, że znalezienie sensownego zestawu współrzędnych, w których można by opisać postęp procesu, staje się bardzo trudne.

Inaczej mówiąc, problem gotopolega na tym, że program może pojawić się w środku bloku kodu, bez zrozumienia przez programistę stanu programu w tym miejscu. Standardowe konstrukcje zorientowane na bloki są zaprojektowane tak, aby jasno określać przejścia stanów, etykieta breakma na celu przeniesienie programu do określonego znanego stanu (poza zawierającym oznaczony blok).

W programie imperatywnym w świecie rzeczywistym stan nie jest wyraźnie wyznaczony granicami bloków, więc wątpliwe jest, czy oznaczenie breakjest dobrym pomysłem. Jeśli blok zmienia stan, który jest widoczny z zewnątrz bloku i istnieje wiele punktów wyjścia z bloku, to oznaczenie breakjest równoważne prymitywu goto. Jedyna różnica polega na tym, że zamiast szansy wylądowania w środku bloku ze stanem nieokreślonym, zaczynasz nowy blok ze stanem nieokreślonym.

Tak więc ogólnie uważałbym etykietę breakza niebezpieczną. Moim zdaniem to znak, że blok należy przekształcić w funkcję, z ograniczonym dostępem do otaczającego zakresu.

Jednak ten przykładowy kod był wyraźnie produktem generatora parserów (OP skomentował, że był to kod źródłowy Xerces). Generatory parserów (lub generatory kodu w ogóle) często zachowują swobodę w kwestii generowanego kodu, ponieważ mają doskonałą wiedzę o stanie, a ludzie nie muszą go rozumieć.

parsifal
źródło
Nie zgadzam się. breakjest bardzo przydatne w kilku przypadkach. Na przykład, gdy szukasz jakiejś wartości, która może pochodzić z kilku zamówionych źródeł. Próbujesz jeden po drugim, a kiedy zostanie znaleziony pierwszy, ty break. Jest to bardziej elegancki kod niż if / else if / else if / else if. (Przynajmniej jest mniej linii). Przeniesienie wszystkiego do kontekstu metody może nie zawsze być praktyczne. Innym przypadkiem jest to, że warunkowo łamiesz kilka zagnieżdżonych poziomów. Krótko mówiąc, jeśli nie lubisz break, nie lubisz returnnigdzie indziej, z wyjątkiem końca metody.
Ondra Žižka
W kontekście; „Go To Statement uważane za szkodliwe” zostało napisane, gdy rzeczy takie jak doi whilenie istniały, a if(.. ) gotozamiast tego wszyscy używali wszędzie. Jego celem było zachęcenie projektantów języków do dodania obsługi takich rzeczy jak doi while. Niestety, gdy modą zaczęli się kręcić, przerodziło się w hype pociąg napędzany przez ignorantów, umieszczających rzeczy w rodzaju breakpętli „wykonywanych tylko raz” i rzucających wyjątki nie wiadomo co, we wszystkich (rzadkich) przypadkach, które gotosą czystsze i mniej szkodliwe.
Brendan
1

To nie jest jak gotoinstrukcja, w której przeskakujesz sterowanie przepływem wstecz. Etykieta po prostu pokazuje Tobie (programiście), gdzie nastąpiła przerwa. Ponadto sterowanie przepływem jest przenoszone do następnej instrukcji po break.

Jeśli chodzi o używanie go, osobiście uważam, że nie ma to większego pożytku, ponieważ gdy zaczniesz pisać kod wart tysiące wierszy, staje się on nieistotny. Ale znowu zależałoby to od przypadku użycia.

koczownik
źródło
for(int i = 0; i < 1; i++) { do_something(); if(I_want_to_go_backwards) { i = 0; continue; } }.
Brendan,
1

Czystszym sposobem wyrażenia zamiaru mogłoby być, przynajmniej moim zdaniem, umieszczenie fragmentu kodu zawierającego pętlę w oddzielnej metodzie i po prostu returnz niej.

Na przykład zmień to:

someLabel:
for (int j = 0; j < 5; j++)
{
    // do something
    if ...
        break someLabel;
}

zaangażowany w to:

private void Foo() {
    for (int j = 0; j < 5; j++)
    {
        // do something
        if ...
            return;
    }
}

Jest to również bardziej idiomatyczne dla programistów biegłych w innych językach, które mogą współpracować z Twoim kodem w przyszłości (lub w przyszłości ).

Marek
źródło