非void方法中缺少的return语句会编译


189

我遇到非空方法缺少return语句且代码仍可编译的情况。我知道while循环之后的语句是不可达的(死代码)并且永远不会执行。但是,为什么编译器甚至不警告返回内容?还是为什么一种语言允许我们使用具有无限循环且不返回任何内容的非空方法?

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

如果我在while循环中添加break语句(甚至是条件语句),则编译器会抱怨臭名昭著的错误:Method does not return a value在Eclipse和Not all code paths return a valueVisual Studio中。

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

Java和C#都是如此。


3
好问题。我会对这个原因感兴趣。
Erik Schierboom

18
一个猜测:这是一个无限循环,所以返回控制流程不相关吗?
Lews Therin

5
“为什么一种语言允许我们使用具有无限循环且不返回任何东西的非空方法?” <-虽然看似很愚蠢,但问题也可以反过来:为什么不允许这样做?顺便说一下,这是实际的代码吗?
fge

3
对于Java,您可以在此答案中找到不错的解释。
开普勒

4
就像其他人解释的那样,这是因为编译器足够聪明,可以知道循环是无限的。请注意,尽管编译器不仅允许缺少返回值,而且会强制执行该返回值,因为在循环不可达之后,它知道任何信息。至少在Netbeans中,它实际上会抱怨循环后unreachable statement是否还有任何内容
SUPR

Answers:


240

为什么一种语言允许我们使用具有无限循环且不返回任何内容的非空方法?

非无效方法的规则是,返回的每个代码路径都必须返回一个值,并且该规则在您的程序中得到满足:返回的零个代码路径中的零个确实返回一个值。规则不是“每个非void方法都必须具有返回的代码路径”。

这使您可以编写如下的存根方法:

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

那是一种非无效的方法。为了满足接口,它必须是非无效方法。但是使该实现非法是很愚蠢的,因为它不返回任何东西。

由于goto(记住,a while(true)只是一种更舒适的书写方式goto)而不是throw(是的另一种形式goto),因此您的方法具有无法到达的终点是无关紧要的。

为什么编译器甚至不警告返回某些东西?

因为编译器没有很好的证据证明代码是错误的。有人写信while(true),看来这样做的人很可能知道他们在做什么。

在哪里可以阅读有关C#中的可达性分析的更多信息?

在这里查看我关于该主题的文章:

ATBG:事实上和法律上的可及性

您也可以考虑阅读C#规范。


那么为什么此代码会给出编译时错误:public int doNotReturnAnything() { boolean flag = true; while (flag) { //Do something } //no return } 此代码也没有断点。因此,现在编译器如何知道代码是错误的。
Sandeep Poonia

@SandeepPoonia:您可能想在这里阅读其他答案...编译器只能检测某些条件。
Daniel Hilgarth

9
@SandeepPoonia:布尔标志可以在运行时更改,因为它不是const,因此循环不一定是无限的。
舒米利

9
@SandeepPoonia:在C#说明书说ifwhileforswitch,等等,这对操作分支构建常量如由编译器无条件分支处理。常量表达式的确切定义在规范中。您的问题的答案在规范中。我的建议是您阅读它。
埃里克·利珀特

1
Every code path that returns must return a value最佳答案。清楚地解决了两个问题。谢谢
cPu1

38

Java编译器足够聪明,可以找到无法访问的代码(while循环后的代码)

并且由于其不可访问因此没有必要return该处添加语句(while结束后)

条件也一样 if

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

由于布尔条件someBoolean只能计算为truefalse,因此无需提供return 显式的 after if-else,因为该代码不可访问,并且Java不会对此进行抱怨。


4
我认为这不能真正解决问题。这回答了为什么您不需要用不return可达代码编写的语句,却与为什么您不需要OP代码中的任何 return语句无关。
Bobson

如果Java编译器足够聪明,可以找到无法访问的代码(while循环之后的代码),那么为什么下面的这些代码可以编译,它们都具有无法访问的代码,但是if的方法需要return语句,而while的方法则不需要。 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:因为编译器不知道System.exit(1)会杀死程序。它只能检测某些类型的不可达代码。
丹尼尔·希尔加斯

@Daniel Hilgarth:好的编译器不知道System.exit(1)会杀死该程序,我们可以使用任何程序return statement,现在编译器可以识别了return statements。与行为相同,返回要求为,if condition但不是while
Sandeep Poonia

1
在不使用特殊情况的情况下很好地演示了无法到达的部分,例如while(true)
Matthew

17

编译器知道while循环将永远不会停止执行,因此该方法将永远不会完成,因此return不需要声明。


13

给定您的循环是在常量上执行的-编译器知道这是一个无限循环-意味着该方法永远都不会返回。

如果使用变量-编译器将强制执行以下规则:

这不会编译:

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

    while(x == true) {
        //do something
    }
    //no return statement - won't compile
}

但是,如果“做某事”不涉及以任何方式修改x怎么办。Bum :(
Lews Therin

不-看起来不像。
Dave Bish

@Lews如果您有一个标记为返回int的方法,但实际上没有返回,那么您应该为编译器将其标记为高兴而感到高兴,因此您可以将该方法标记为void,或者如果您不打算这样做则将其修复。行为。
MikeFHay

@MikeFHay是的,没有争议。
Lews Therin 2013年

1
我可能是错的,但某些调试器允许修改变量。在这里,虽然x不会被代码修改,而JIT会对其进行优化,但有人可能会将x修改为false,并且方法应该返回某些内容(如果C#调试器允许这样做)。
Maciej Piechotka

11

Java规范定义了一个称为的概念Unreachable statements。您不得在代码中包含无法访问的语句(这是编译时错误)。while(true)之后甚至不允许您使用return语句;Java中的语句。一个while(true);语句使下面的语句不可达的定义,所以你并不需要一个return说法。

请注意,尽管在一般情况下无法确定暂停问题,但Unreachable Statement的定义比仅暂停更严格。它决定了程序绝对不会停止的非常特殊的情况。理论上,编译器无法检测所有无限循环和不可达的语句,但必须检测规范中定义的特定情况(例如while(true)case)


7

编译器足够聪明,可以发现您的while循环是无限的。

因此,编译器无法为您考虑。它无法猜测您为何编写该代码。相同代表方法的返回值。如果您对方法的返回值不做任何事情,Java不会抱怨。

因此,回答您的问题:

编译器会分析您的代码,并在发现没有执行路径会导致函数结束时退出,然后按OK结束。

无限循环可能有正当的理由。例如,许多应用程序使用无限的主循环。另一个示例是可以无限期等待请求的Web服务器。


7

在类型理论中,有一种称为底部类型的东西,它是所有其他类型(!)的子类,用于指示除其他外的非终止。(异常可以算作非终止的一种,您不能通过常规路径终止。)

因此,从理论上讲,可以将这些非终止的语句视为返回某种Bottom类型的东西,它是int的子类型,因此您(确实)确实从类型角度获得了返回值。完全可以,一个类型可以是包括int在内的所有其他内容的子类是毫无意义的,因为您实际上从未返回过一个。

无论如何,无论是否通过显式类型理论,编译器(编译器作者)都认识到在非终止语句之后请求返回值是愚蠢的:在任何情况下都不需要该值。(最好是让编译器在知道某些内容不会终止时发出警告,但看起来像您希望它返回某些内容。但这最好留给样式检查器使用,因为也许您需要使用类型签名出于某些其他原因(例如子类化),但您确实希望不终止。)



5

Visual Studio具有智能引擎来检测您是否键入了返回类型,然后它应该在函数/方法中带有return语句。

与PHP一样,如果您未返回任何内容,则返回类型为true。如果什么都没有返回,则编译器将得到1。

截至此

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

编译器知道while语句本身具有无限性质,因此不要考虑它。如果您在while表达式中编写条件,php编译器将自动为true。

但是对于VS则不是,它将在堆栈中返回错误。


4

您的while循环将永远运行,因此一会儿就不会出现。它将继续执行。因此,while {}的外部部分是无法访问的,编写返回与否没有任何意义。编译器足够智能,可以确定哪些部分可以访问,哪些部分不可以访问。

例:

public int xyz(){
    boolean x=true;

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

    // no return statement
}

上面的代码无法编译,因为可能会在while循环体内修改变量x的值。因此,这使得while循环的外部部分可以访问!因此,编译器将抛出错误“未找到返回语句”。

编译器不够智能(或者比较懒惰;)),无法确定x的值是否会被修改。希望这能清除一切。


7
在这种情况下,实际上并不是编译器足够聪明的问题。在C#2.0中,编译器非常聪明,足以知道这int x = 1; while(x * 0 == 0) { ... }是一个无限循环,但是规范指出,仅当循环表达式为常数时,编译器才应进行控制流推导,并且规范将常量表达式定义为不包含变量。因此,编译器太聪明了。在C#3中,我使编译器与规范匹配,从那时起,对于这类表达式,故意使它不那么聪明。
埃里克·利珀特

4

“为什么编译器甚至不警告返回某些东西?或者为什么一种语言允许我们使用具有无限循环且不返回任何东西的非空方法?”。

该代码在所有其他语言中也有效(可能除了Haskell!)。因为第一个假设是我们正在“有意”编写一些代码。

在某些情况下,此代码可能是完全有效的,例如您打算将其用作线程。或者,如果返回a Task<int>,则可以基于返回的int值进行一些错误检查-不应返回。


3

我可能是错的,但某些调试器允许修改变量。在这里,虽然x不会被代码修改,而JIT会对其进行优化,但有人可能会将x修改为false,并且方法应该返回某些内容(如果C#调试器允许这样做)。


1

Java案例的具体细节(可能与C#案例非常相似)与Java编译器如何确定方法是否能够返回有关。

具体来说,规则是根据JLS 8.4.7,具有返回类型的方法必须不能正常完成,而必须始终突然完成(此处通过返回语句或异常指示)。

如果声明某个方法具有返回类型,则如果该方法的主体可以正常完成,则会发生编译时错误。换句话说,具有返回类型的方法只能通过使用提供值return的return语句来返回;不允许“掉落尸体的末端”

编译器将根据JLS 14.21 Unreachable Statements中定义的规则来查看是否可以正常终止,因为它还定义了正常完成的规则​​。

值得注意的是,不可达语句的规则仅对具有已定义true常量表达式的循环具有特殊情况:

如果满足以下至少一项条件,则while语句可以正常完成:

  • while语句是可到达的,并且条件表达式不是值为true的常量表达式(第15.28节)。

  • 有一个可达的break语句退出while语句。

因此,如果该while语句可以正常完成,则下面的return语句是必需的,因为该代码被认为是可到达的,并且while没有可到达的break语句或常量true表达式的任何循环都被视为能够正常完成。

这些规则意味着你while有一个恒定的真实意思表示,没有一份声明break从来没有考虑过正常完成,因此它下面的任何代码永远不会认为是可达的。该方法的结尾在循环之下,并且由于该循环之下的所有内容均不可访问,因此该方法的结尾也是如此,因此该方法可能无法正常完成(这是编译器所寻找的)。

if 另一方面,对于循环提供的常量表达式,语句没有特殊的豁免。

比较:

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

    if (condition) return true;
}

带有:

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

    while (condition)
    {
        return true;
    }
}

区别的原因非常有趣,并且是由于希望允许不会导致编译器错误的条件编译标志(来自JLS):

可能希望if语句以下列方式处理:

  • 如果至少满足以下条件之一,则if-then语句可以正常完成:

    • if-then语句是可到达的,并且条件表达式不是值为真的常量表达式。

    • 然后,该语句可以正常完成。

    如果if-then语句是可到达的,并且条件表达式不是其值为false的常数表达式,则then语句是可到达的。

  • 如果then语句可以正常完成或else语句可以正常完成,则if-then-else语句可以正常完成。

    • 如果if-then-else语句是可到达的,并且条件表达式不是其值为false的常数表达式,则then语句是可到达的。

    • 如果if-then-else语句是可到达的,并且条件表达式不是其值为true的常数表达式,则else语句是可到达的。

这种方法将与其他控制结构的处理一致。但是,为了允许if语句方便地用于“条件编译”目的,实际规则有所不同。

例如,以下语句导致编译时错误:

while (false) { x=3; }因为该陈述x=3;是无法达到的;但表面上类似的情况:

if (false) { x=3; }不会导致编译时错误。优化的编译器可能会意识到该语句x=3;将永远不会执行,并且可以选择从生成的类文件中省略该语句的代码,但是从x=3;此处指定的技术意义上讲,该语句不被视为“不可访问的”。

这种不同处理的原理是允许程序员定义“标志变量”,例如:

static final boolean DEBUG = false; 然后编写如下代码:

if (DEBUG) { x=3; } 这个想法是应该可以将DEBUG的值从false更改为true或从true更改为false,然后正确编译代码,而无需对程序文本进行其他更改。

为什么条件break语句会导致编译器错误?

如循环可达性规则中所述,如果while循环包含可到达的break语句,则while循环也可以正常完成。由于if语句的then子句的可到达性规则根本没有考虑条件,因此始终if将条件if语句的then子句视为可到达。

如果break可以到达,则循环后的代码也再次被视为可到达。由于没有可访问的代码导致循环后突然终止,因此该方法被认为能够正常完成,因此编译器将其标记为错误。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.