A declaração de retorno ausente em um método não nulo compila

189

Eu encontrei uma situação em que um método não nulo está faltando uma instrução de retorno e o código ainda é compilado. Eu sei que as instruções após o loop while são inacessíveis (código morto) e nunca seriam executadas. Mas por que o compilador nem avisa sobre o retorno de algo? Ou por que uma linguagem nos permitiria ter um método não vazio com um loop infinito e não retornando nada?

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

Se eu adicionar uma instrução break (mesmo que condicional) no loop while, o compilador reclamará dos erros infames: Method does not return a valueno Eclipse e Not all code paths return a valueno Visual Studio.

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

Isso vale para Java e C #.

cPu1
fonte
3
Boa pergunta. Eu estaria interessado no motivo disso.
Erik Schierboom
18
Um palpite: é um loop infinito; portanto, um fluxo de controle de retorno é irrelevante?
Lews Therin
5
"por que uma linguagem nos permite ter um método não vazio com um loop infinito e não retornando nada?" <- embora pareça estúpida, a questão também pode ser revertida: por que isso não seria permitido? A propósito, esse código é real?
Fge 28/05
3
Para Java, você pode encontrar uma boa explicação nesta resposta .
Keppil
4
Como outros explicaram, é porque o compilador é inteligente o suficiente para saber que o loop é infinito. Observe que o compilador não apenas permite que o retorno esteja ausente, como também o impõe porque ele sabe tudo depois que o loop está inacessível. Pelo menos no Netbeans, ele literalmente reclamará unreachable statementse houver algo após o loop.
Supr

Respostas:

240

Por que uma linguagem nos permite ter um método não nulo com um loop infinito e sem retornar nada?

A regra para métodos não nulos é todo caminho de código retornado deve retornar um valor , e essa regra é satisfeita no seu programa: zero dos zero caminhos de código retornados retornam um valor. A regra não é "todo método não nulo deve ter um caminho de código que retorne".

Isso permite que você escreva métodos stub como:

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

Esse é um método não vazio. Ele tem de ser um método não-nula, a fim de satisfazer a interface. Mas parece bobagem tornar essa implementação ilegal porque ela não retorna nada.

O fato de seu método ter um ponto final inacessível por causa de um goto(lembre-se, a while(true)é apenas uma maneira mais agradável de escrever goto) em vez de um throw(que é outra forma de goto) não é relevante.

Por que o compilador nem avisa sobre o retorno de algo?

Porque o compilador não tem boas evidências de que o código esteja errado. Alguém escreveu while(true)e parece provável que a pessoa que fez isso sabia o que estava fazendo.

Onde posso ler mais sobre a análise de acessibilidade em C #?

Veja meus artigos sobre o assunto, aqui:

ATBG: acessibilidade de fato e de jure

E você também pode considerar ler a especificação C #.

Eric Lippert
fonte
Portanto, por que esse código fornece erro de tempo de compilação: public int doNotReturnAnything() { boolean flag = true; while (flag) { //Do something } //no return } esse código também não possui um ponto de interrupção. Então agora como o compilador sabe que o código está errado.
Sandeep Poonia
@ SandeepPoonia: Você pode querer ler as outras respostas aqui ... O compilador pode detectar apenas determinadas condições.
Daniel Hilgarth 29/05
9
@ SandeepPoonia: o sinalizador booleano pode ser alterado em tempo de execução, uma vez que não é uma const, portanto o loop não é necessariamente infinito.
Schmuli
9
@SandeepPoonia: Em C # a especificação diz que if, while, for, switch, etc, as construções de ramificação que operam sobre as constantes são tratados como desvios incondicionais pelo compilador. A definição exata de expressão constante está na especificação. As respostas para suas perguntas estão na especificação; meu conselho é que você leia.
precisa
1
Every code path that returns must return a valueMelhor resposta. Aborda claramente as duas questões. Obrigado
cPu1
38

O compilador Java é inteligente o suficiente para encontrar o código inacessível (o código após o whileloop)

e, como é inacessível , não faz sentido adicionar uma returndeclaração lá (após o whiletérmino)

o mesmo acontece com condicional if

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

como a condição booleana someBooleansó pode ser avaliada como uma trueou outra false, não há necessidade de fornecer uma após return explicitamenteif-else , porque esse código é inacessível e o Java não se queixa.

Sanbhat
fonte
4
Eu não acho que isso realmente resolva a questão. Isso responde por que você não precisa de uma returndeclaração no código inacessível, mas não tem nada a ver com o motivo pelo qual você não precisa de nenhuma return declaração no código do OP.
Bobson 28/05
Se o compilador Java for inteligente o suficiente para encontrar o código inacessível (o código depois do loop while), então por que esses códigos abaixo são compilados, ambos têm código inacessível, mas o método com if requer uma declaração de retorno, mas o método com um tempo não. 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 o compilador não sabe que System.exit(1)irá matar o programa. Ele só pode detectar certos tipos de código inacessível.
Daniel Hilgarth
@ Daniel Hilgarth: Ok, o compilador não sabe System.exit(1)que matará o programa, podemos usar qualquer um return statement, agora o compilador reconhece o return statements. E o comportamento é o mesmo, o retorno requer, if conditionmas não para while.
Sandeep Poonia
1
Boa demonstração da seção inacessível sem o uso de um caso especial comowhile(true)
Mateus
17

O compilador sabe que o whileloop nunca irá parar de executar; portanto, o método nunca será concluído; portanto, uma returninstrução não é necessária.

Daniel Hilgarth
fonte
13

Dado que seu loop está sendo executado em uma constante - o compilador sabe que é um loop infinito - o que significa que o método nunca poderia retornar.

Se você usar uma variável - o compilador aplicará a regra:

Isso não será compilado:

// 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
fonte
Mas e se "fazer alguma coisa" não envolver a modificação de x de forma alguma .. o compilador não é tão inteligente para descobrir isso?
Lews Therin
Não - não parece.
quer
@Lews se você tiver um método que está marcado como retornando int, mas na verdade não retorna, então você deve estar feliz que o compilador sinalize isso, para que você possa marcar o método como nulo ou corrigi-lo se não pretender isso comportamento.
MikeFHay
@ MikeFHay Sim, sem contestar isso.
Lews Therin
1
Posso estar errado, mas alguns depuradores permitem a modificação de variáveis. Aqui, enquanto x não é modificado pelo código e será otimizado pelo JIT, pode-se modificar x para false e o método deve retornar algo (se isso for permitido pelo depurador de C #).
Maciej Piechotka 28/05
11

A especificação Java define um conceito chamado Unreachable statements. Você não tem permissão para ter uma instrução inacessível no seu código (é um erro de tempo de compilação). Você não tem permissão para ter uma declaração de retorno depois de um tempo (true); declaração em Java. Uma while(true);instrução torna as seguintes instruções inacessíveis por definição; portanto, você não precisa de umreturn instrução.

Observe que, embora o problema de parada seja indecidível em casos genéricos, a definição de Instrução inacessível é mais rigorosa do que apenas parar. Está decidindo casos muito específicos em que um programa definitivamente não é interrompido. Teoricamente, o compilador não é capaz de detectar todos os loops infinitos e instruções inacessíveis, mas precisa detectar casos específicos definidos na especificação (por exemplo, o while(true)caso)

Konstantin Yovkov
fonte
7

O compilador é inteligente o suficiente para descobrir que seu while loop é infinito.

Portanto, o compilador não pode pensar em você. Não dá para adivinhar por que você escreveu esse código. O mesmo vale para os valores de retorno dos métodos. Java não vai reclamar se você não fizer nada com os valores de retorno do método.

Então, para responder sua pergunta:

O compilador analisa seu código e, depois de descobrir que nenhum caminho de execução leva a cair no final da função, ele termina com OK.

Pode haver razões legítimas para um loop infinito. Por exemplo, muitos aplicativos usam um loop principal infinito. Outro exemplo é um servidor web que pode esperar indefinidamente por solicitações.

Adam Arold
fonte
7

Na teoria dos tipos, existe algo chamado tipo inferior que é uma subclasse de qualquer outro tipo (!) E é usado para indicar não terminação, entre outras coisas. (Exceções podem contar como um tipo de não-rescisão - você não termina pelo caminho normal.)

Portanto, de uma perspectiva teórica, pode-se considerar que essas declarações que não terminam retornam algo do tipo Bottom, que é um subtipo de int, para que você (tipo de) obtenha seu valor de retorno, afinal, de uma perspectiva de tipo. E é perfeitamente aceitável que não faça sentido que um tipo possa ser uma subclasse de todo o resto, incluindo int, porque você nunca retorna um.

De qualquer forma, por meio da teoria explícita dos tipos ou não, os compiladores (escritores do compilador) reconhecem que solicitar um valor de retorno após uma declaração não-terminante é tolo: não há um caso possível em que você possa precisar desse valor. (Pode ser bom ter o seu compilador avisando quando algo não terminar, mas parece que você deseja retornar algo. Mas isso é melhor para os verificadores de estilo a la lint, pois talvez você precise da assinatura de tipo que por algum outro motivo (por exemplo, subclassificação), mas você realmente deseja não rescisão.)

Rex Kerr
fonte
6

Não há situação em que a função possa chegar ao fim sem retornar um valor apropriado. Portanto, não há nada para o compilador reclamar.

Graham Borland
fonte
5

O Visual Studio possui o mecanismo inteligente para detectar se você digitou um tipo de retorno, e deve ter uma instrução de retorno na função / método

Como no PHP Seu tipo de retorno é verdadeiro se você não retornou nada. compilador obter 1 se nada tiver retornado.

A partir deste

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

O compilador sabe que a declaração while possui uma natureza infinita, portanto, não deve ser considerada. O compilador php será automaticamente verdadeiro se você escrever uma condição na expressão while.

Mas não no caso do VS, ele retornará um erro na pilha.

Tarun Gupta
fonte
4

Seu loop while funcionará para sempre e, portanto, não sairá enquanto; continuará sendo executado. Portanto, a parte externa de while {} está inacessível e não faz sentido escrever ou não retornar. O compilador é inteligente o suficiente para descobrir qual parte é alcançável e qual não é.

Exemplo:

public int xyz(){
    boolean x=true;

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

    // no return statement
}

O código acima não será compilado, porque pode haver um caso em que o valor da variável x seja modificado dentro do corpo do loop while. Portanto, isso torna a parte externa do loop while acessível! E, portanto, o compilador lançará um erro 'nenhuma declaração de retorno encontrada'.

O compilador não é inteligente o suficiente (ou melhor, preguiçoso;)) para descobrir se o valor de x será modificado ou não. Espero que isso limpe tudo.

kunal18
fonte
7
Na verdade, não é uma questão de o compilador ser inteligente o suficiente. No C # 2.0, o compilador era inteligente o suficiente para saber que int x = 1; while(x * 0 == 0) { ... }era um loop infinito, mas a especificação diz que o compilador deve fazer deduções de fluxo de controle apenas quando a expressão do loop é constante , e a especificação define uma expressão constante como não contendo variáveis . O compilador era, portanto, muito inteligente . No C # 3, fiz o compilador corresponder à especificação e, desde então, é deliberadamente menos inteligente sobre esse tipo de expressão.
Eric Lippert 28/05
4

"Por que o compilador nem avisa sobre o retorno de algo? Ou por que uma linguagem nos permite ter um método não vazio com um loop infinito e sem retornar nada?".

Este código também é válido em todos os outros idiomas (provavelmente exceto Haskell!). Como a primeira suposição é que estamos "intencionalmente" escrevendo algum código.

E há situações em que esse código pode ser totalmente válido, como se você fosse usá-lo como um encadeamento; ou se estava retornando a Task<int>, você poderia fazer alguma verificação de erro com base no valor int retornado - que não deve ser retornado.

Kaveh Shahbazian
fonte
3

Posso estar errado, mas alguns depuradores permitem a modificação de variáveis. Aqui, enquanto x não é modificado pelo código e será otimizado pelo JIT, pode-se modificar x para false e o método deve retornar algo (se isso for permitido pelo depurador de C #).


fonte
1

As especificidades do caso Java para isso (que provavelmente são muito semelhantes ao caso C #) têm a ver com a forma como o compilador Java determina se um método pode retornar.

Especificamente, as regras são que um método com um tipo de retorno não deve ser capaz de concluir normalmente e deve sempre ser concluído abruptamente (abruptamente aqui indicando por meio de uma declaração de retorno ou uma exceção) de acordo com o JLS 8.4.7 .

Se um método for declarado com um tipo de retorno, ocorrerá um erro em tempo de compilação se o corpo do método puder ser concluído normalmente. Em outras palavras, um método com um tipo de retorno deve retornar apenas usando uma instrução de retorno que forneça um retorno de valor; não é permitido "deixar cair o final do corpo" .

O compilador verifica se a terminação normal é possível com base nas regras definidas em Instruções inacessíveis do JLS 14.21 , pois também define as regras para a conclusão normal.

Notavelmente, as regras para instruções inacessíveis formam um caso especial apenas para loops que possuem uma trueexpressão constante definida :

Uma instrução while pode ser concluída normalmente se pelo menos um dos seguintes for verdadeiro:

  • A instrução while é alcançável e a expressão da condição não é uma expressão constante (§15.28) com o valor true.

  • Há uma instrução de quebra acessível que sai da instrução while.

Portanto, se a whileinstrução puder ser concluída normalmente , será necessária uma instrução de retorno abaixo dela, pois o código é considerado acessível e qualquer whileloop sem uma instrução de quebra acessível ou constantetrue expressão é considerado capaz de ser concluído normalmente.

Essas regras significam que sua whiledeclaração com uma expressão verdadeira constante e sem a nuncabreak é considerada concluída normalmente e, portanto, qualquer código abaixo dela nunca é considerado alcançável . O final do método está abaixo do loop, e como tudo abaixo do loop é inacessível, o mesmo ocorre com o final do método e, portanto, o método não pode ser concluído normalmente normalmente (que é o que o complier procura).

if as instruções, por outro lado, não têm a isenção especial em relação às expressões constantes que são oferecidas aos loops.

Comparar:

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

    if (condition) return true;
}

Com:

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

    while (condition)
    {
        return true;
    }
}

O motivo da distinção é bastante interessante e se deve ao desejo de permitir sinalizadores de compilação condicionais que não causam erros do compilador (do JLS):

Pode-se esperar que a instrução if seja tratada da seguinte maneira:

  • Uma instrução if-then pode ser concluída normalmente se pelo menos uma das seguintes opções for verdadeira:

    • A instrução if-then é alcançável e a expressão da condição não é uma expressão constante cujo valor é verdadeiro.

    • A declaração then pode ser concluída normalmente.

    A instrução then é alcançável se a instrução if-then é alcançável e a expressão de condição não é uma expressão constante cujo valor é falso.

  • Uma instrução if-then-else pode ser concluída normalmente se a instrução then pode ser concluída normalmente ou a instrução else pode ser concluída normalmente.

    • A instrução then é alcançável se a instrução if-then-else for alcançável e a expressão da condição não for uma expressão constante cujo valor for falso.

    • A instrução else é alcançável se a instrução if-then-else for alcançável e a expressão da condição não for uma expressão constante cujo valor for verdadeiro.

Essa abordagem seria consistente com o tratamento de outras estruturas de controle. No entanto, para permitir que a instrução if seja usada convenientemente para fins de "compilação condicional", as regras reais diferem.

Como exemplo, a seguinte instrução resulta em um erro em tempo de compilação:

while (false) { x=3; }porque a afirmação x=3;não é alcançável; mas o caso superficialmente semelhante:

if (false) { x=3; }não resulta em um erro em tempo de compilação. Um compilador otimizador pode perceber que a instrução x=3;nunca será executada e pode optar por omitir o código dessa instrução do arquivo de classe gerado, mas a instruçãox=3; não é considerada "inacessível" no sentido técnico especificado aqui.

A lógica desse tratamento diferente é permitir que os programadores definam "variáveis ​​de flag", como:

static final boolean DEBUG = false; e escreva um código como:

if (DEBUG) { x=3; } A idéia é que seja possível alterar o valor de DEBUG de falso para verdadeiro ou de verdadeiro para falso e compilar o código corretamente, sem outras alterações no texto do programa.

Por que a instrução de interrupção condicional resulta em um erro do compilador?

Conforme citado nas regras de alcançabilidade do loop, um loop while também pode ser concluído normalmente se contiver uma instrução de interrupção acessível. Como as regras para a acessibilidade da cláusula then de uma ifinstrução não levam em consideração a condição da condição , essa declaração condicional é entãoifif cláusula cláusula é sempre considerada alcançável.

Se breakestiver acessível, o código após o loop será novamente considerado acessível. Como não há código acessível que resulte em término abrupto após o loop, o método é considerado capaz de ser concluído normalmente e, portanto, o compilador o sinaliza como erro.

Trevor Freeman
fonte