Jak uniknąć bezużytecznego zwrotu w metodzie Java?


115

Mam sytuację, w której returninstrukcja zagnieżdżona w dwóch forpętlach teoretycznie zawsze zostanie osiągnięta.

Kompilator nie zgadza się i wymaga returninstrukcji poza forpętlą. Chciałbym poznać elegancki sposób optymalizacji tej metody, który wykracza poza moje obecne rozumienie, i żadna z moich prób implementacji przerwy nie wydaje się działać.

Attached to metoda z przypisania, która generuje losowe liczby całkowite i zwraca iteracje wykonywane cyklicznie do momentu znalezienia drugiej losowej liczby całkowitej, wygenerowanej w zakresie przekazanym do metody jako parametr int.

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
    }
    return 0; // Never reached
}

9
Jeśli ostatni element nigdy nie zostanie osiągnięty, możesz użyć while(true)zamiast indeksowanej pętli. To mówi kompilatorowi, że pętla nigdy nie powróci.
Boris the Spider,

101
wywołaj funkcję z zakresem jako 0 (lub dowolną inną liczbą mniejszą niż 1) ( oneRun(0)) i zobaczysz, że szybko osiągasz swój nieosiągalnyreturn
mcfedr

55
Zwrot jest osiągany, gdy podany zakres jest ujemny. Masz również walidację 0 dla zakresu wejściowego, więc obecnie nie przechwytujesz go w żaden inny sposób.
Miejmy nadzieję, że

4
@HopefullyHelpful To jest oczywiście prawdziwa odpowiedź. To wcale nie jest bezcelowy powrót!
Pan Lister

4
@HopefullyPomocne nie, ponieważ nextIntrzuca wyjątek dla range < 0Jedynym przypadkiem, w którym osiągany jest zwrot, jestrange == 0
njzk2

Odpowiedzi:


343

Heurystyka kompilatora nigdy nie pozwoli ci pominąć ostatniego return. Jeśli jesteś pewien, że nigdy nie zostanie osiągnięty, zastąpiłbym go znakiem, throwaby wyjaśnić sytuację.

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) {
        ...
    }

    throw new AssertionError("unreachable code reached");
}

135
Nie tylko czytelność, ale także upewnienie się, że dowiesz się, czy coś jest nie tak z Twoim kodem. Jest całkowicie prawdopodobne, że generator może mieć błąd.
JollyJoker,

6
Jeśli jesteś ABSOLUTNIE pewny, że Twój kod nigdy nie dotrze do tego „nieosiągalnego kodu”, utwórz kilka testów jednostkowych dla wszystkich przypadków brzegowych, które mogą się pojawić (zakres to 0, zakres to -1, zakres to min / max int). Może to być teraz metoda prywatna, ale następny programista może tego nie trzymać. Sposób obsługi nieoczekiwanych wartości (zgłaszanie wyjątku, zwracanie wartości błędu, nie robienie niczego) zależy od tego, jak zamierzasz używać metody. Z mojego doświadczenia wynika, że ​​zwykle chcesz po prostu zarejestrować błąd i wrócić.
Rick Ryker

22
Może tak powinno byćthrow new AssertionError("\"unreachable\" code reached");
Bohemian

8
Napisałbym dokładnie to, co umieściłem w mojej odpowiedzi w programie produkcyjnym.
John Kugelman,

1
W przypadku podanego przykładu istnieje lepszy sposób radzenia sobie z nim niż po prostu dodanie czegoś throw, co nigdy nie zostanie osiągnięte. Zbyt często ludzie chcą po prostu szybko zastosować „jedno rozwiązanie, aby wszystkimi rządzić”, nie zastanawiając się zbytnio nad podstawowym problemem. Ale programowanie to znacznie więcej niż tylko kodowanie. Zwłaszcza jeśli chodzi o algorytmy. Jeśli (uważnie) przyjrzysz się podanemu problemowi, zdasz sobie sprawę, że możesz odliczyć ostatnią iterację zewnętrznej forpętli i tym samym zastąpić „bezużyteczny” zwrot użytecznym (zobacz tę odpowiedź ).
a_guest

36

Jak zauważył @BoristheSpider , możesz upewnić się, że druga returninstrukcja jest semantycznie nieosiągalna:

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    int count = 0;

    while (true) {
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
        count++;
    }
}

Kompiluje się i działa dobrze. A jeśli kiedykolwiek otrzymasz komunikat ArrayIndexOutOfBoundsException, będziesz wiedział, że implementacja była semantycznie błędna, bez konieczności jawnego rzucania czegokolwiek.


1
Dokładnie: warunek nie jest w rzeczywistości używany do sterowania pętlą, więc nie powinien tam być. Jednak napisałbym to jako for(int count = 0; true; count++)zamiast.
cmaster

3
@cmaster: możesz pominąć true:for(int count = 0; ; count++) …
Holger

@Holger Ah. Nie byłem tego pewien, ponieważ chodzi o Javę i nie dotykałem tego języka od lat, ponieważ bardziej interesuję się C / C ++. Więc kiedy zobaczyłem użycie while(true), pomyślałem, że może java jest nieco bardziej rygorystyczny pod tym względem. Miło wiedzieć, że nie było powodów do zmartwień ... Właściwie trueznacznie bardziej podoba mi się pominięta wersja: pokazuje wizualnie, że nie ma absolutnie żadnego warunku do obejrzenia :-)
cmaster - przywróć monikę

3
Wolałbym nie przełączać się na „za”. „Podczas gdy prawda” jest oczywistą flagą, która oznacza, że ​​blok powinien zapętlić się bezwarunkowo, chyba że coś wewnątrz go przerwie. „Za” z pominiętym warunkiem nie komunikuje się tak wyraźnie; można by było łatwiej przeoczyć.
Corrodias,

1
@ l0b0 Całe pytanie prawdopodobnie byłoby lepsze do przeglądu kodu, biorąc pod uwagę, że ostrzeżenie jest związane z jakością kodu.
Sulthan

18

Ponieważ pytałeś o wyrwanie się z dwóch forpętli, możesz użyć do tego etykiety (zobacz przykład poniżej):

private static int oneRun(int range) {
    int returnValue=-1;

    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    OUTER: for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                returnValue = count;
                break OUTER;
            }
        }
    }
    return returnValue;
}

Czy to się nie skompiluje, ponieważ returnValuemoże być używane niezainicjowane?
pkt.

1
Najprawdopodobniej. Celem postu było pokazanie, jak wyrwać się z obu pętli, o co pytał oryginalny plakat. OP był pewien, że wykonanie nigdy nie dotrze do końca metody, więc to, co wywołałeś, można naprawić, inicjując returnValue dowolną wartość, gdy zostanie zadeklarowana.
David Choweller,

4
Czy etykiety nie są tego rodzaju rzeczami, których w jakikolwiek sposób lepiej unikać?
Serverfrog

2
Myślę, że myślisz o gotach.
David Choweller

4
Etykiety w języku programowania wysokiego (-awego) poziomu. Absolutnie barbarzyńskie ...
xDaizu

13

Chociaż asercja jest dobrym, szybkim rozwiązaniem. Ogólnie tego rodzaju problemy oznaczają, że Twój kod jest zbyt skomplikowany. Kiedy patrzę na twój kod, jest oczywiste, że tak naprawdę nie chcesz, aby tablica zawierała poprzednie liczby. Chcesz Set:

Set<Integer> previous = new HashSet<Integer>();

int randomInt = generator.nextInt(range);
previous.add(randomInt);

for (int count = 1; count <= range; count++) {
    randomInt = generator.nextInt(range);
    if (previous.contains(randomInt)) {
       break;
    }

    previous.add(randomInt);
}

return previous.size();

Zwróćmy teraz uwagę, że zwracamy w rzeczywistości rozmiar zestawu. Złożoność kodu zmniejszyła się z kwadratowej do liniowej i jest od razu bardziej czytelny.

Teraz możemy zdać sobie sprawę, że nie potrzebujemy nawet tego countindeksu:

Set<Integer> previous = new HashSet<Integer>();

int randomInt = generator.nextInt(range);

while (!previous.contains(randomInt)) {          
    previous.add(randomInt);      
    randomInt = generator.nextInt(range);
}

return previous.size();

8

Ponieważ wartość zwracana jest oparta na zmiennej zewnętrznej pętli, możesz po prostu zmienić warunek pętli zewnętrznej na, count < rangea następnie zwrócić tę ostatnią wartość (którą właśnie ominąłeś) na końcu funkcji:

private static int oneRun(int range) {
    ...

    for (int count = 1; count < range; count++) {
        ...
    }
    return range;
}

W ten sposób nie musisz wprowadzać kodu, który nigdy nie zostanie osiągnięty.


Ale czy nie wymagałoby to wyrwania się z dwóch warstw pętli po znalezieniu dopasowania? Nie widzę sposobu, aby zredukować do jednego dla instrukcji, należy wygenerować nową liczbę losową i porównać ją z liczbą przed wygenerowaniem kolejnej.
Oliver Benning

Nadal masz dwie zagnieżdżone pętle for (właśnie pominąłem drugą ...), ale zewnętrzna pętla została zmniejszona przez ostatnią iterację. Przypadek odpowiadający tej ostatniej iteracji jest obsługiwany oddzielnie przez ostatnią instrukcję return. Chociaż jest to eleganckie rozwiązanie dla twojego przykładu, istnieją scenariusze, w których takie podejście staje się trudniejsze do odczytania; jeśli na przykład wartość zwracana zależy od pętli wewnętrznej, to zamiast pojedynczej instrukcji return na końcu pozostałaby dodatkowa pętla (reprezentująca wewnętrzną pętlę dla ostatniej iteracji pętli zewnętrznej).
a_guest

5

Użyj zmiennej tymczasowej, na przykład „result”, i usuń wewnętrzny zwrot. Zmień pętlę for na chwilę z odpowiednim warunkiem. Dla mnie zawsze bardziej eleganckie jest mieć tylko jeden powrót jako ostatnią instrukcję funkcji.


Cóż, wygląda na to, że bardzo wewnętrzny, gdyby mógł być stan zewnętrzny, podczas gdy. Pamiętaj, że zawsze jest chwila odpowiednika a (i bardziej elegancka, ponieważ nie planujesz, zawsze przechodź przez wszystkie iteracje). Te dwie zagnieżdżone pętle for można na pewno uprościć. Cały ten kod pachnie „zbyt skomplikowanym”.
David

@ Sekret Salomonoffa Twoje stwierdzenie jest absurdalne. Oznacza to, że „ogólnie” powinniśmy dążyć do dwóch lub więcej instrukcji powrotu. Ciekawy.
David

@ Solomonoff'sSecret To kwestia preferowanych stylów programowania i niezależnie od tego, co było fajne w tym tygodniu, a co było kiepskie w zeszłym tygodniu, istnieją pewne zalety posiadania tylko jednego punktu wyjścia, a na końcu metody (pomyśl tylko o pętli z wielu return resultrozsypało się w nim, a potem myśli o tym, aby teraz jeszcze raz sprawdzić znalezione resultrzeczy przed zwróceniem go, a to tylko przykład). Mogą być też minusy, ale tak szerokie stwierdzenie, jak „Ogólnie rzecz biorąc, nie ma dobrego powodu, aby mieć tylko jeden zwrot”, nie wytrzymuje.
SantiBailors

@David Pozwól, że powtórzę: nie ma ogólnego powodu, aby mieć tylko jeden zwrot. W niektórych przypadkach mogą to być powody, ale zazwyczaj jest to kult cargo w najgorszym przypadku.
Przywróć Monikę

1
Chodzi o to, co sprawia, że ​​kod jest bardziej czytelny. Jeśli w swojej głowie mówisz „jeśli tak, zwróć to, co znaleźliśmy; w przeciwnym razie kontynuuj; jeśli czegoś nie znaleźliśmy, zwróć tę inną wartość”. Wtedy twój kod powinien mieć dwie wartości zwracane. Zawsze koduj to, co sprawia, że ​​jest to bardziej zrozumiałe dla następnego programisty. Kompilator ma tylko jeden punkt powrotu z metody Java, nawet jeśli dla naszych oczu wygląda to na dwa.
Rick Ryker

3

Może to jest wskazówka, że ​​powinieneś przepisać swój kod. Na przykład:

  1. Utwórz tablicę liczb całkowitych 0 .. zakres-1. Ustaw wszystkie wartości na 0.
  2. Wykonaj pętlę. W pętli wygeneruj liczbę losową. Spójrz na swoją listę, na ten indeks, aby zobaczyć, czy wartość wynosi 1. Jeśli tak, wyrwij się z pętli. W przeciwnym razie ustaw wartość tego indeksu na 1
  3. Policz liczbę jedynek na liście i zwróć tę wartość.

3

Metody, które mają instrukcję return i mają wewnątrz pętlę / pętle, zawsze wymagają instrukcji return poza pętlą (-ami). Nawet jeśli ta instrukcja poza pętlą nigdy nie zostanie osiągnięta. W takich przypadkach, aby uniknąć niepotrzebnych instrukcji powrotu, możesz zdefiniować zmienną odpowiedniego typu, w twoim przypadku liczbę całkowitą, na początku metody, tj. Przed i poza odpowiednią pętlą (-ami). Po osiągnięciu pożądanego wyniku wewnątrz pętli można przypisać odpowiednią wartość do tej wstępnie zdefiniowanej zmiennej i użyć jej do instrukcji return poza pętlą.

Ponieważ chcesz, aby metoda zwracała pierwszy wynik, gdy rInt [i] jest równe rInt [count], zaimplementowanie tylko wyżej wymienionej zmiennej nie wystarczy, ponieważ metoda zwróci ostatni wynik, gdy rInt [i] jest równe rInt [count]. Jedną z opcji jest zaimplementowanie dwóch "instrukcji przerwania", które są wywoływane, gdy mamy pożądany wynik. Tak więc metoda będzie wyglądać mniej więcej tak:

private static int oneRun(int range) {

        int finalResult = 0; // the above-mentioned variable
        int[] rInt = new int[range + 1];
        rInt[0] = generator.nextInt(range);

        for (int count = 1; count <= range; count++) {
            rInt[count] = generator.nextInt(range);
            for (int i = 0; i < count; i++) {
                if (rInt[i] == rInt[count]) {
                    finalResult = count;
                    break; // this breaks the inside loop
                }
            }
            if (finalResult == count) {
                break; // this breaks the outside loop
            }
        }
        return finalResult;
    }

2

Zgadzam się, że należy wrzucić wyjątek, w którym występuje stwierdzenie nieosiągalne. Chciałem tylko pokazać, jak ta sama metoda może to zrobić w bardziej czytelny sposób (wymagane strumienie Java 8).

private static int oneRun(int range) {
    int[] rInt = new int[range + 1];
    return IntStream
        .rangeClosed(0, range)
        .peek(i -> rInt[i] = generator.nextInt(range))
        .filter(i -> IntStream.range(0, i).anyMatch(j -> rInt[i] == rInt[j]))
        .findFirst()
        .orElseThrow(() -> new RuntimeException("Shouldn't be reached!"));
}

-1
private static int oneRun(int range) {
    int result = -1; // use this to store your result
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range && result == -1; count++) { // Run until result found.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count && result == -1; i++) { // Check for past occurence and leave after result found.
            if (rInt[i] == rInt[count]) {
                result = count;
            }
        }
    }
    return result; // return your result
}

Kod jest również nieefektywne, ponieważ będzie zrobić, wiele z result == -1kontroli, które mogą być pominięte z returnwewnątrz pętli ...
Willem Van Onsem
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.