为什么在 Scala 中创建 List 时需要使用 Nil?

12

我对列表有一个基本问题。

当我尝试使用cons操作符创建列表时,我收到了以下错误:

scala> val someList = 1::2
<console>:10: error: value :: is not a member of Int
   val someList = 1::2
                   ^

但是如果你看下面,只要我在末尾添加Nil,它就可以工作...

    scala> val someList = 1::2::Nil
    someList: List[Int] = List(1, 2)

我想知道为什么在创建列表时至少需要一次使用Nil

Nil是数据类型吗?还是空元素?

4个回答

17

正是因为这个原因。

value :: 不是 Int 的成员

在 Scala 中,运算符实际上是对象上的函数。在这种情况下,::Nil 对象上的一个函数,它实际上是一个空列表对象。

scala> Nil
res0: scala.collection.immutable.Nil.type = List()
当你执行1::2时,Scala会在2上查找名为::的函数,但它没有找到。这就是为什么会出现错误的原因。
注意:在Scala中,如果操作符的最后一个字符不是冒号,则该操作符被调用时将在第一个操作数上进行。例如,1 + 2实际上是1.+(2)。但是,如果最后一个字符是冒号,则该操作符将在右侧操作数上调用。因此,在这种情况下,1 :: Nil实际上是Nil.::(1)。由于::返回另一个列表对象,您可以像这样链接它:1 :: 2 :: Nil实际上是Nil.::(2).::(1)

7
:结尾的中缀运算符会被解释为在右侧操作数上调用方法。所以 1 :: 2 等价于 2.::(1),即它在 2 上调用了方法 ::。同样地,1 :: 2 :: Nil 等价于 Nil.::(2).::(1)
第一个无法正常工作的原因是 2 是一个Int 类型,而Int类型没有 :: 方法。第二个可以正常工作的原因是 Nil 是一个列表,而列表确实有一个 :: 方法。由于 List.:: 的结果也是一个列表,因此您仍然可以在第一个 :: 的结果上调用::方法。

3
Nil是创建作为递归数据结构List的基本构建块。List是有用的数据结构,提供恒定的时间访问O(1)到头部(第一个元素)。
List在其最小核心上,基于3个操作构建: headtailisEmptyNilList的单例子类,因此它是表示空列表的一种特殊的独特实例。在List上定义了cons运算符::来递归地构建列表,将一个元素追加到List中。
请参见List特质和Nil对象的定义(高度简化)。
trait List[A] {
    def head: A
    def tail: List[A]
    def isEmpty: Boolean
}
case object Nil extends List[Nothing] {
    def head = throw new NoSuchElementException("head of empty list")
    def tail = throw new UnsupportedOperationException("tail of empty list")
    def isEmpty = true
}

由于任何以 : 结尾的标识符/运算符都与右侧相关,:: 运算符也与右侧相关。

当你写 1::2::3 时,Scala 尝试将此调用重写为 3.::(2.::(1))。即 3 成为第一个 :: 调用的接收器,该接收器不存在于任意数据类型(在本例中为 Int)。

这就是为什么你总是基于空列表 - Nil 来构建。将每个元素逐一添加到空列表 Nil 中。


1
任何以冒号:结尾的Scala运算符都在右操作数上定义。因此,当您编写1::2时,意味着::应该在2上定义,即在Int上定义,但实际上不是这样的。 NilList的一个值,并且在其上定义了一个方法::。因此,当您编写1 :: 2 :: Nil时,它将被计算为(Nil.::(2)).::(1)

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