Scala,返回类型指南 - 何时首选Seq,Iterable,Traversable

55
什么时候应该选择将给定函数的返回类型定义为SeqIterableTraversable(或者更深层次地在Seq的继承体系内)?如何做出这个决定?我们有很多默认返回Seq的代码(通常是从DB查询和连续转换开始)。我倾向于默认将返回类型设为Traversable并且当特别需要顺序时使用Seq。但我没有一个强有力的理由支持这样做。 我非常熟悉每个特质的定义,所以请不要用定义术语来回答。
5个回答

44

这是一个很好的问题。您需要平衡两个方面:

  • (1) 尽量保持 API 的通用性,以便稍后可以更改实现
  • (2) 给调用者一些有用的操作来执行集合

其中 (1) 要求您对类型不那么具体(例如 Seq 上的 Iterable),而 (2) 则要求你相反。

即使返回类型只是 Iterable,您仍然可以返回比如一个 Vector,因此如果调用者希望获得额外的功能,则可以在其上调用 .toSeq.toIndexedSeq,并且该操作对于 Vector 来说是便宜的。

作为平衡措施,我会添加第三点:

  • (3) 使用一种反映数据组织方式的类型。例如,当您可以假设数据确实具有顺序时,请使用 Seq。如果可以假设不存在两个相等的对象,请给出一个 Set。等等。

以下是我的经验法则:

  • 尝试仅使用一小组集合: SetMapSeqIndexedSeq
  • 我经常违反上一个规则,使用 List 而不是 Seq。它允许调用者使用 cons 提取器进行模式匹配。
  • 仅使用不可变类型(例如 collection.immutable.Setcollection.immutable.IndexedSeq
  • 不要使用具体实现(Vector),而是使用提供相同 API 的通用类型(IndexedSeq
  • 如果您封装了一个可变结构,请只返回 Iterator 实例,调用者可以轻松地在其上生成一个严格的结构,例如通过对其调用 toList
  • 如果你的API规模较小,并且明显针对“大数据吞吐量”,请使用IndexedSeq
  • 当然,这是我的个人选择,但我希望听起来很合理。


    目前正在讨论IndexedSeqVector的区别。从技术上讲,IndexedSeq不能保证在追加和前置操作时具有O(1)的性能,只有它的默认实现Vector才能做到。在Scala 2.9.2中,IndexedSeq使用了高效的Vector方法。但在2.10.0-M6版本中,这个特性被破坏了,不过似乎已经达成共识,要修复这个问题,使得IndexedSeq仍然能够保持同样的速度。我写这条评论只是为了提醒大家。有些人会建议使用Vector而不是IndexedSeq - 0__
    4
    使用List而非Seq的理由已经过时:"它允许调用者使用 cons 提取器进行模式匹配"。这已经不再相关,因为类似的提取器在Scala 2.10中已经添加到了Seq中。 - tksfz

    9
    • 默认情况下,应该在任何地方使用Seq
    • 需要按索引访问时,请使用IndexedSeq
    • 仅在特殊情况下使用其他内容。

    这些是“常识”指导方针。它们简单、实用,在实践中很有效,平衡了原则和性能。原则如下:

    1. 使用反映数据组织方式的类型(感谢 OP 和 ziggystar)。
    2. 在方法参数和返回类型中使用接口类型。 API 的输入和返回类型都受益于普遍性的灵活性。

    Seq 满足以上两个原则。正如在 http://docs.scala-lang.org/overviews/collections/seqs.html 中所描述的:

    序列是一种可迭代的集合,具有[有限的]长度和元素的固定索引位置,从 0 开始。

    90% 的时间,你的数据是一个 Seq。

    其他注意事项:

    • List 是一种实现类型,因此不应在 API 中使用它。例如,不能将 Vector 作为 List 使用而不经过转换。
    • Iterable 没有定义 lengthIterable 在有限序列和潜在无限流之间抽象。大多数时候,人们处理的是有限序列,所以你“拥有一个长度”,Seq 反映了这一点。通常情况下,你实际上不会使用长度。但是,它经常需要,并且很容易提供,因此请使用 Seq

    缺点:

    这些“常识”约定存在一些轻微的缺点。

    • 不能使用 List cons 模式匹配,即 case head :: tail => ...。可以使用 :++:,如此处所述。然而,重要的是,匹配 Nil 仍然像Scala:Pattern matching Seq[Nothing]中所描述的那样起作用。

    脚注:


    5

    尽可能将您的方法的返回类型指定为具体类型。这样,如果调用者想要将其保留为SuperSpecializedHashMap或将其类型化为GenTraversableOnce,他们可以这样做。这就是编译器默认推断最具体类型的原因。


    2
    这是对于实现方法的好建议,但我想要补充一点,对于抽象方法来说会有些棘手。在接口上拥有一个非常具体的返回类型可能会强制实现将其返回的对象进行不必要的转换。 - dhg
    1
    我并不完全理解这个答案。是的,调用者可以保持类型与我定义的方法返回类型的专业程度相同。但不能比那更加专业,对吗? - user1250537
    2
    问题在于这也会将你锁定在那个选择上,而这可能不是你想要的。 - Casey

    1
    我遵循的一个经验法则是,根据实现情况,尽可能使返回类型具体化,参数类型尽可能通用。这是一条易于遵循的规则,并且为您提供了关于类型属性的一致保证,同时最大限度地提供了自由度。
    比如说,如果您有一个函数实现,它只是使用像map、filter或fold这样的方法来遍历数据结构 - 这些方法都是在Traversable特质中实现的,那么您可以期望它在任何类型的输入集合上执行相同 - 无论是List、Vector、HashSet还是HashMap,因此您的输入参数应该被指定为Traversable[T]。函数的输出类型选择应该仅取决于其实现:在这种情况下,它也应该是Traversable。然而,如果在函数中强制将此数据结构转换为某个更具体的类型,例如使用toList、toSeq或toSet等方法,则应指定适当的类型。注意实现和返回类型之间的一致性。

    如果您的函数通过索引访问输入元素,则应将输入指定为IndexedSeq,因为它是提供有关方法apply有效实现的保证的最通用类型。

    对于抽象成员,相同的规则适用,唯一的区别在于您应该根据计划如何使用它们来指定返回类型,因此它们通常比实现更通用。分类选择SeqSetMap是最常见的。

    遵循这个规则,您可以保护自己免受非常常见的瓶颈情况的影响,例如项目附加到List或在Seq而不是Set上调用contains,但您的程序仍然具有良好的自由度,并且在类型选择方面保持一致。


    1
    快速提示:在Scala 2.13.x版本中,`Traversable`已经不再使用。`Iterable`更加通用,我们决定不再保持这种对称性。`Iterable`现在位于集合层次结构的顶部,而`Traversable`已经被废弃。

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