Scala: "map"和"foreach" - 在实践中使用"foreach"有什么理由吗?

18
在Scala集合中,如果要迭代一个集合(不返回结果,即在集合的每个元素上执行副作用),可以通过以下两种方式之一实现:
final def foreach(f: (A) ⇒ Unit): Unit


final def map[B](f: (A) ⇒ B): SomeCollectionClass[B]

除了可能的惰性映射(*)之外,从终端用户的角度来看,我认为这些调用没有任何区别:

myCollection.foreach { element =>
  doStuffWithElement(element);
}

myCollection.map { element =>
  doStuffWithElement(element);
}

考虑到我可以忽略map输出的内容,我想不出为什么需要存在和使用两种不同的方法,因为map似乎包含了foreach的所有功能。实际上,如果一个智能编译器和虚拟机没有将集合对象创建优化掉,即使它没有被分配给任何变量、读取或在任何地方使用,我也会印象深刻。
因此,问题是:我对吗?在代码中没有理由调用foreach吗?
注意:
(*) 惰性映射的概念,正如这个问题详细说明的那样,可能会改变一些事情,并且有理由使用foreach,但据我所见,需要特别遇到LazyMap,而正常
(**) 如果不是“使用”集合,而是“编写”集合,那么很快就会发现for推导语法实际上是一个语法糖,生成"foreach"调用,即这两行代码生成完全等效的代码:
for (element <- myCollection) { doStuffWithElement(element); }
myCollection.foreach { element => doStuffWithElement(element); }

因此,如果一个人在意其他人使用该集合类与for语法,那么他可能仍然希望实现foreach方法。


13
使用 foreach 而不是 map 可以很好地区分有副作用和无副作用的函数,这样做很好。我并不关心编译器是否对它们进行了优化。不同之处可以更清楚地表达代码的目的。 - Michael Zajac
1
为了更进一步,我更喜欢使用 for ..(不带 yield)而不是直接使用 .foreach,因为它进一步(对我来说)显示了迭代的副作用和序列转换之间的区别。此外,假设被迭代的序列不是惰性的可能是危险的。 - user2864740
дёҺдҪ зӣёжҜ”пјҢеҰӮжһңеҪ“еүҚзҡ„Scalaзј–иҜ‘еҷЁе’Ң/жҲ–JVMиғҪеӨҹеңЁз»“жһңжңӘдҪҝз”Ёж—¶дјҳеҢ–mapеҲ°foreachд»Јз ҒпјҢйӮЈд№ҲжҲ‘дјҡйқһеёёеҚ°иұЎж·ұеҲ»гҖӮиҝҷеҜ№жҲ‘жқҘиҜҙжҳҜдёҖдёӘжһҒе…¶дёҚе№іеҮЎзҡ„дјҳеҢ–гҖӮ - ziggystar
3个回答

17
我可以想到几个动机:
  1. foreach是一个返回类型为Unit的方法的最后一行时,编译器不会报错,但使用map则会(你需要加上-Ywarn-value-discard选项)。有时你会在使用map时得到warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses, 但使用foreach则可能不会。
  2. 更容易阅读 - 读者可以一眼看出你正在改变一些状态而没有返回任何东西,但如果使用map,理解相同的操作将需要更多的认知资源。
  3. 进一步来说,当传递命名函数到mapforeach时,也可以进行类型检查。
  4. 使用foreach不会构建新的列表,因此会更有效率(感谢@Vishnu)。

1
请纠正我,Map返回一个不可变对象,而foreach的返回类型为Unit。因此,如果您的意图只是读取内容,那么foreach更具有内存效率。对吗? - Vishnu
好的,谢谢 @Vishnu - samthebest
其实,我认为map返回与应用它的集合相同类型的集合。因此,如果将其应用于可变集合,则应该返回一个新的可变集合。 - Pietrotull

8
scala> (1 to 5).iterator map println
res0: Iterator[Unit] = non-empty iterator

scala> (1 to 5).iterator foreach println
1
2
3
4
5

这正是我在 (*) 中提到的 - 这是惰性映射,在所有迭代器上都是默认的。 - GreyCat
那么你在这个问题中的断言是,foreachmap除非它们不同,否则它们是相同的? - Chris Martin
我的断言是,“它们是相同的,除了明确定义的情况 () 和 (* )”,在我看来,这两种情况在普通终端用户实践中都相当罕见。 - GreyCat

4
如果建造机械可以被优化,那就太棒了。
scala> :pa
// Entering paste mode (ctrl-D to finish)

implicit val cbf = new collection.generic.CanBuildFrom[List[Int],Int,List[Int]] {
def apply() = new collection.mutable.Builder[Int, List[Int]] {
val b = new collection.mutable.ListBuffer[Int]
override def +=(i: Int) = { println(s"Adding $i") ; b +=(i) ; this }
override def clear() = () ; override def result() = b.result() }
def apply(from: List[Int]) = apply() }

// Exiting paste mode, now interpreting.

cbf: scala.collection.generic.CanBuildFrom[List[Int],Int,List[Int]] = $anon$2@e3cee7b

scala> List(1,2,3) map (_ + 1)
Adding 2
Adding 3
Adding 4
res1: List[Int] = List(2, 3, 4)

scala> List(1,2,3) foreach (_ + 1)

据我所见,与CanBuildFrom相关的隐式内容将在JVM级别生成一组类层次结构,其中包括一个将包含+=方法的类,该方法实际上会执行某些操作,而不仅仅是构造最终将被丢弃的变量(因此所有这些代码都可以安全地跳过)。当然,一个执行某些操作的方法,特别是当这些操作是明确的invokevirtual调用时,将不会被优化掉。 - GreyCat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接