¿Cómo evito una devolución inútil en un método Java?

115

Tengo una situación en la que siempre se alcanzará la returndeclaración anidada en dos forbucles, en teoría.

El compilador no está de acuerdo y requiere una returndeclaración fuera del forciclo. Me gustaría conocer una forma elegante de optimizar este método que está más allá de mi comprensión actual, y ninguna de mis implementaciones de ruptura intentadas parece funcionar.

Attached es un método de una asignación que genera enteros aleatorios y devuelve las iteraciones recorridas hasta que se encuentra un segundo entero aleatorio, generado dentro de un rango pasado al método como un parámetro 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
}
Oliver Benning
fuente
9
Si nunca se alcanza el último elemento, puede utilizar un while(true)bucle en lugar de un bucle indexado. Esto le dice al compilador que el bucle nunca regresará.
Boris the Spider
101
llame a la función con rango como 0 (o cualquier otro número menor que 1) ( oneRun(0)) y verá que rápidamente alcanza su inalcanzablereturn
mcfedr
55
El retorno se alcanza cuando el rango suministrado es negativo. También tiene 0 validación para el rango de entrada, por lo que actualmente no lo está detectando de ninguna otra manera.
HopefullyHelpful
4
@HopefullyHelpful Esa es la verdadera respuesta, por supuesto. ¡No es un regreso inútil en absoluto!
Mr Lister
4
@HopefullyHelpful no, porque nextIntarroja una excepción para range < 0El único caso en el que se alcanza el retorno es cuandorange == 0
njzk2

Respuestas:

343

La heurística del compilador nunca le permitirá omitir la última return. Si está seguro de que nunca se alcanzará, lo reemplazaría con un throwpara aclarar la situación.

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");
}
John Kugelman
fuente
135
No solo legibilidad, sino que se asegura de que averigüe si hay algún problema con su código. Es completamente plausible que el generador tenga un error.
JollyJoker
6
Si está ABSOLUTAMENTE seguro de que su código nunca podrá llegar a ese "código inalcanzable", cree algunas pruebas unitarias para todos los casos extremos que puedan surgir (el rango es 0, el rango es -1, el rango es mínimo / máximo int). Puede que ahora sea un método privado, pero es posible que el próximo desarrollador no lo mantenga así. La forma en que maneja los valores inesperados (lanzar una excepción, devolver un valor de error, no hacer nada) depende de cómo vaya a utilizar el método. En mi experiencia, normalmente solo desea registrar un error y regresar.
Rick Ryker
22
Tal vez debería serthrow new AssertionError("\"unreachable\" code reached");
Bohemio
8
Escribiría exactamente lo que pongo en mi respuesta en un programa de producción.
John Kugelman
1
Para el ejemplo dado, hay una mejor manera de tratar que simplemente agregar un throwque nunca se alcanzará. Con demasiada frecuencia, la gente solo quiere aplicar rápidamente "una solución para gobernarlos a todos" sin pensar demasiado en el problema subyacente. Pero programar es mucho más que codificar. Especialmente cuando se trata de algoritmos. Si inspecciona (cuidadosamente) el problema dado, se dará cuenta de que puede factorizar la última iteración del forbucle externo y, por lo tanto, reemplazar el retorno "inútil" por uno útil (consulte esta respuesta ).
a_guest
36

Como señaló @BoristheSpider, puede asegurarse de que la segunda returndeclaración sea semánticamente inalcanzable:

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

Se compila y funciona bien. Y si alguna vez obtiene un ArrayIndexOutOfBoundsException, sabrá que la implementación fue semánticamente incorrecta, sin tener que arrojar nada explícitamente.

l0b0
fuente
1
Exactamente: la condición no se usa realmente para controlar el bucle, por lo que no debería estar allí. Sin embargo, escribiría esto como en su for(int count = 0; true; count++)lugar.
cmaster - reinstalar a monica
3
@cmaster: puede omitir true:for(int count = 0; ; count++) …
Holger
@Holger Ah. No estaba seguro de eso porque se trata de Java, y no he tocado ese lenguaje durante años ya que estoy mucho más en C / C ++. Entonces, cuando vi el uso de while(true), pensé que quizás Java sea un poco más estricto en este sentido. Es bueno saber que no hay necesidad de preocuparse ... En realidad, me gusta truemucho más la versión omitida : deja visualmente claro que no hay absolutamente ninguna condición para mirar :-)
cmaster - reinstalar monica
3
Preferiría no cambiar a "para". Un "while-true" es una señal obvia de que se espera que el bloque se repita incondicionalmente, a menos que y hasta que algo dentro lo rompa. Un "para" con una condición omitida no comunica con tanta claridad; podría pasarse por alto más fácilmente.
Corrodias
1
@ l0b0 Probablemente, toda la pregunta sería mejor para la revisión del código, considerando que la advertencia está relacionada con la calidad del código.
Sulthan
18

Como preguntaste sobre cómo romper dos forbucles, puedes usar una etiqueta para hacerlo (mira el ejemplo a continuación):

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;
}
David Choweller
fuente
¿No fallaría esto en la compilación porque returnValuese puede usar sin inicializar?
pts
1
Más probable. El objeto de la publicación era mostrar cómo romper ambos bucles, ya que el cartel original había preguntado sobre esto. El OP estaba seguro de que la ejecución nunca llegaría al final del método, por lo que lo que mencionó se puede arreglar inicializando returnValue en cualquier valor cuando se declara.
David Choweller
4
¿No son las etiquetas ese tipo de cosas que es mejor evitar de alguna manera?
Serverfrog
2
Creo que estás pensando en gotos.
David Choweller
4
Etiquetas en un lenguaje de programación de alto nivel (-ish). Absolutamente bárbaro ...
xDaizu
13

Mientras que una aserción es una buena solución rápida. En general, este tipo de problemas significa que su código es demasiado complicado. Cuando miro su código, es obvio que realmente no desea que una matriz contenga números anteriores. Quieres 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();

Ahora tenga en cuenta que lo que estamos devolviendo es en realidad el tamaño del conjunto. La complejidad del código ha disminuido de cuadrática a lineal y es inmediatamente más legible.

Ahora podemos darnos cuenta de que ni siquiera necesitamos ese countíndice:

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();
Sulthan
fuente
8

Como su valor de retorno se basa en la variable del ciclo externo, simplemente puede alterar la condición del ciclo externo count < rangey luego devolver este último valor (que acaba de omitir) al final de la función:

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

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

De esta forma, no es necesario introducir un código que nunca se alcanzará.

un invitado
fuente
Pero, ¿no requeriría esto romper las dos capas de bucles cuando se encuentre la coincidencia? No veo una manera de reducir a una declaración for, es necesario generar un nuevo número aleatorio y compararlo con los anteriores antes de que se genere otro.
Oliver Benning
Todavía te quedan dos bucles for anidados (acabo de omitir el segundo ...) pero el bucle exterior se ha reducido en su última iteración. El caso correspondiente a esta última iteración es manejado por separado por la última declaración de retorno. Aunque esta es una solución elegante para su ejemplo, hay escenarios en los que este enfoque se vuelve más difícil de leer; si, por ejemplo, el valor de retorno dependiera del ciclo interno, entonces, en lugar de una declaración de retorno única al final, se quedaría con un ciclo adicional (que representa el ciclo interno para la última iteración del ciclo externo).
a_guest
5

Utilice una variable temporal, por ejemplo, "resultado", y elimine el retorno interno. Cambie el bucle for por un bucle while con la condición adecuada. Para mí, siempre es más elegante tener solo un retorno como última declaración de la función.

David
fuente
Bueno, parece que el if muy interno podría ser la condición while externa. Tenga en cuenta que siempre hay un tiempo equivalente a un for (y más elegante, ya que no está planeando pasar siempre por todas las iteraciones). Esos dos bucles for anidados se pueden simplificar con seguridad. Todo ese código huele "demasiado complejo".
David
@ Solomonoff'sSecret Su afirmación es absurda. Implica que deberíamos "en general" apuntar a dos o más declaraciones de retorno. Interesante.
David
@ Solomonoff'sSecret Es una cuestión de estilos de programación preferidos, e independientemente de lo que sea genial esta semana, que fue poco convincente la semana pasada, hay ventajas definidas para tener un solo punto de salida y al final del método (solo piense en un bucle con muchos se return resultesparcieron dentro de él y luego piensan en querer hacer una verificación más en el encontrado resultantes de devolverlo, y esto es solo un ejemplo). También puede haber contras, pero una afirmación tan amplia como "En general, no hay una buena razón para tener una sola devolución" no es válida.
SantiBailors
@David Déjame reformular: no hay una razón general para tener una sola devolución. En casos particulares, pueden ser razones, pero por lo general es el culto a la carga en su peor momento.
Reintegro a Monica
1
Es una cuestión de qué hace que el código sea más legible. Si en tu cabeza dices "si es esto, devuelve lo que encontramos; de lo contrario, continúa; si no encontramos algo devuelve este otro valor". Entonces su código debería tener dos valores de retorno. Codifique siempre lo que lo haga más comprensible de inmediato para el próximo desarrollador. El compilador tiene solo un punto de retorno de un método Java, incluso si a nuestros ojos parecen dos.
Rick Ryker
3

Tal vez esto sea una indicación de que debería reescribir su código. Por ejemplo:

  1. Cree una matriz de enteros 0 .. rango-1. Establezca todos los valores en 0.
  2. Realiza un bucle. En el ciclo, genere un número aleatorio. Mire en su lista, en ese índice, para ver si el valor es 1. Si lo es, salga del ciclo. De lo contrario, establezca el valor en ese índice en 1
  3. Cuente el número de unos en la lista y devuelva ese valor.
Robert Hanson
fuente
3

Los métodos que tienen una declaración de retorno y tienen un bucle / bucles dentro de ellos siempre requieren una declaración de devolución fuera del bucle (s). Incluso si esta declaración fuera del ciclo nunca se alcanza. En tales casos, para evitar declaraciones de retorno innecesarias, puede definir una variable del tipo respectivo, un entero en su caso, al comienzo del método, es decir, antes y fuera de los bucles respectivos. Cuando se alcanza el resultado deseado dentro del ciclo, puede atribuir el valor respectivo a esta variable predefinida y usarlo para la declaración de retorno fuera del ciclo.

Como desea que su método devuelva el primer resultado cuando rInt [i] es igual a rInt [count], implementar solo la variable mencionada anteriormente no es suficiente porque el método devolverá el último resultado cuando rInt [i] es igual a rInt [count]. Una opción es implementar dos "sentencias de interrupción" que se llaman cuando tenemos el resultado deseado. Entonces, el método se verá así:

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;
    }
Lachezar
fuente
2

Estoy de acuerdo en que se debería lanzar una excepción cuando ocurra una declaración inalcanzable. Solo quería mostrar cómo el mismo método puede hacer esto de una manera más legible (se requieren secuencias de 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!"));
}
Sergey Fedorov
fuente
-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
}
bla bla
fuente
El código también es ineficiente ya que uno va a hacer una gran cantidad de result == -1controles que se pueden omitir con el returninterior del bucle ...
Willem Van Onsem