为啥这个Iterable在映射后会产生一个Set?

Posted

技术标签:

【中文标题】为啥这个Iterable在映射后会产生一个Set?【英文标题】: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]

【问题讨论】:

【参考方案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 一样,我认为该机制是用静态类型编码的。

以上是关于为啥这个Iterable在映射后会产生一个Set?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 Clojure 的瞬态映射中插入 1000 000 个值会产生一个包含 8 个项目的映射?

mysql 数据库乱码问题,页面,数据库都是UTF-8 的字符集,为啥INSERT INTO插入后会是乱码呢?

foreach的使用原理简单解析

为啥 map over 一个 iterable 返回一个一次性的 iterable?

为啥在“set /p”提示时使用 Ctrl+C 取消 Windows 批处理脚本会产生不一致的行为?

有人能告诉我为啥这个 JSfiddle 在播放完后会重复第一首歌吗?