谨慎使用Scala Map的mapValues, 你需要的可能是transform
没有踩过mapValues的坑之前,我相信大多数人会认为mapValues和所有其他map类方法的逻辑是一样的:对Map里所有的value施加一个map函数,返回一个新的Map。但实际情况却并不这么简单,还是先看一段“诡异”的代码吧 (本文原文出处: 本文原文链接: http://www.voidcn.com/article/p-ykdrsldf-brv.html 转载请注明出处。): object Main extends App{
class A {
def this(f: String) = {
this()
this.f = f
}
var f: String = _
override def toString = s"A(${f})"
}
val m = Map(1 -> new A("a"))
println("-------------------------")
println(s"m=$m")
println("-------------------------")
val m1 = m.mapValues{a=>a.f = "b";a}
println(s"m=$m")
println(s"m1=$m1")
m1.foreach(_._2.f = "c")
println(s"m1=$m1")
println("-------------------------")
val m2 = m.transform{(k,v)=>v.f = "d";v}
println(s"m=$m")
println(s"m2=$m2")
println("-------------------------")
}
程序输出: ------------------------- m=Map(1 -> A(a)) -------------------------
m=Map(1 -> A(a))
m1=Map(1 -> A(b))
m1=Map(1 -> A(b)) -------------------------
m=Map(1 -> A(d))
m2=Map(1 -> A(d)) -------------------------
这段代码的“诡异”之处是在完成 这确实让人费解,并且会让很多使用mapValues的程序员“入坑”,因为这个事例中对mapValues的使用意图是很有普遍性和代表性的:即将原有map通过mapValues转换为新的map之后继续在上面进行其他的操作,但是这里被mapValues返回的m1似乎永远停留在了它在第一次赋值时的那个状态! 那么到底背后发生了什么了呢?我们在 object Main extends App{
class A {
def this(f: String) = {
this()
this.f = f
}
var f: String = _
override def toString = s"A(${f})"
}
val m = Map(1 -> new A("a"))
println("-------------------------")
println(s"m=$m")
println("-------------------------")
val m1 = m.mapValues{a=>a.f = "b";println("1:"+a.f);a}
println(s"m=$m")
println(s"m1=$m1")
m1.foreach{kv=>kv._2.f = "c";println("2:"+kv._2.f)}
println(s"m1=$m1")
println("-------------------------")
val m2 = m.transform{(k,v)=>v.f = "d";v}
println(s"m=$m")
println(s"m2=$m2")
println("-------------------------")
}
程序输出: ------------------------- m=Map(1 -> A(a)) -------------------------
m=Map(1 -> A(a))
1:b
m1=Map(1 -> A(b))
1:b
2:c
1:b
m1=Map(1 -> A(b)) -------------------------
m=Map(1 -> A(d))
m2=Map(1 -> A(d)) -------------------------
添加打印语句之后,透露了诡秘之处的些许原因,关键点是在 1:b
2:c
1:b
显然 a map view which maps every key of this map to f(this(key)). The resulting map wraps the original map without copying any elements. 这里的关键点是view,既然是view,那么所有的transformation的操作就都是lazy的。关于Scala集合的View,请参考官方文档:https://docs.scala-lang.org/overviews/collections/views.html , 本文不做过多赘述,总之mapValues返回的这个结果是一个view,提供给它的那个转换函数 f(this(key))总是在需要实际读取值时才会被触发执行,那我们来解释一下前后两个1:b是怎么回事:第一个是在m1调用foreach时触发的,因为此时需要实际读取m1中的值了,所以mapValues将会被关联性地触发,从当前状态的m通过mapValues得到了m1,这是第一个1:b的来历。在 为了更加清楚地验证这一点,我们给类A再加一个字段g,让f依然保持在mapValues中被修改,g通过m或m1来修改,我们看一下会发生什么: object Main extends App{
class A {
def this(f: String,g: String) = {
this()
this.f = f
this.g = g
}
var f: String = _
var g: String = _
override def toString = s"A(f=${f},g=${g})"
}
val m = Map(1 -> new A("a","a"))
println("-------------------------")
println(s"m=$m")
println("-------------------------")
val m1 = m.mapValues{a=>a.f = "b";println("1:"+a.f);a}
println(s"m=$m")
println(s"m1=$m1")
m1.foreach{kv=>kv._2.f = "c";println("2:"+kv._2.f)}
println(s"m1=$m1")
println("-------------------------")
m.foreach{kv=>kv._2.f = "e";kv._2.g = "e"}
println(s"m=$m")
println(s"m1=$m1")
println("-------------------------")
m1.foreach{kv=>kv._2.f = "f";kv._2.g = "f";println("2:"+kv._2.f)}
println(s"m=$m")
println(s"m1=$m1")
println("-------------------------")
}
程序输出: ------------------------- m=Map(1 -> A(f=a,g=a)) -------------------------
m=Map(1 -> A(f=a,g=a))
1:b
m1=Map(1 -> A(f=b,g=a))
1:b
2:c
1:b
m1=Map(1 -> A(f=b,g=a)) -------------------------
m=Map(1 -> A(f=e,g=e))
1:b
m1=Map(1 -> A(f=b,g=e)) -------------------------
1:b
2:f
m=Map(1 -> A(f=f,g=f))
1:b
m1=Map(1 -> A(f=b,g=f)) -------------------------
在这个对比明显的示例中,我们可以看到,不管我们如何的折腾,集合元素的f字段被死死的锁定为b,当然,用锁定并不准确,而是每次试图读取值时都会被重新刷回b,但是对于字段g则完全不同,不管是是通过m还是m1,对g的任何修改都是有效的! 所以总结一下这个诡异问题的根源就是:mapValues创建的map的view总是lazy的,如果mapValues的操作涉及到改写元素值的操作,要特别小心,因为在每次引用到这个view时,mapValues都会被重新执行,在次之前的任何修改都会被覆盖!这是mapValues最糟糕的地方!如果你不希望这种情况发生(显然大多数情况下你是不希望这样做的),那么你需要使用的是transform而不是mapValues. 最后提一下,scala集合在mapValues上的设计确实有瑕疵,倒不是因为mapValues的实现逻辑有问题,而是mapValues与其他Map的操作有质的区别,它是切换成View模式进行工作的一个方法,而Scala在API层面上没有给到使用者足够的提示!为此,有人建议Scala能校方SeqView为Map设立一个MapView,使用者应该显示的通过Map.view切换到一个view上再通过view.mapValues这样的方式来调用,这会避免使用者因为不了解mapValues的特性而导致错误地使用了该方法,这个请求已经作为一个bug被log给了Scala官方:https://issues.scala-lang.org/browse/SI-4776 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |