scala雾中风景(25): try-finally表达式的类型推导

一段实际代码简化后如下:

class A {
  def foo():String = {
    try{
      Thread.sleep(1000)
    }finally{
      "ok"
    }
  }
}

本来期望这个foo方法返回finally块中的”ok”,但编译的时候却给出了类型不匹配的错误:

 ➜  scalac A.scala
A.scala:4: error: type mismatch;
 found   : Unit
 required: String
      Thread.sleep(1000)
                  ^

按说scala类型推断这么强大,不应该推断不出最终的返回值类型,从编译器的错误来看似乎它非要求在try代码块里最后一行表达式必须也是String类型的值,为什么finally里的表达式没有参与类型推断呢?

把上述代码稍作改动,在try代码块里明确的给出一个String结果

def foo():String = {
    try{
        Thread.sleep(1000)
        "res"
    }finally{
        "ok"
    }
}

再编译一下,却给出了一行警告:

 ➜  scalac A.scala
A.scala:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
    "ok"
    ^
one warning found

分析一下为什么编译器认为这句表达式”does nothing”,而不把它当作返回值对待:

 ➜  scalac -Xprint:typer A.scala
 ...

def foo(): String = try {
  java.this.lang.Thread.sleep(1000L);
  "res"
} finally {
  "ok";
  ()
}

看到编译器在finally块的”ok”表达式后边自行增加了一个返回Unit类型的值(),看上去编译器认为finally块里的逻辑是一个“procedure”,一定要满足Unit

从scala语言规范来看,try-catch-finally表达式也是有返回值的,且返回值主要是取决于trycatch里的最后一行表达式,而finally被认为是做一些收尾的工作的,不应该在里面去改变返回结果。

具体到这个案例,foo方法声明的返回值类型是Stringfoo方法体里的try-finally表达式的值就是最终的返回值,而try-finally表达式的值是取决于try代码块里的最后一行表达式,而非finally块里的。

看几个例子:

scala> val a = try { 100 } finally { 200 }
<console>:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
   val a = try { 100 } finally { 200 }
                                 ^
a: Int = 100

上面try-finally语句的结果是try里的值。

scala> val a = try { throw new Exception() } catch { case e:Exception => 200 }
a: Int = 200

上面try里发生了异常,最终的结果是catch里的。

scala> val a = try { throw new Exception() } catch { case e:Exception => 200 } finally { 300 }
<console>:7: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
   val a = try { throw new Exception() } catch { case e:Exception => 200 } finally { 300 }
                                                                                     ^
a: Int = 200

上面finally里的表达式并不会被当作最终返回值。

当然,在finally块里是可以使用return关键字的,但return关键字在这里并不能改变try-finally表达式的结果类型,我们对原代码增加return再编译:

def foo():String = {
    try{
        Thread.sleep(1000)
    }finally{
      return "ok"
    }
}

 ➜  scalac A.scala
A.scala:4: error: type mismatch;
 found   : Unit
 required: String
      Thread.sleep(1000)
                  ^
one error found

依然编译错误,注意return是面向method的,属于流控层面,并不影响表达式的类型推断。因为在foo方法里try-finally就是最后一句表达式,所以编译器要求这句表达式的类型必须也满足foo的返回值类型签名。如果try-finally不是最后一句,就没有这个约束了,比如:

def foo():String = {
    try{
        Thread.sleep(1000)
    }finally{
      return "ok"
    }

    "no"
}

上面对foo方法在最后一行增加了一句返回”no”的表达式,使得前边的try-finally表达式类型推导不受方法签名的约束,编译可以通过了。当然这个代码逻辑肯定不会走到那里,我更希望编译器给出代码不可达的警告。

如果打开typer-debug编译选项,可以看到编译器总会期待方法里的最后一个表达式满足方法返回值类型,如果最后的这个表达式又是由多个更小粒度的表达式组合成的(比如这个try-finally,我们暂称它为大表达式),则进一步对这个大表达式拆分推导,约束其中的决定整个大表达式类型的小表达式也必须符合方法的返回类型,对于try-finally这个大表达式来说就是其中try块里的最后一行表达式。

对于try-catch-finallyfinally,编译器总是预期它里面的表达式类型为Unit,所以如果在里面的最后一条语句不是一个Unit类型的值,编译会自动给你加上。

注意,return ok这句表达式的类型是Nothing,不要混淆方法返回值类型和表达式自身类型。return, throw等跟流控相关的表达式都是Nothing,它可以满足任何类型,自发可以符合finally里的Unit预期。

其实这个问题是scala类型推导实现的问题,我们期望它更聪明一些,比如:

scala> def bar:String = { return "A";  "B"  }
bar: String

在Java里编译器会报错后边的语句不可达,但Scala里却编译通过。虽然后边的表达式没有意义,不会走到那儿,但并不意味着你能给出任意的值:

scala> def bar:String = { return "A";  200 }
<console>:7: error: type mismatch;
 found   : Int(200)
 required: String
   def bar:String = { return "A";  200 }

尽管后边的表达式不会被执行到,但它在编译时参与类型推导,因为对于该方法来说 { return "A"; 200 }整体是一个大表达式也必须满足String类型才行。

scala雾中风景(25): try-finally表达式的类型推导》上有1条评论

  1. xxd

    Java的Finally语句可以使用`return value`来显式的改变方法的返回值。貌似还有关于这个的puzzler。有可能Scala的设计者在设计Scala的时候,就改掉了这个他们认为不太好的地方。。。

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注