Falta la declaración de retorno en un método no vacío compila

189

Encontré una situación en la que a un método no nulo le falta una declaración de devolución y el código aún se compila. Sé que las declaraciones después del ciclo while son inalcanzables (código muerto) y nunca se ejecutarán. Pero, ¿por qué el compilador ni siquiera advierte sobre devolver algo? ¿O por qué un lenguaje nos permitiría tener un método no vacío que tenga un bucle infinito y no devuelva nada?

public int doNotReturnAnything() {
    while(true) {
        //do something
    }
    //no return statement
}

Si agrego una declaración de interrupción (incluso una condicional) en el bucle while, el compilador se queja de los errores infames: Method does not return a valueen Eclipse y Not all code paths return a valueen Visual Studio.

public int doNotReturnAnything() {
    while(true) {
        if(mustReturn) break;
        //do something
    }
    //no return statement
}

Esto es cierto tanto para Java como para C #.

cPu1
fuente
3
Buena pregunta. Me interesaría la razón de esto.
Erik Schierboom
18
Una suposición: es un bucle infinito, ¿entonces un flujo de control de retorno es irrelevante?
Lews Therin
55
"¿Por qué un lenguaje nos permitiría tener un método no vacío que tenga un bucle infinito y no devuelva nada?" <- aunque aparentemente estúpido, la pregunta también podría revertirse: ¿por qué no se permitiría esto? ¿Es este código real, por cierto?
fge
3
Para Java, puede encontrar una buena explicación en esta respuesta .
Keppil
44
Como otros han explicado, es porque el compilador es lo suficientemente inteligente como para saber que el bucle es infinito. Sin embargo, tenga en cuenta que el compilador no solo permite que falte el retorno, sino que lo aplica porque sabe algo después de que el bucle no es accesible. Al menos en Netbeans, literalmente se quejará de unreachable statementsi hay algo después del ciclo.
Supr

Respuestas:

240

¿Por qué un lenguaje nos permitiría tener un método no vacío que tenga un bucle infinito y no devuelva nada?

La regla para los métodos no nulos es que cada ruta de código que devuelve debe devolver un valor , y esa regla se cumple en su programa: cero de cero rutas de código que devuelven devuelven un valor. La regla no es "todos los métodos no nulos deben tener una ruta de código que devuelva".

Esto le permite escribir métodos de código auxiliar como:

IEnumerator IEnumerable.GetEnumerator() 
{ 
    throw new NotImplementedException(); 
}

Ese es un método no vacío. Se tiene que ser un método no vacío con el fin de satisfacer la interfaz. Pero parece una tontería hacer que esta implementación sea ilegal porque no devuelve nada.

Que su método tenga un punto final inalcanzable debido a que goto(recuerde, a while(true)es solo una forma más agradable de escribir goto) en lugar de a throw(que es otra forma de goto) no es relevante.

¿Por qué el compilador ni siquiera advierte sobre devolver algo?

Porque el compilador no tiene buena evidencia de que el código esté equivocado. Alguien escribió while(true)y parece probable que la persona que hizo eso supiera lo que estaba haciendo.

¿Dónde puedo leer más sobre el análisis de accesibilidad en C #?

Vea mis artículos sobre el tema, aquí:

ATBG: accesibilidad de facto y de jure

Y también puede considerar leer la especificación de C #.

Eric Lippert
fuente
Entonces, ¿por qué este código da un error de tiempo de compilación? public int doNotReturnAnything() { boolean flag = true; while (flag) { //Do something } //no return } Este código tampoco tiene un punto de interrupción. Entonces, ahora cómo el compilador sabe que el código está mal.
Sandeep Poonia
@SandeepPoonia: es posible que desee leer las otras respuestas aquí ... El compilador solo puede detectar ciertas condiciones.
Daniel Hilgarth
9
@SandeepPoonia: la bandera booleana se puede cambiar en tiempo de ejecución, ya que no es una constante, por lo tanto, el bucle no es necesariamente infinito.
Schmuli
9
@SandeepPoonia: En C # la especificación dice que if, while, for, switch, etc, construcciones de ramificación que operan sobre las constantes son tratados como ramas incondicionales por el compilador. La definición exacta de expresión constante está en la especificación. Las respuestas a sus preguntas están en la especificación; Mi consejo es que lo leas.
Eric Lippert
1
Every code path that returns must return a valueLa mejor respuesta. Aborda claramente ambas preguntas. Gracias
cPu1
38

El compilador de Java es lo suficientemente inteligente como para encontrar el código inalcanzable (el código después del whilebucle)

y dado que es inalcanzable , no tiene sentido agregar una returndeclaración allí (después de whilefinalizar)

lo mismo va con condicional if

public int get() {
   if(someBoolean) {   
     return 10;
   }
   else {
     return 5;
   }
   // there is no need of say, return 11 here;
}

dado que la condición booleana someBooleansolo puede evaluar a uno trueu otro false, no es necesario proporcionar un return explícitamente después if-else, porque ese código es inalcanzable y Java no se queja de ello.

sanbhat
fuente
44
No creo que esto realmente aborde la pregunta. Esto responde por qué no necesita una returndeclaración en un código inalcanzable, pero no tiene nada que ver con por qué no necesita ninguna return declaración en el código del OP.
Bobson
Si el compilador de Java es lo suficientemente inteligente como para encontrar el código inalcanzable (el código después del bucle while), entonces por qué compila estos códigos a continuación, ambos tienen código inalcanzable pero el método con if requiere una declaración return pero el método con un while no. public int doNotReturnAnything() { if(true){ System.exit(1); } return 11; } public int doNotReturnAnything() { while(true){ System.exit(1); } return 11;// Compiler error: unreachable code }
Sandeep Poonia
@SandeepPoonia: Porque el compilador no sabe que System.exit(1)matará al programa. Solo puede detectar ciertos tipos de código inalcanzable.
Daniel Hilgarth
@Daniel Hilgarth: Ok, el compilador no sabe System.exit(1)que matará al programa, podemos usar cualquiera return statement, ahora el compilador reconoce el return statements. Y el comportamiento es el mismo, el retorno requiere if conditionpero no para while.
Sandeep Poonia
1
Buena demostración de la sección inalcanzable sin usar un caso especial comowhile(true)
Matthew
17

El compilador sabe que el whileciclo nunca dejará de ejecutarse, por lo tanto, el método nunca terminará, por lo tanto, returnno es necesaria una declaración.

Daniel Hilgarth
fuente
13

Dado que su ciclo se está ejecutando en una constante, el compilador sabe que es un ciclo infinito, lo que significa que el método nunca podría volver, de todos modos.

Si usa una variable, el compilador aplicará la regla:

Esto no compilará:

// Define other methods and classes here
public int doNotReturnAnything() {
    var x = true;

    while(x == true) {
        //do something
    }
    //no return statement - won't compile
}
Dave Bish
fuente
Pero, ¿qué pasa si "hacer algo" no implica modificar x de ninguna manera ... el compilador no es tan inteligente como para descubrirlo? Bum :(
Lews Therin
No, no lo parece.
Dave Bish
@Lews si tiene un método que está marcado como return int, pero de hecho no regresa, entonces debería estar contento de que el compilador marque esto, para que pueda marcar el método como vacío o corregirlo si no tenía la intención de hacerlo comportamiento.
MikeFHay
@MikeFHay Sí, no disputo eso.
Lews Therin
1
Puedo estar equivocado, pero algunos depuradores permiten la modificación de variables. Aquí, mientras que x no está modificado por el código y JIT lo optimizará, uno podría modificar x a falso y el método debería devolver algo (si el depurador de C # lo permite).
Maciej Piechotka
11

La especificación Java define un concepto llamado Unreachable statements. No está permitido tener una declaración inalcanzable en su código (es un error de tiempo de compilación). Ni siquiera se le permite tener una declaración de devolución después del tiempo (verdadero); declaración en Java. Una while(true);declaración hace que las siguientes declaraciones sean inalcanzables por definición, por lo tanto, no necesita una returndeclaración.

Tenga en cuenta que si bien el problema de detención es indecidible en un caso genérico, la definición de declaración inalcanzable es más estricta que simplemente detenerla. Está decidiendo casos muy específicos en los que un programa definitivamente no se detiene. Teóricamente, el compilador no puede detectar todos los bucles infinitos y las declaraciones inalcanzables, pero tiene que detectar casos específicos definidos en la especificación (por ejemplo, el while(true)caso)

Konstantin Yovkov
fuente
7

El compilador es lo suficientemente inteligente como para descubrir que su whileciclo es infinito.

Entonces el compilador no puede pensar por ti. No puedo adivinar por qué escribiste ese código. Lo mismo representa los valores de retorno de los métodos. Java no se quejará si no hace nada con los valores de retorno del método.

Entonces, para responder a su pregunta:

El compilador analiza su código y después de descubrir que ninguna ruta de ejecución conduce a la caída del final de la función, termina con OK.

Puede haber razones legítimas para un bucle infinito. Por ejemplo, muchas aplicaciones usan un bucle principal infinito. Otro ejemplo es un servidor web que puede esperar indefinidamente las solicitudes.

Adam Arold
fuente
7

En la teoría de tipos, hay algo llamado tipo inferior que es una subclase de cualquier otro tipo (!) Y se usa para indicar la no terminación, entre otras cosas. (Las excepciones pueden contar como un tipo de no terminación: no termina a través de la ruta normal).

Entonces, desde una perspectiva teórica, se puede considerar que estas declaraciones que no terminan devuelven algo del tipo Bottom, que es un subtipo de int, por lo que, después de todo, obtiene su valor de retorno desde una perspectiva de tipo. Y está perfectamente bien que no tenga ningún sentido que un tipo pueda ser una subclase de todo lo demás, incluido int, porque en realidad nunca devuelve uno.

En cualquier caso, a través de la teoría explícita de tipos o no, los compiladores (escritores de compiladores) reconocen que pedir un valor de retorno después de una declaración sin terminación es una tontería: no hay ningún caso posible en el que pueda necesitar ese valor. (Puede ser agradable que su compilador le avise cuando sabe que algo no terminará pero parece que quiere que devuelva algo. Pero es mejor dejarlo para los correctores de estilo, ya que tal vez necesite la firma de tipo que por alguna otra razón (por ejemplo, subclases) pero realmente desea la no terminación).

Rex Kerr
fuente
6

No existe una situación en la que la función pueda llegar a su fin sin devolver un valor apropiado. Por lo tanto, el compilador no tiene nada de qué quejarse.

Graham Borland
fuente
5

Visual Studio tiene el motor inteligente para detectar si ha escrito un tipo de retorno, entonces debe tener una declaración de retorno con la función / método.

Como en PHP, su tipo de devolución es verdadero si no ha devuelto nada. el compilador obtiene 1 si no se ha devuelto nada.

A partir de esto

public int doNotReturnAnything() {
    while(true) {
        //do something
    }
    //no return statement
}

El compilador sabe que, si bien la declaración en sí tiene una naturaleza infinita, no lo tenga en cuenta. y el compilador de php se volverá verdadero automáticamente si escribe una condición en expresión de while.

Pero no en el caso de VS, le devolverá un error en la pila.

Tarun Gupta
fuente
4

Su ciclo while se ejecutará para siempre y, por lo tanto, no saldrá afuera mientras; continuará ejecutándose. Por lo tanto, la parte exterior de while {} es inalcanzable y no tiene sentido escribir return o no. El compilador es lo suficientemente inteligente como para descubrir qué parte es accesible y qué parte no.

Ejemplo:

public int xyz(){
    boolean x=true;

    while(x==true){
        // do something  
    }

    // no return statement
}

El código anterior no se compilará, porque puede darse el caso de que el valor de la variable x se modifique dentro del cuerpo del ciclo while. ¡Esto hace que la parte exterior de while sea accesible! Y, por lo tanto, el compilador arrojará un error 'no se encontró ninguna declaración de retorno'.

El compilador no es lo suficientemente inteligente (o más bien vago;)) para determinar si el valor de x se modificará o no. Espero que esto aclare todo.

kunal18
fuente
77
En realidad, en este caso no se trata de que el compilador sea lo suficientemente inteligente. En C # 2.0, el compilador era lo suficientemente inteligente como para saber que int x = 1; while(x * 0 == 0) { ... }era un bucle infinito, pero la especificación dice que el compilador solo debe hacer deducciones de flujo de control cuando la expresión del bucle es constante , y la especificación define una expresión constante como que no contiene variables . El compilador, por lo tanto, era demasiado inteligente . En C # 3 hice que el compilador coincida con la especificación, y desde entonces es deliberadamente menos inteligente sobre este tipo de expresiones.
Eric Lippert
4

"¿Por qué el compilador ni siquiera advierte acerca de devolver algo? ¿O por qué un lenguaje nos permitiría tener un método no vacío que tenga un bucle infinito y no devuelva nada?".

Este código también es válido en todos los demás idiomas (¡probablemente excepto Haskell!). Porque la primera suposición es que estamos escribiendo "intencionalmente" algún código.

Y hay situaciones en las que este código puede ser totalmente válido, como si lo va a usar como hilo; o si estaba devolviendo a Task<int>, podría hacer alguna comprobación de errores en función del valor int devuelto, que no debería devolverse.

Kaveh Shahbazian
fuente
3

Puedo estar equivocado, pero algunos depuradores permiten la modificación de variables. Aquí, mientras que x no está modificado por el código y JIT lo optimizará, uno podría modificar x a falso y el método debería devolver algo (si el depurador de C # lo permite).


fuente
1

Los detalles del caso de Java para esto (que probablemente son muy similares al caso de C #) tienen que ver con la forma en que el compilador de Java determina si un método puede regresar.

Específicamente, las reglas son que un método con un tipo de retorno no debe poder completarse normalmente y en su lugar siempre debe completarse abruptamente (abruptamente aquí indicando mediante una declaración de retorno o una excepción) según JLS 8.4.7 .

Si se declara que un método tiene un tipo de retorno, se produce un error en tiempo de compilación si el cuerpo del método puede completarse normalmente. En otras palabras, un método con un tipo de retorno debe devolver solo mediante el uso de una declaración de retorno que proporciona un valor de retorno; no está permitido "dejar el extremo de su cuerpo" .

El compilador busca ver si la terminación normal es posible en función de las reglas definidas en JLS 14.21 Declaraciones inalcanzables, ya que también define las reglas para la finalización normal.

En particular, las reglas para las declaraciones inalcanzables hacen un caso especial solo para los bucles que tienen una trueexpresión constante definida :

Una instrucción while puede completarse normalmente si al menos uno de los siguientes es verdadero:

  • La declaración while es accesible y la expresión de condición no es una expresión constante (§15.28) con valor verdadero.

  • Hay una declaración de ruptura accesible que sale de la declaración while.

Entonces, si la whiledeclaración puede completarse normalmente , entonces es necesaria una declaración de retorno a continuación, ya que el código se considera accesible, y cualquier whileciclo sin una declaración de ruptura alcanzable o trueexpresión constante se considera capaz de completarse normalmente.

Estas reglas significan que su whiledeclaración con una verdadera expresión constante y sin una breakestá nunca se consideró a completar normalmente , por lo que cualquier código de abajo se nunca se consideró para ser alcanzable . El final del método está debajo del ciclo, y dado que todo lo que está debajo del ciclo es inalcanzable, también lo es el final del método y, por lo tanto, el método no puede completarse normalmente (que es lo que busca el cumplidor).

if las declaraciones, por otro lado, no tienen la exención especial con respecto a las expresiones constantes que se otorgan a los bucles.

Comparar:

// I have a compiler error!
public boolean testReturn()
{
    final boolean condition = true;

    if (condition) return true;
}

Con:

// I compile just fine!
public boolean testReturn()
{
    final boolean condition = true;

    while (condition)
    {
        return true;
    }
}

La razón de la distinción es bastante interesante, y se debe al deseo de permitir marcas de compilación condicional que no causen errores de compilación (del JLS):

Uno podría esperar que la declaración if se maneje de la siguiente manera:

  • Una instrucción if-then puede completarse normalmente si al menos uno de los siguientes es verdadero:

    • La instrucción if-then es accesible y la expresión de condición no es una expresión constante cuyo valor es verdadero.

    • La instrucción then puede completarse normalmente.

    La declaración then es accesible si la declaración if-then es accesible y la expresión de condición no es una expresión constante cuyo valor es falso.

  • Una declaración if-then-else puede completarse normalmente si la declaración then puede completarse normalmente o la declaración else puede completarse normalmente.

    • La declaración then es accesible si la declaración if-then-else es accesible y la expresión de condición no es una expresión constante cuyo valor es falso.

    • La declaración else es accesible si la declaración if-then-else es accesible y la expresión de condición no es una expresión constante cuyo valor es verdadero.

Este enfoque sería coherente con el tratamiento de otras estructuras de control. Sin embargo, para permitir que la declaración if se use convenientemente para propósitos de "compilación condicional", las reglas reales difieren.

Como ejemplo, la siguiente declaración da como resultado un error en tiempo de compilación:

while (false) { x=3; }porque la declaración x=3;no es accesible; pero el caso superficialmente similar:

if (false) { x=3; }no produce un error en tiempo de compilación. Un compilador optimizador puede darse cuenta de que la declaración x=3;nunca se ejecutará y puede optar por omitir el código para esa declaración del archivo de clase generado, pero la declaración x=3;no se considera "inalcanzable" en el sentido técnico especificado aquí.

La razón de este tratamiento diferente es permitir que los programadores definan "variables de marca" como:

static final boolean DEBUG = false; y luego escribe código como:

if (DEBUG) { x=3; } La idea es que debería ser posible cambiar el valor de DEBUG de falso a verdadero o de verdadero a falso y luego compilar el código correctamente sin otros cambios en el texto del programa.

¿Por qué la declaración de interrupción condicional da como resultado un error del compilador?

Como se cita en las reglas de accesibilidad de bucle, un bucle while también puede completarse normalmente si contiene una declaración de interrupción alcanzable. Dado que las normas para la accesibilidad de una ifde declaración a continuación cláusula de no tener la condición de la ifen consideración en absoluto, tal condicional ifde la declaración a continuación, la cláusula se considera siempre localizable.

Si breakse puede acceder a él, el código después del ciclo también se considera accesible nuevamente. Dado que no hay un código accesible que dé como resultado una terminación abrupta después del ciclo, el método se considera capaz de completarse normalmente, por lo que el compilador lo marca como un error.

Trevor Freeman
fuente