一段实际代码简化后如下:
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
表达式也是有返回值的,且返回值主要是取决于try
和catch
里的最后一行表达式,而finally
被认为是做一些收尾的工作的,不应该在里面去改变返回结果。
具体到这个案例,foo
方法声明的返回值类型是String
,foo
方法体里的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-finally
的finally
,编译器总是预期它里面的表达式类型为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类型才行。