【问题标题】:Why does this Iterable produce a Set after mapping?为什么这个Iterable在映射后会产生一个Set?
【发布时间】:2016-12-27 14:53:26
【问题描述】:

在下面的示例代码中,为什么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]

【问题讨论】:

    标签: scala dictionary collections iterator iterable


    【解决方案1】:

    挖掘 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

    【讨论】:

    • 另外值得补充的是,覆盖将类型绑定到源的常用机制是使用scala.collection.breakOut。所以foo.keys.map(foo)(collection.breakOut) 将导致Vector(1, 1): scala.collection.immutable.IndexedSeq[Int]。这也允许从结果中进行类型推断,因此val l: List[Int] = foo.keys.map(foo)(collection.breakOut) 将导致运行时List(1, 1)
    • 还有一个社区回答选项,但我不知道击键。此刻必须赶路。
    【解决方案2】:

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

    在您的示例中,编译器能够确定foo.keys 的最常见类型是Set。这听起来很合理:Set 可以被视为缺少值的 Map(java 的 HashMap/HashSet 也可以这样做)。当您转换为可迭代时,隐式丢失,Set 消失(作为旁注,那些CanBuildFrom hack 并不强大,将来可能会消失,因为它们确实使现有集合的扩展变得复杂,您可能还想要阅读this答案和cmets)。

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

    最后,1 在第一个 println 中是因为在底层映射中 foo 所有值都相同,Set(1,1) 变为 Set(1)

    【讨论】:

    • 这个答案有点错误。这不是隐式魔法,更像是通常的 OOP 多态魔法。编译器无法确定foo.keysSet。它使用来自IterableCanBuildFromIterable.canBuildFrom,但canBuildFrom 在实际集合对象上调用genericBuilder,因为,是的,这是运行时的Set,这导致Set.newBuilder正在使用。
    • 另外,我认为没有任何计划让 CBF 消失。您链接到的答案不支持该主张。它说它们应该默认隐藏在文档中,并且已经实施了很长时间。
    • @Kolmar 我对“没有 CBF”的猜测是基于最近关于新系列提案的讨论。这就是为什么“可能会消失”。
    • 是这个提议github.com/lampepfl/dotty/issues/818 吗?这很有趣,感谢您指出这一点。
    • 是的,这个提议。
    【解决方案3】:

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

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

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

    【讨论】:

    • 如果是这样的话,我就不会感到困惑了。但实际上:foo.keySet 返回一个Set。 foo.keys 返回一个 Iterable。
    • @Chris SetIterable 的子类,所以Map.keys 只需调用keySet 并将Set 作为Iterable 返回:github.com/scala/scala/blob/v2.11.8/src/library/scala/…
    • 我仍然不知道为什么set.map 会产生一个集合。和 OP 一样,我认为该机制是用静态类型编码的。
    猜你喜欢
    • 2011-12-08
    • 1970-01-01
    • 2015-06-23
    • 1970-01-01
    • 2014-06-23
    • 1970-01-01
    • 2013-03-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多