首先来看一段代码:
// 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...");
}

11万+

被折叠的 条评论
为什么被折叠?



