如果您不知道它们有何不同
someMap.map{ case (k, v) => k -> foo(v)}
和
someMap.mapValues(foo)
除了语法外,或者您怀疑/不知道这种差异会导致什么不良后果,以及identity
在哪里,那么本文适合您。
否则,请参加文章末尾的调查。
让我们从一个简单的开始
让我们愚蠢地尝试举个例子,看看发生了什么:
val someMap = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): String = value + "_changed" val resultMap = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}")
该代码有望打印
resultMap: Map(key1 -> value1_changed, key2 -> value2_changed) resultMapValues: Map(key1 -> value1_changed, key2 -> value2_changed) equality: true
在此级别上,在学习Scala的早期阶段就mapValues
了对mapValues
方法的理解:是的,是的,有这样一种方法,当键不变时,在Map
更改值很方便。 真的,还有什么要考虑的? 用该方法的名称,一切都是显而易见的,行为是显而易见的。
更复杂的例子
让我们修改一下示例(我将显式地编写类型,以免您认为不存在带有隐式含义的麻瓜):
case class ValueHolder(value: String) val someMap: Map[String, String] = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): ValueHolder = ValueHolder(value) val resultMap: Map[String, ValueHolder] = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}")
这样的代码将在启动后产生
resultMap: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) resultMapValues: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) equality: true
这是很合逻辑且显而易见的。 “老兄,是时候深入本文了!” -读者会说。 让我们根据外部条件创建类,并添加一些简单的白痴检查:
case class ValueHolder(value: String, seed: Int) def foo(value: String): ValueHolder = ValueHolder(value, Random.nextInt()) ... println(s"simple assert for resultMap: ${resultMap.head == resultMap.head}") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}")
在输出中,我们得到:
resultMap: Map(key1 -> ValueHolder(value1,1189482436), key2 -> ValueHolder(value2,-702760039)) resultMapValues: Map(key1 -> ValueHolder(value1,-1354493526), key2 -> ValueHolder(value2,-379389312)) equality: false simple assert for resultMap: true simple assert for resultMapValues: false
现在得出的结果是不相等而是随机的,这是合乎逻辑的。 但是等等,为什么第二个断言给出了false
? 实际上, resultMapValues
的值已更改,但是我们没有对它们做任何事情吗? 让我们检查一下里面的所有东西是否都一样:
println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues")
在输出中,我们得到:
resultMapValues: Map(key1 -> ValueHolder(value1,1771067356), key2 -> ValueHolder(value2,2034115276)) resultMapValues: Map(key1 -> ValueHolder(value1,-625731649), key2 -> ValueHolder(value2,-1815306407))

为什么会这样呢?
为什么println
更改Map
的值?
是时候进入mapValues
方法的文档了:
第一行告诉我们我们的想法-它更改Map
,将参数中传递的函数应用于每个值。 但是,如果您非常仔细地阅读并最后阅读,那么结果是返回的不是Map
,而是“ map view”(视图)。 这不是普通视图( View
),您可以使用view
方法获得该view
,并且具有用于显式计算的force
方法。 一个特殊的类(代码来自Scala版本2.12.7,但是对于2.11来说几乎是同一件事):
protected class MappedValues[W](f: V => W) extends AbstractMap[K, W] with DefaultMap[K, W] { override def foreach[U](g: ((K, W)) => U): Unit = for ((k, v) <- self) g((k, f(v))) def iterator = for ((k, v) <- self.iterator) yield (k, f(v)) override def size = self.size override def contains(key: K) = self.contains(key) def get(key: K) = self.get(key).map(f) }
如果阅读此代码,您将看到没有任何缓存,并且每次访问这些值时,都将重新计算它们。 我们在示例中观察到的内容。
如果您使用纯函数并且所有内容都是不可变的,那么您将不会注意到任何区别。 好吧,也许性能会耗尽。 但是,不幸的是,并不是我们世界上的所有事物都是干净的和完美的,并且使用这些方法,您可以踩到耙子(这发生在我们的一个项目中,否则本文不会发生)。
当然,我们不是第一个遇到这种情况的人。 早在2011年,就已经在这种情况下打开了一个主要错误 (在撰写本文时,它被标记为已打开)。 它还提到了filterKeys
方法,该方法具有完全相同的问题,因为它是基于相同的原理实现的。
此外,自2015年以来,为了对IntelliJ Idea进行检查而挂票 。
怎么办
最简单的解决方案是愚蠢地不使用这些方法,因为 从名称上看,我认为它们的行为非常明显。
更好的选择是调用map(identity)
。
identity
,如果任何人都不知道,这是标准库中的函数,仅返回其输入参数。 在这种情况下,主要工作由map
方法完成,该方法显式创建一个普通Map
。 让我们检查一下以防万一:
val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo).map(identity) println(s"resultMapValues: $resultMapValues") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}") println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues")
在输出中,我们得到
resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) simple assert for resultMapValues: true resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608))
一切都很好:)
如果您仍然希望保持懒惰,最好更改代码以使其显而易见。 您可以使用mapValues
和filterKeys
的包装方法来mapValues
隐式类,从而为它们提供一个易于理解的新名称。 或显式使用.view
并使用成对的迭代器。
此外,值得在静态分析器中的其他环境中对开发环境/规则进行检查,以警告使用这些方法。 因为现在花一些时间在此之上比踩着耙子并在很长一段时间内耙开后果要好。
您还能如何踩踏耙子,我们如何踩踏耙子
除了在上面的示例中观察到的依赖外部条件的情况之外,还有其他选择。
例如,一个可变的值(请注意,表面上一切都是“不变的”):
val someMap1 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) val someMap2 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) def increment(value: AtomicInteger): Int = value.incrementAndGet() val resultMap: Map[String, Int] = someMap1.map { case (k, v) => k -> increment(v) } val resultMapValues: Map[String, Int] = someMap2.mapValues(increment) println(s"resultMap (1): $resultMap") println(s"resultMapValues (1): $resultMapValues") println(s"resultMap (2): $resultMap") println(s"resultMapValues (2): $resultMapValues")
此代码将产生以下结果:
resultMap (1): Map(key1 -> 1, key2 -> 1) resultMapValues (1): Map(key1 -> 1, key2 -> 1) resultMap (2): Map(key1 -> 1, key2 -> 1) resultMapValues (2): Map(key1 -> 2, key2 -> 2)
当我再次访问someMap2
时, someMap2
得到了一个有趣的结果。
mapValues
地使用mapValues
和filterKeys
时可能出现的问题可以添加到性能下降,内存消耗增加和/或GC负载增加的问题上,但这更多地取决于特定情况,因此可能并不那么重要。
您还应该将迭代器的toSeq
方法添加到toSeq
的存钱罐中,这将返回惰性的Stream
。
我们偶然地对mapValues
了mapValues
佣。 它用在一种方法中,该方法通过反射从配置中创建了一组处理程序:键是处理程序的标识符,值是它们的设置,然后将其转换为处理程序本身(创建了类实例)。 由于处理程序仅由纯函数组成,因此一切工作都没有问题,甚至明显也没有影响性能(在进行了耙后,我们进行了测量)。
但是一旦我不得不在其中一个处理程序中制作一个信号量,以便仅一个处理程序执行繁重的功能,其结果就会被其他处理程序缓存并使用。 然后在测试环境中开始出现问题-由于信号量问题,在本地运行良好的有效代码开始崩溃。 当然,关于新功能无法操作的第一个想法是问题与它本身有关。 我们讨论了很长时间,直到得出结论“某些游戏,为什么要使用不同的处理程序实例”? 他们只是在堆栈跟踪中发现我们mapValues
了mapValues
。
如果您使用的是Apache Spark,则突然发现对于带有.mapValues
一些基本代码,您可能会发现类似的问题
java.io.NotSerializableException: scala.collection.immutable.MapLike$$anon$2
https://stackoverflow.com/questions/32900862/map-can-not-be-serializable-in-scala
https://issues.scala-lang.org/browse/SI-7005
但是map(identity)
解决了这个问题,通常没有动力/时间进行更深入的研究。
结论
错误可能会在最意想不到的地方潜伏-即使在那些看起来100%显而易见的方法中也是如此。 具体来说,我认为此问题与方法名称差和返回类型不够严格有关。
当然,研究标准库中使用的所有方法的文档很重要,但是它并不总是显而易见的,并且坦率地说,并不总是有足够的动力来阅读“显而易见的东西”。
懒惰计算本身就是一个很酷的笑话,并且这篇文章绝不鼓励他们放弃。 但是,当懒惰不明显时,这可能会导致问题。
公平地说, mapValues
的问题已经在翻译中出现在哈布雷(Habré)上了,但就我个人而言,我曾经非常mapValues
这篇文章, 有许多已知的/基本的东西,尚不完全清楚使用这些功能可能带来的危险:
filterKeys方法包装源表而不复制任何元素。 这没什么不对,但是您几乎无法从filterKeys期待这种行为
也就是说,有关于意外行为的评论,同时您也可以稍微踩一下耙子,这显然不太可能。
→本文中的所有代码均在此要点中