为什么Seq.contains接受类型Any而不是类型参数A?

Ada*_*ung 13 scala

例如:

scala> val l:List[String] = List("one", "two")
l: List[String] = List(one, two)

scala> l.contains(1) //wish this didn't compile
res11: Boolean = false 
Run Code Online (Sandbox Code Playgroud)

在Java中这种方式完成事情的原因的各种解释在这里似乎并不适用,因为Map和Set确实实现了类型安全版本contains和朋友.有没有办法contains在Seq上做一个类型安全的东西,不能把它克隆成一个Set?

Dan*_*wak 31

问题是Seq它的类型参数是协变的.这对其大部分功能都很有意义.作为一个不可变的容器,它确实应该是协变的.不幸的是,当他们必须定义一个采用某些参数化类型的方法时,这确实会妨碍.请考虑以下示例:

trait Seq[+A] {
  def apply(i: Int): A       // perfectly valid

  def contains(v: A): Boolean   // does not compile!
}
Run Code Online (Sandbox Code Playgroud)

问题是函数的参数类型总是逆变,返回类型中的协变.因此,该apply方法可以返回类型的值,A因为A协变性与返回类型一致apply.但是,contains 不能取类型值,A因为其参数必须是逆变的.

这个问题可以用不同的方式解决.一种选择是简单地创建A一个不变的类型参数.这允许它在协变和逆变环境中使用.然而,这种设计意味着Seq[String]不会是一个亚型Seq[Any].另一种选择(以及最常用的选项)是采用局部类型参数,该参数在协变类型下面.例如:

trait Seq[+A] {
  def +[B >: A](v: B): Seq[B]
}
Run Code Online (Sandbox Code Playgroud)

这个技巧保留了Seq[String] <: Seq[Any]属性,并在编写使用异构容器的代码时提供了一些非常直观的结果.例如:

val s: Seq[String] = ...
s + 1      // will be of type Seq[Any]
Run Code Online (Sandbox Code Playgroud)

+此示例中的函数结果是类型的值Seq[Any],因为类型Any的最小上限(LUB)StringInt(换句话说,最不常见的超类型).如果你考虑一下,这正是我们所期望的行为.如果使用both StringIntcomponents 创建序列,则其类型Seq[Any].

不幸的是,这个技巧虽然适用于类似的方法contains,但会产生一些令人惊讶的结果:

trait Seq[+A] {
  def contains[B >: A](v: B): Boolean    // compiles just fine
}

val s: Seq[String] = ...
s contains 1        // compiles!
Run Code Online (Sandbox Code Playgroud)

这里的问题是我们正在调用contains传递类型值的方法Int.阶看到这一点,并试图推断类型B这既是的超类型IntA,在这种情况下被实例化为String.这两种类型的LUB是Any(如前所示),因此本地类型实例化contains将是Any => Boolean.因此,该contains方法似乎不是类型安全的.

这个结果不是问题,Map或者Set因为它们的参数类型都不是协变的:

trait Map[K, +V] {
  def contains(key: K): Boolean    // compiles
}

trait Set[A] {
  def contains(v: A): Boolean      // also compiles
}
Run Code Online (Sandbox Code Playgroud)

因此,简而言之,contains协变容器类型的方法不能仅限于采用组件类型的值,因为函数类型的工作方式(参数类型中的逆变).这不是Scala的限制或不好的实现,这是一个数学事实.

安慰奖是这在实践中确实不是问题.而且,正如其他答案所提到的,你可以随时定义自己的隐式转换,contains如果你真的需要额外的检查,它会增加一个"类型安全" 的方法.