scala - 了解列表[+ A]的协方差

标签 scala

查看List.scala的来源:

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

  ...

  def isEmpty: Boolean
  def head: A
  def tail: List[A]
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))

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

我已经阅读了3到4次StackOverflow answer,但我还不了解。

最佳答案

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

scala> val list: List[Kid] = List(Boy("kevin"), Girl("sally"))
list: List[Kid] = List(Boy(kevin), Girl(sally))

即使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
                               ^
Arrays.asList方法具有如下签名:
def asList[T](args: T*): java.util.List[T]

由于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))

这是因为scala.List在其type参数中是协变的。请注意,协变量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)
  }
}

请注意-参数之前的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

如果Predicate[T]不是协变的,我们将无法将pred1分配给pred2,尽管它是完全合法和安全的:显然,在超类型上定义的谓词可以轻松地在子类型上使用。

简而言之,方差会影响参数化类型之间的类型兼容性。 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)

这就是为什么第一个效果称为* co * variance(左边的T <: S
右边是..T.. <: ..S..),第二个是* contra * variance(左边是T <: S,右边是..S.. <: ..T..)。

使自己的参数化类型是协变还是逆变还是不变取决于您的类职责。如果只返回通用类型的值,则使用协方差是有意义的。例如,List[T]仅包含返回T的方法,从不接受T作为参数,因此可以使其变数以提高表达能力是安全的。这样的参数化类型可以称为生产者。

如果您的类仅接受泛型类型的值作为参数,而不返回它们(就像上面的Predicate具有单个方法def apply(obj: T): Boolean一样),那么可以放心地使其成为互变量。这样的参数化类型可以称为使用者

如果您的类既接受并返回通用类型的值,即它既是生产者又是消费者,那么您别无选择,只能将类保持不变。

这个习惯用法通常称为“PECS”(“生产者extends,消费者super”),因为方差注释是用Java编写的extendssuper

关于scala - 了解列表[+ A]的协方差,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21843805/

相关文章:

scala - 如何在 Play 中通过网络套接字向 Actor 发送消息!框架?

scala - 使用流程中的最新项目完成请求

scala - SBT 0.13.8 SettingKey.~= 方法有什么作用

scala - 在 CHISEL 中创建查找表

application.conf 中的 Scala 环境 ${USER}

Scala:按大小写分区(而不是按过滤器)

scala - 如何删除Chisel verilog后端生成的无用寄存器?

list - Scala 使用合并前置方法扩展 List 类

scala - 是否可以从 self 类型调用重写的方法?

scala - 如何使以下代码类型安全?