scala雾中风景(3): for表达式的背后

这个例子是以前从scala-user邮件列表看到的,我借用这里例子加工了一下,这是一个for表达式转换的细节

//有一个map,里面有一些自定义类型的数据,然后在用下面的for进行操作
for((key,value) <- m; name=key) { 
    println(name) 
}

他在运行时发现上面的代码时发现key的hashCode会被调用,而正好这个hashCode的实现很复杂,导致效率很低。而他使用下面的写法则没有问题:

for((key,value) <- m) { 
    val name=key
    println(name) 
}

分析一下第一种写法为什么会导致hashCode方法被执行,先模拟一下:

scala> class MyKey { override def hashCode() = { println("key"); super.hashCode }}

scala> class MyValue { override def hashCode() = { println("value"); super.hashCode }}

// 把上面的k,v放入一个map;在放入Map的过程 k和v的hashCode方法都被执行了
scala> val m = Map[MyKey, MyValue](new MyKey() -> new MyValue())
key     
value   

// 模拟第一种for表达式
scala> for( (k,v) <- m; name = k ) { println(name+" do nothing") }
key     // k的hashCode被调用
$line3.$read$$iw$$iw$MyKey@63b97fd0 do nothing

是否这里又把k放入了Map?在上面的for表达式里,没有用到yield,是否直接被转换为foreach呢?

for表达式的展开和转换不在这里讨论,关于其语法背后的转换可以参考这篇文章。我们聚焦在这个案例的细节上。for表达式可以这样描述:

for([pattern <- generator; definition*]+; filter* )
    [yield] expression

在for小括号(也可以用花括号)内部由一个或多个 p <- generator; definition* 以及0个或多个filter组成。后跟着一个可选的yield关键字,以及后续的逻辑表达。

p <- generator; definition* 这句再展开,由一个 p <- generator 以及0个或多个definition组成。

这个问题的细节就在于有没有definition对for表达式在转换时的影响;也就是说 for(p <- generator)for(p <- generator; definition) 是不一样的。

没有yield关键字,for((key,value) <- m) 直接翻译为

m.foreach(…)

for((key,value) <- m; name=key) 被翻译为

m.map(…).foreach(…)

为什么中间多了一次map操作?我们通过一个简单例子来看:

scala> import reflect.runtime.universe._

scala> val list = List("A","B","C")

scala> reify( for(e <- list; x=e;y=e) { println(x+y) } )
res1: reflect.runtime.universe.Expr[Unit] =
Expr[Unit]($read.list.map(((e) => {
    val x = e;
    val y = e;
    Tuple3.apply(e, x, y)
}))(List.canBuildFrom).foreach(((x$1) => x$1: @unchecked match {
    case Tuple3((e @ _), (x @ _), (y @ _)) => Predef.println(x.$plus(y))
})))

从reify的结果可以看到,对于for(e <- list; x=e; y=e) 里面的x=ey=e两个definition中的变量x,y与e一同用一个tuple封装起来,即map的结果得到的是一个 List[Tuple3] 类型的数据。随后再对这个新结果进行foreach

所以最初的问题是因为for里面存着了一个definition,而导致转换过程多了一次map,也就是创建了一个新的Map[Tuple2],并把数据放入新Map,在这个过程导致了hashCode的调用。

小结:对于for表达式要知道背后会被转换为什么,不要滥用for的语法糖,误用导致低效率。

scala雾中风景(3): for表达式的背后》上有6个想法

发表评论

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