您是否读过Scaladoc的“明显”收集方法? 还是为什么懒惰并不总是那么好

如果您不知道它们有何不同


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方法的文档了:


  /** Transforms this map by applying a function to every retrieved value. * @param f the function used to transform values of this map. * @return 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. */ 

第一行告诉我们我们的想法-它更改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)) 

一切都很好:)


如果您仍然希望保持懒惰,最好更改代码以使其显而易见。 您可以使用mapValuesfilterKeys的包装方法来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地使用mapValuesfilterKeys时可能出现的问题可以添加到性能下降,内存消耗增加和/或GC负载增加的问题上,但这更多地取决于特定情况,因此可能并不那么重要。


您还应该将迭代器的toSeq方法添加到toSeq的存钱罐中,这将返回惰性的Stream


我们偶然地对mapValuesmapValues佣。 它用在一种方法中,该方法通过反射从配置中创建了一组处理程序:键是处理程序的标识符,值是它们的设置,然后将其转换为处理程序本身(创建了类实例)。 由于处理程序仅由纯函数组成,因此一切工作都没有问题,甚至明显也没有影响性能(在进行了耙后,我们进行了测量)。


但是一旦我不得不在其中一个处理程序中制作一个信号量,以便仅一个处理程序执行繁重的功能,其结果就会被其他处理程序缓存并使用。 然后在测试环境中开始出现问题-由于信号量问题,在本地运行良好的有效代码开始崩溃。 当然,关于新功能无法操作的第一个想法是问题与它本身有关。 我们讨论了很长时间,直到得出结论“某些游戏,为什么要使用不同的处理程序实例”? 他们只是在堆栈跟踪中发现我们mapValuesmapValues


如果您使用的是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期待这种行为

也就是说,有关于意外行为的评论,同时您也可以稍微踩一下耙子,这显然不太可能。


→本文中的所有代码均在此要点中

Source: https://habr.com/ru/post/zh-CN430538/


All Articles