为什么这个Iterable映射后会产生一个Set?

Why does this Iterable produce a Set after mapping?

在下面的示例代码中,为什么 Iterable[String] test1 映射后会产生一个 Set?

val foo = Map("a" -> 1, "b" -> 1)
val test1: Iterable[String] = foo.keys
val test2: Iterator[String] = foo.keys.toIterator

println(test1.map(foo).size) // 1
println(test2.map(foo).size) // 2

我对此感到困惑,因为它在阅读代码时完全违反直觉。尽管 foo.keys 只是 returns 一个 Iterable,它在调用 map 时创建了一个 Set,如反射代码所示:

println(test1.map(foo).getClass.getName) // immutable.Set.Set1
println(test2.map(foo).getClass.getName) // Iterator$$anon

标准库如何确定它应该在这里创建一个 immutable.Set,即使集合的推断类型只是 Iterable[String]

foo.keys returns a Set(尽管它的 return 类型更通用)并且在 Set 上调用 map 会产生另一个 Set.推断或编译时类型并不总是最精确的。

您可以看到 Set return 上的 keys 方法是 Set,即使 return typeIterable[A]:

scala> Map(1 -> 2).keys
res0: Iterable[Int] = Set(1)

这是棘手的隐式魔法。简化答案:存在 CanBuildFrom 值,该值在隐式范围内传递。当编译器搜索最常见的类型时,它会在参数范围内寻找隐式。

在您的示例中,编译器能够找出 foo.keys 最常见的类型是 Set。这听起来很合理:Set 可以看作是没有值的 Map(java 的 HashMap/HashSet 也是这样做的)。当您转换为可迭代对象时,隐含函数将丢失,并且 Set 消失(作为旁注,那些 CanBuildFrom 黑客并不健壮,并且将来可能会消失,因为它们确实使现有集合的扩展复杂化,您可能还想阅读 this 答案和评论)。

Scala 分享 Java 的 "de-jure and de-facto types" 概念。 "de-jure" 是在方法定义中声明的,但 "de-facto" 可能是继承者之一。这就是为什么,例如,您看到 Map.keys 类型为 Iterable,而实际上它是 Set(从 Map.keySet 产生,它有 Set de-法律类型)。

最后,第一个 println 中的 1 是因为在基础映射 foo 中所有值都相同,并且 Set(1,1) 变为 Set(1)

挖掘 Kolmar 的评论,尽管有一个隐式参数在起作用,它决定了如何构建结果集合,但在这种情况下,只是查询源集合以供构建器使用。

Iterable.map:

def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[Iterable[A], B, That]): That

隐式范围包括与类型 args 相关的类型,包括 IterableInt

Iterable 定义了一个 "generic" CanBuildFrom,它在源集合上调用 genericBuilder。这就是结果类型与源关联的方式。

相反,结果集合通过 CanBuildFrom[From = Nothing, _, _] 与源分离。这就是 cc.to[Set] 的表达方式,其中构建了一个 Set,而不考虑源集合 cc。对于 map 等操作,方法 collection.breakOut 提供了这样一个 CanBuildFrom,其中可以有用地推断结果类型。

您可以为所需的行为注入任意 CanBuildFrom

$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_92).
Type in expressions for evaluation. Or try :help.

scala> val m = Map("a" -> 1, "b" -> 1)
m: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 1)

scala> val k = m.keys
k: Iterable[String] = Set(a, b)

scala> import collection.{generic, mutable}, generic.{CanBuildFrom => CBF}, mutable.ListBuffer
import collection.{generic, mutable}
import generic.{CanBuildFrom=>CBF}
import mutable.ListBuffer

scala>   implicit def `as list`: CBF[Iterable[_], Int, List[Int]] =
     |     new CBF[Iterable[_], Int, List[Int]] {
     |       def apply() = new ListBuffer[Int]
     |       def apply(from: Iterable[_]) = apply()
     |     }
as$u0020list: scala.collection.generic.CanBuildFrom[Iterable[_],Int,List[Int]]

scala> k.map(m)
res0: List[Int] = List(1, 1)

值得补充的是,从 2.11.8 开始,完成可以显示类型:

scala> k.map(m) //print<tab>

$line4.$read.$iw.$iw.k.map[Int, Iterable[Int]]($line3.$read.$iw.$iw.m)(scala.collection.Iterable.canBuildFrom[Int]) // : Iterable[Int]

使用breakOut

scala> k.map(m)(collection.breakOut)
res1: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 1)

scala> k.map(m)(collection.breakOut) //print

$line4.$read.$iw.$iw.k.map[Int, scala.collection.immutable.IndexedSeq[Int]]($line3.$read.$iw.$iw.m)(scala.collection.`package`.breakOut[Any, Int, scala.collection.immutable.IndexedSeq[Int]](scala.Predef.fallbackStringCanBuildFrom[Int])) // : scala.collection.immutable.IndexedSeq[Int]

如图所示,它实际上选择了 CanBuildFrom 用于以下操作:

scala> "abc".map(_ + 1)
res2: scala.collection.immutable.IndexedSeq[Int] = Vector(98, 99, 100)

scala> "abc".map(_ + 1) //print

scala.Predef.augmentString("abc").map[Int, scala.collection.immutable.IndexedSeq[Int]](((x: Char) => x.+(1)))(scala.Predef.fallbackStringCanBuildFrom[Int]) // : scala.collection.immutable.IndexedSeq[Int]

比较:

scala> k.map(m)(collection.breakOut) : List[Int] //print

(($line6.$read.$iw.$iw.k.map[Int, List[Int]]($line5.$read.$iw.$iw.m)(scala.collection.`package`.breakOut[Iterable[String], Int, List[Int]](scala.collection.immutable.List.canBuildFrom[Int]))): scala.`package`.List[scala.Int]) // : List[Int]

canonical Q&A on breakOut.