谨慎使用Scala Map的mapValues, 你需要的可能是transform

推荐:博主历时三年倾注大量心血创作的《大数据平台架构与原型实现:数据中台建设实战》一书已由知名IT图书品牌电子工业出版社博文视点出版发行,真诚推荐给每一位读者!点击《重磅推荐:建大数据平台太难了!给我发个工程原型吧!》了解图书详情,扫码进入京东手机购书页面!

在这里插入图片描述

 

 

没有踩过mapValues的坑之前,我相信大多数人会认为mapValues和所有其他map类方法的逻辑是一样的:对Map里所有的value施加一个map函数,返回一个新的Map。但实际情况却并不这么简单,还是先看一段“诡异”的代码吧 (本文原文出处: 本文原文链接: http://blog.csdn.net/bluishglc/article/details/80156218 转载请注明出处。):

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))
-------------------------

这段代码的“诡异”之处是在完成m1.foreach(_._2.f = "c")之后打印m1时,输出的竟然是m1=Map(1 -> A(b)),仅从效果上看似乎foreach操作根本没有执行过。而真正有效的实现是后面的transform的版本。(注: mapValues和transform往往是将集合元素转换为某一个类型的值,本例我们为了简化演示示例,让它们都返回了原来的类型。)

这确实让人费解,并且会让很多使用mapValues的程序员“入坑”,因为这个事例中对mapValues的使用意图是很有普遍性和代表性的:即将原有map通过mapValues转换为新的map之后继续在上面进行其他的操作,但是这里被mapValues返回的m1似乎永远停留在了它在第一次赋值时的那个状态!

那么到底背后发生了什么了呢?我们在m.mapValuesm1.foreach在迭代过程中打印一些信息来看看能不能发现一些问题:

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))
-------------------------

添加打印语句之后,透露了诡秘之处的些许原因,关键点是在m1.foreach完成之后打印m1之前输出的三行:

1:b
2:c
1:b

显然2:c是在foreach时打印的,那么前后两个1:b是什么情况呢?到这里,我们需要深入了解一下mapValues了,Scala的API文档里对mapValues的返回值是这样说明的:

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的来历。在m1.foreach的操作过程中,集合m元素的字段f确确实实地被改成了b, 但是,有趣而糟糕的是,在其之后打打印m1的过程中,m1作为一个view, 任然会重复前面的动作,就是从当前的集合m中应用mapValues而确定每一个并打印出来,所以mapValues又被执行了一次,这是第二个1:b的来由,而这样一来,集合m元素的字段f又从c被设置回了b!也就是说:在这个示例里,不管你对m或m1的元素做什么样的修改,在试图通过m1来读取它的元素的那一刻,所有的元素都会被重置回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

推荐:博主历时三年倾注大量心血创作的《大数据平台架构与原型实现:数据中台建设实战》一书已由知名IT图书品牌电子工业出版社博文视点出版发行,真诚推荐给每一位读者!点击《重磅推荐:建大数据平台太难了!给我发个工程原型吧!》了解图书详情,扫码进入京东手机购书页面!

在这里插入图片描述

 

 

你可能感兴趣的:(Scala语言)