Come posso evitare un ritorno inutile in un metodo Java?


115

Ho una situazione in cui l' returnistruzione annidata in due forcicli verrà sempre raggiunta, in teoria.

Il compilatore non è d'accordo e richiede returnun'istruzione al di fuori del forciclo. Mi piacerebbe conoscere un modo elegante per ottimizzare questo metodo che sia al di là della mia attuale comprensione e nessuna delle mie tentate implementazioni di break sembra funzionare.

In allegato è un metodo da un'assegnazione che genera numeri interi casuali e restituisce le iterazioni ripetute fino a quando non viene trovato un secondo numero intero casuale, generato all'interno di un intervallo passato nel metodo come parametro 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
Se l'ultimo elemento non viene mai raggiunto, è possibile utilizzare un while(true)ciclo anziché un ciclo indicizzato. Questo dice al compilatore che il ciclo non tornerà mai più.
Boris the Spider

101
chiama la funzione con intervallo come 0 (o qualsiasi altro numero inferiore a 1) ( oneRun(0)) e vedi che raggiungi rapidamente il tuo irraggiungibilereturn
mcfedr

55
Il rendimento si raggiunge quando l'intervallo fornito è negativo. Hai anche 0 convalida per l'intervallo di input, quindi attualmente non lo stai rilevando in nessun altro modo.
spera utile il

4
@HopefullyHelpful Questa è la vera risposta, ovviamente. Non è affatto un inutile ritorno!
Mr Lister

4
@HopefullyHelpful no, perché nextIntgenera un'eccezione per range < 0L'unico caso in cui viene raggiunto il ritorno è quandorange == 0
njzk2,

Risposte:


343

L'euristica del compilatore non ti consentirà mai di omettere l'ultimo return. Se sei sicuro che non verrà mai raggiunto, lo sostituisco con un throwper rendere chiara la situazione.

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
Non solo leggibilità, ma ti assicura di scoprire se c'è qualcosa di sbagliato nel tuo codice. È del tutto plausibile che il generatore possa avere un bug.
JollyJoker

6
Se sei ASSOLUTAMENTE sicuro che il tuo codice non potrà mai arrivare a quel "codice irraggiungibile" allora crea alcuni unit test per tutti i casi limite che potrebbero sorgere (range è 0, range è -1, range è min / max int). Potrebbe essere un metodo privato ora, ma il prossimo sviluppatore potrebbe non mantenerlo in questo modo. Il modo in cui gestisci i valori imprevisti (lanciare un'eccezione, restituire un valore di errore, non fare nulla) dipende da come utilizzerai il metodo. Nella mia esperienza, di solito si desidera solo registrare un errore e tornare.
Rick Ryker

22
Forse dovrebbe esserethrow new AssertionError("\"unreachable\" code reached");
Bohemian

8
Scriverei esattamente quello che ho inserito nella mia risposta in un programma di produzione.
John Kugelman

1
Per l'esempio fornito c'è un modo migliore per affrontarlo che aggiungere semplicemente un messaggio throwche non sarà mai raggiunto. Troppo spesso le persone vogliono solo applicare rapidamente "un'unica soluzione per domarle tutte" senza pensare troppo al problema sottostante. Ma la programmazione è molto più della semplice codifica. Soprattutto quando si tratta di algoritmi. Se esamini (attentamente) il problema dato, ti renderai conto che puoi escludere l'ultima iterazione del forciclo esterno e quindi sostituire il ritorno "inutile" con uno utile (vedi questa risposta ).
a_guest

36

Come ha sottolineato @BoristheSpider, puoi assicurarti che la seconda returnaffermazione sia semanticamente irraggiungibile:

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++;
    }
}

Compila e funziona bene. E se mai ArrayIndexOutOfBoundsExceptionne avrai una , saprai che l'implementazione era semanticamente sbagliata, senza dover lanciare esplicitamente nulla.


1
Esatto: la condizione non viene effettivamente utilizzata per controllare il ciclo, quindi non dovrebbe essere presente. Tuttavia, lo scriverei come for(int count = 0; true; count++)invece.
cmaster - ripristina monica il

3
@cmaster: puoi omettere true:for(int count = 0; ; count++) …
Holger

@Holger Ah. Non ne ero sicuro perché si tratta di Java, e non tocco quel linguaggio da anni da quando sono molto più interessato a C / C ++. Quindi, quando ho visto l'uso di while(true), ho pensato che forse java è un po 'più rigoroso in questo senso. Bello sapere che non c'era motivo di preoccuparsi ... In realtà, mi piace truemolto di più la versione omessa : rende visivamente chiaro che non ci sono assolutamente condizioni da guardare :-)
cmaster - reinstate monica

3
Preferisco non passare a "per". Un "while-true" è un flag ovvio che ci si aspetta che il blocco esegua un ciclo incondizionato, a meno che e fino a quando qualcosa all'interno non lo rompe. Un "per" con una condizione omessa non comunica così chiaramente; potrebbe essere trascurato più facilmente.
Corrodias

1
@ l0b0 L'intera domanda sarebbe probabilmente migliore per la revisione del codice considerando che l'avvertenza è correlata alla qualità del codice.
Sulthan

18

Dato che hai chiesto di spezzare due forloop, puoi usare un'etichetta per farlo (vedi l'esempio sotto):

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;
}

Questo non fallirebbe la compilazione perché returnValuepuò essere utilizzato non inizializzato?
punti

1
Più probabilmente. Lo scopo del post era mostrare come uscire da entrambi i loop, come il poster originale aveva chiesto al riguardo. L'OP era sicuro che l'esecuzione non sarebbe mai arrivata alla fine del metodo, quindi ciò che hai sollevato può essere corretto inizializzando returnValue su qualsiasi valore quando viene dichiarato.
David Choweller

4
Le etichette non sono quel tipo di cose che è meglio evitare con qualsiasi mezzo?
Serverfrog

2
Credo che tu stia pensando a gotos.
David Choweller

4
Etichette in un linguaggio di programmazione di alto livello (-ish). Assolutamente barbaro ...
xDaizu

13

Mentre un'affermazione è una buona soluzione rapida. In generale questo tipo di problemi significa che il tuo codice è troppo complicato. Quando guardo il tuo codice, è ovvio che non vuoi davvero che un array contenga i numeri precedenti. Vuoi un 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();

Ora nota che ciò che stiamo restituendo è in realtà la dimensione del set. La complessità del codice è diminuita da quadratica a lineare ed è immediatamente più leggibile.

Ora possiamo renderci conto che non abbiamo nemmeno bisogno di quell'indice count:

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

Poiché il valore di ritorno si basa sulla variabile del ciclo esterno, potresti semplicemente modificare la condizione del ciclo esterno count < rangee quindi restituire quest'ultimo valore (che hai appena omesso) alla fine della funzione:

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

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

In questo modo non è necessario introdurre codice che non verrà mai raggiunto.


Ma questo non richiederebbe una rottura dei due strati di loop quando viene trovata la corrispondenza? Non vedo un modo per ridurre a una dichiarazione for, è necessario generare un nuovo numero casuale e confrontarlo con quelli precedenti prima che ne venga generato un altro.
Oliver Benning

Ti rimangono ancora due cicli for annidati (ho appena omesso il secondo ...) ma il ciclo esterno è stato ridotto dalla sua ultima iterazione. Il caso corrispondente a quest'ultima iterazione viene gestito separatamente dall'ultima istruzione return. Sebbene questa sia una soluzione elegante per il tuo esempio, ci sono scenari in cui questo approccio diventa più difficile da leggere; se per esempio il valore restituito dipendesse dal ciclo interno, allora, invece di una singola istruzione return alla fine, rimarrebbe con un ciclo aggiuntivo (che rappresenta il ciclo interno per l'ultima iterazione del ciclo esterno).
a_guest

5

Usa una variabile temporanea, ad esempio "risultato", e rimuovi il ritorno interno. Modificare il ciclo for per un ciclo while con la condizione corretta. Per me è sempre più elegante avere un solo ritorno come ultima istruzione della funzione.


Bene, sembra proprio la condizione interiore se potrebbe essere la condizione while esterna. Tieni presente che c'è sempre un tempo equivalente a un for (e più elegante dato che non hai intenzione di passare sempre attraverso tutte le iterazioni). Quei due cicli for annidati possono essere semplificati di sicuro. Tutto quel codice ha un odore "troppo complesso".
David

@ Solomonoff'sSecret La tua affermazione è assurda. Implica che dovremmo puntare "in generale" a due o più dichiarazioni di ritorno. Interessante.
David

@ Solomonoff'sSecret È una questione di stili di programmazione preferiti, e indipendentemente da ciò che è bello questa settimana che è stato noioso la scorsa settimana, ci sono dei pro nell'avere un solo punto di uscita, e alla fine del metodo (pensa solo a un ciclo con molti si sono return resultcosparsi al suo interno e poi pensano di voler ora fare un ulteriore controllo sul ritrovato resultprima di restituirlo, e questo è solo un esempio). Ci possono essere anche degli svantaggi, ma un'affermazione così ampia come "In generale, non c'è una buona ragione per avere un solo ritorno" non regge.
SantiBailors

@David Fammi riformulare: non c'è motivo generale per avere un solo ritorno. In casi particolari le ragioni potrebbero essere, ma in genere è il culto del carico al suo peggio.
Ripristina Monica il

1
È una questione di ciò che rende il codice più leggibile. Se nella tua testa dici "se è questo, restituisci quello che abbiamo trovato; altrimenti continua; se non abbiamo trovato qualcosa restituisci quest'altro valore". Quindi il tuo codice dovrebbe avere due valori di ritorno. Codifica sempre ciò che lo rende più immediatamente comprensibile per lo sviluppatore successivo. Il compilatore ha un solo punto di ritorno da un metodo Java anche se ai nostri occhi sembra due.
Rick Ryker

3

Forse questa è un'indicazione che dovresti riscrivere il tuo codice. Per esempio:

  1. Crea un array di numeri interi 0 .. range-1. Imposta tutti i valori su 0.
  2. Esegui un loop. Nel ciclo, genera un numero casuale. Guarda nella tua lista, in quell'indice, per vedere se il valore è 1 Se lo è, esci dal ciclo. Altrimenti, imposta il valore di quell'indice su 1
  3. Contare il numero di 1 nell'elenco e restituire quel valore.

3

I metodi che hanno un'istruzione return e hanno un ciclo / cicli al loro interno richiedono sempre un'istruzione return al di fuori dei cicli. Anche se questa istruzione al di fuori del ciclo non viene mai raggiunta. In questi casi, per evitare inutili dichiarazioni di ritorno, potresti definire una variabile del rispettivo tipo, un intero nel tuo caso, all'inizio del metodo, cioè prima e al di fuori dei rispettivi cicli. Quando viene raggiunto il risultato desiderato all'interno del ciclo, è possibile attribuire il rispettivo valore a questa variabile predefinita e utilizzarlo per l'istruzione return al di fuori del ciclo.

Poiché vuoi che il tuo metodo restituisca il primo risultato quando rInt [i] è uguale a rInt [count], implementare solo la variabile sopra menzionata non è sufficiente perché il metodo restituirà l'ultimo risultato quando rInt [i] è uguale a rInt [count]. Una delle opzioni è implementare due "istruzioni break" che vengono chiamate quando si ha il risultato desiderato. Quindi, il metodo sarà simile a questo:

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

Sono d'accordo sul fatto che si dovrebbe lanciare un'eccezione dove si verifica un'istruzione irraggiungibile. Volevo solo mostrare come lo stesso metodo può farlo in modo più leggibile (java 8 stream richiesti).

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
}

Il codice è anche inefficiente in quanto si farà un sacco di result == -1controlli che possono essere omessi con l' returnall'interno del ciclo ...
Willem Van Onsem
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.