首先来看一段代码:
// kotlin
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value
}
is Sum -> {
val left = eval(e.left)
val right = eval(e.right)
println("sum: $left + $right")
left + right
}
else -> throw UnknownError("unknown err")
}
fun main(args: Array<String>) {
println(eval(Sum(Sum(Num(1), Num(2)), Sum(Num(4),Num(5)))))
}
这段代码来自 《
Kotiln in Action
》
然后是看一下输出:
num: 1
num: 2
sum: 1 + 2
num: 4
num: 5
sum: 4 + 5
sum: 3 + 9
12
代码量很少,但是能看出这是一个递归。
但是看到 eval(Sum(Sum(Num(1), Num(2)), Sum(Num(4),Num(5))))
这句调用,估计很多人都是一脸懵逼。
首先要明确一点, eval
的参数是 Expr
所以,上面这段调用是符合语法规范的。
然后是分析一下 eval
的实现逻辑:
- 如果参数是
Num
类型的,直接返回对应的value
,不进行递归; - 如果参数是
Sum
类型的,分别对其left
,right
调用eval()
, 也就是递归,并且对返回值求和。由于
eval
的返回值是Int
, 所以求和也没毛病。
看上面的 调用栈知道,如果参数不是Num
, 就会一直递归,直到参数是Num
类型为止。所以这样理论上不会导致栈溢出,因为有递归结束条件。
如果使用Java
来写的话,可读性会好很多。
这个递归最巧妙的地方,我感觉是:
Sum
需要两个参数,类型都是Expr
, 如果希望传入的是Sum
类型 ,那么这个作为参数的Sum
依然需要两个Expr
,那即使你继续创建Sum
,最终还是需要两个Expr
,而只有创建Num(Int)
, 才能完成一个Expr
对象的实例化。
这就导致,你需要至少有两个Num
类型的Expr
对象,你才能完成一个Sum
类型对象的实例化。
而eval
就利用了这一点,保证了递归不会一直调用下去。因为总会碰到Num
类型的参数,碰到了,就不会再递归,直接进行取值。
// java
public static int eval(Expr e) {
if (e instanceof Num) {
Num n = (Num) e;
System.out.println("NUM: " + n.getValue());
return n.getValue();
} else if (e instanceof Sum) {
Sum s = (Sum) e;
int left = eval(s.getLeft());
int right = eval(s.getRight());
System.out.println("SUM: " + left + " + " + right);
return left + right;
}
throw new RuntimeException("error when eval...");
}