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

时间:2016-08-20 15:58:02

标签: scala dictionary collections iterator iterable

在下面的示例代码中,为什么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只返回Iterable,它也会在调用map时创建一个Set,如反射代码所示:

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

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

3 个答案:

答案 0 :(得分:5)

foo.keys会返回Set(尽管其返回类型更为通用),并且在Set上调用地图会产生另一个Set。推断或编译时间类型并不总是最精确。

即使返回类型keys,您也可以看到Set上的Set方法返回Iterable[A]

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

答案 1 :(得分:2)

挖掘Kolmar的评论,虽然隐含的参数决定了如何构建结果集合,但在这种情况下,只需查询源集合以供构建器使用。

Iterable.map

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

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

Iterable定义了一个“泛型”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$1: Char) => x$1.+(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

答案 2 :(得分:0)

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

在您的示例中,编译器能够确定foo.keys的最常见类型是Set。这听起来很合理:Set可以被视为具有缺失值的Map(也是java的HashMap / HashSet)。当你转换为iterable时,implicits会丢失,并且Set消失了(正如旁注,那些CanBuildFrom hacks不健壮,将来可能会消失,因为它们确实使现有集合的扩展变得复杂,您可能还想阅读this回答和评论。)

Scala分享了Java&#34; de-jure和事实上类型&#34;的概念。 &#34;去法律上&#34;是在方法定义中声明的,但是&#34; de-facto&#34;可能是继承人之一。这就是为什么,例如,您将Map.keys类型视为Iterable,而事实上它是Set(由Map.keySet生成,其{{1} de-jure type)。

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