理解协方差列表[+ A]

Kev*_*ith 2 scala

查看List.scala的源代码:

sealed abstract class List[+A] extends ... 

  ...

  def isEmpty: Boolean
  def head: A
  def tail: List[A]
Run Code Online (Sandbox Code Playgroud)

List[+A]是基于的协变+A.这是否意味着,可以创建一个List[T]T可以是类型本身或其任何子类?

例:

scala> trait Kid
defined trait Kid

scala> case class Boy(name: String) extends Kid
defined class Boy

scala> case class Girl(name: String) extends Kid
defined class Girl

scala> val list: List[Kid] = List(Boy("kevin"), Girl("sally"))
list: List[Kid] = List(Boy(kevin), Girl(sally))
Run Code Online (Sandbox Code Playgroud)

分别观察headtail类型是AList[A].一旦我们定义了List[+A],那么headtails A也是协变的?

我已经读过这个StackOverflow的答案 3或4次,但我还不明白.

Vla*_*eev 8

您的示例与方差无关.而且,头部和尾部也与方差无关.

scala> val list: List[Kid] = List(Boy("kevin"), Girl("sally"))
list: List[Kid] = List(Boy(kevin), Girl(sally))
Run Code Online (Sandbox Code Playgroud)

这是否会甚至工作List不协变,因为Scala会自动推断的共同超BoyGirl,那就是,Kid和右侧的表达式的类型将是List[Kid],你需要在左侧什么.

下面,但是,工作,因为java.util.List不是协变(这是不变的,因为它是Java类型):

scala> import java.util.{List => JList, Arrays}
import java.util.{List=>JList, Arrays}

scala> trait Kid
defined trait Kid

scala> case class Boy(name: String) extends Kid
defined class Boy

scala> val list1 = Arrays.asList(Boy("kevin"), Boy("bob"))
list1: java.util.List[Boy] = [Boy(kevin), Boy(bob)]

scala> val list2: JList[Kid] = list1
<console>:12: error: type mismatch;
 found   : java.util.List[Boy]
 required: java.util.List[Kid]
Note: Boy <: Kid, but Java-defined trait List is invariant in type E.
You may wish to investigate a wildcard type such as `_ <: Kid`. (SLS 3.2.10)
       val list2: JList[Kid] = list1
                               ^
Run Code Online (Sandbox Code Playgroud)

Arrays.asList 方法有这样的签名:

def asList[T](args: T*): java.util.List[T]
Run Code Online (Sandbox Code Playgroud)

由于java.util.List[T]是不变的,因此无法将JList[Boy](list1)分配给JList[Kid](list2).并且有一个原因:如果你可以,那么因为它JList是可变的,你也可以添加任何扩展Kid(不仅Boy)到同一个列表中的东西,打破类型安全.

另一方面,scala.List 在完全相同的情况下工作:

scala> val list1 = List(Boy("kevin"), Boy("bob"))
list1: List[Boy] = List(Boy(kevin), Boy(bob))

scala> val list2: List[Kid] = list1
list2: List[Kid] = List(Boy(kevin), Boy(bob))
Run Code Online (Sandbox Code Playgroud)

那是因为scala.List它的类型参数是协变的.请注意,协变List类型的作用就好像List[Boy]是子类型List[Kid],非常类似于您可以将所有内容分配给类型变量的情况,Any因为每个其他类型都是子类型Any.这是非常有用的类比.

逆变法以非常相似的方式起作用,但在其他方向上起作用.考虑这个特点:

trait Predicate[-T] {
  def apply(obj: T): Boolean
}

object Predicate {
  // convenience method to convert functions to predicates
  def apply[T](f: (T) => Boolean) = new Predicate[T] {
    def apply(obj: T) = f(obj)
  }
}
Run Code Online (Sandbox Code Playgroud)

注意-before T参数:它是一个逆变量注释,也就是说,它Predicate[T]被定义为唯一的类型参数的逆变.

回想一下,协变列表List[Boy]是一个子类型List[Kid].好吧,对于逆变谓词,它以相反的方式工作:Predicate[Kid]是子类型Predicate[Boy],因此您可以将类型的值赋给类型Predicate[Kid]的变量Predicate[Boy]:

scala> val pred1: Predicate[Kid] = Predicate { kid => kid.hashCode % 2 == 0 }
pred1: Predicate[Kid] = Predicate$$anon$1@3bccdcdd

scala> val pred2: Predicate[Boy] = pred1
pred2: Predicate[Boy] = Predicate$$anon$1@3bccdcdd
Run Code Online (Sandbox Code Playgroud)

如果Predicate[T]不逆变,我们就不能够分配pred1pred2,但它是完全合法的,安全的:很显然,在超类型定义的谓词可以很容易地亚型工作.


简而言之,方差会影响参数化类型之间的类型兼容性.List是协变的,因此您可以将类型的值赋给类型List[Boy]的变量List[Kid](事实上​​,对于任何T扩展S,您可以将类型的值赋给类型List[T]的变量List[S]).

在另一方面,因为,Predicate是逆变,可以分配Predicate[Kid]Predicate[Boy](即,对于任何T延伸S,可以分配类型的值Predicate[S],以类型的变量Predicate[T]).

如果类型在其类型参数中是不变的,则上述任何一种都不能完成(如下所示JList).

请注意参数化类型与其参数之间的对应关系:

T <: S   ===>   List     [T] <: List     [S]  (covariance)
T <: S   ===>   Predicate[S] <: Predicate[T]  (contravariance)
Run Code Online (Sandbox Code Playgroud)

这就是为什么第一个效果被称为*co*方差(T <: S在左边和
..T.. <: ..S..右边),第二个是*contra*方差(T <: S在左边,但..S.. <: ..T..在右边)的原因.

是否使自己的参数化类型协变或逆变或不变取决于您的类职责.如果它只返回泛型类型的值,那么使用协方差是有意义的.List[T]例如,仅包含返回的方法T,从不接受T作为参数,因此为了增加表现力而使其协变是安全的.这种参数化类型可以称为生产者.

如果你的类只接受泛型类型的值作为参数,不返回它们(完全像Predicate上面有单个方法def apply(obj: T): Boolean),那么你可以安全地使它逆变.这种参数化类型可以称为消费者

如果您的类既接受并返回泛型类型的值,即它既是生产者又是消费者,那么您别无选择,只能将该类保持在此泛型类型参数中.

这个成语通常被称为"佩奇"("生产者extends,消费者super"),因为方差批注写入extendssuperJava编写的.