TINY Talk is cheap.

Scala&Java的协变与逆变

2019-01-30
tiny

前言

第一次听说协变&逆变是刚接触Scala的时候,但协变&逆变却不是Scala所特有的,Java, C#等语言也有协变&逆变的概念,本文首先会解释协变&逆变这两个术语的含义,然后进一步探讨它们在Scala中具体的书写语法和使用场景。

术语讲解

协变&逆变的概念入门可以关注下边这两个篇文章,本段内容也是学习这两篇文章所得到的:Covariance and Contravariance of Hosts and Visitors, Cheat Codes for Contravariance and Covariance

  • 定义三个实体Fuel, Plant, Bamboo;图中的符号>表示可替代性(类比多态:子类可作为父类使用),Plant可以做为这一种Fuel(燃料),Bamboo是一种Plant,在作为燃料的这件事情上也可以认为 Bamboo extends Plant, Plant extends Fuel
  • 定义Box:用于实体(Fuel, Plant, Bamboo)的存放; entities
  • 定义三个可消费实体的消费者Burn, Animal, Pandaconsumers

/** box contains entities */
class Box<T> {
  /**
   * 若有一个Box<Plant>,可以用BoxVisitor<Fuel,_>, BoxVisitor<Plant,_>来消费他,不能用BoxVisitor<Bamboo,_>来消费;
   * 对于BoxVisitor来讲T是逆变的,只能接收类型为T或T父类的BoxVisitor,即BoxVisitor<? super T>;
   *
   */
  public <R> R accept(BoxVisitor<? super T, ? extends R> visitor) {
      return null;
  }
}

/** consume entities in box */
class BoxVisitor<U, R> {
    public R visit(Box<? extends U> box) {}
}

class Burn<R> extends BoxVisitor<Fuel, R> {
  /**
   * 对于Burn而言,它可消费Fuel,Plant,Bamboo,所以visit可接收任何有Fuel属性的Box;
   * 由此Box<T>可以被认为是`协变`的,即如果T extends U 则 Box<T> extends Box<U>
   */
  public R visit(Box<? extends Fuel> box) {}
}

  • 协变

Burn可以燃烧一切包括:Fuel, ` Plant, BambooAnimal可以吃所有类型的植物包括:Plant, BambooPanda只吃一种植物:Bamboo`。由此,可得出下面这关系张图: covariance-of-boxes

如果消费者是Burn,我可以把Box<Fuel>, Box<Plant>, Box<Bamboo>传递给它;如果消费者是Animal,我可以把Box<Plant>, Box<Bamboo>传递给它;如果消费者是Panda,我可以把Box<Bamboo>传递给它。

对于Box<T>而言,它存储的实体对象用于被使用/消费,所以它是一个生产者,若能将Box<Fuel>传给某个BoxVisitor,则也能将Box<Plant>, Box<Bamboo>传递给这个BoxVisitor,这时Box<Fuel>中的泛型是上界(upper bound),需要用extends关键字来定义Box<T>的范围,该范围能被上文中的BoxVisitor消费。即,若BoxVisitor能接收装满TBox,则肯定能接收装满T子类的BoxBox<T>是生产者,remember: Prdoucer Extends),Box<T>随着它的泛型T进行协变,协变的简单定义:

如果:

graph LR
A(Class A) -->|extends| B(Class B)

则:

graph LR
A(Box &lt A &gt) -->|extends| B(Box &lt B &gt)

注::Java中的数组是协变的,如下列Java代码可正常编译,但运行时会抛出ArrayStoreException

Box.Bamboo[] bamboos = new Box.Bamboo[1];
bamboos[0] = new Box.Bamboo();
Stream.of(bamboos).forEach(System.out::println);
Box.Plant[] plants = bamboos;
plants[0] = new Box.Plant();
// throw ArrayStoreException at running stage.
Stream.of(plants).forEach(System.out::println);

  • 逆变 可以将装满FuelBox,传递给Burn(汽油只可以燃烧);可以将装满PlantBox传递给Burn, Animal(植物可以燃烧&吃);可以将装满BambooBox传递给Burn, Animal, Panda(竹子可以燃烧,可以给动物&熊猫吃)。由此我们可以得出下面的这张图: covariance-of-boxes

如果我有Box<Fuel>,我可以把它传递消费者Burn;如果我有Box<Plant>,我可以把它传递给消费者Animal, Burn;如果我有Box<Bamboo>,我可以把它传递给消费者Panda, Animal, Burn

对于BoxVisitor<T>本身而言,它是一个可消费<T>的消费者,若一个Box<Bamboo>能被Panda(即: BoxVisitor<Bamboo>)消费,则其必然也能被Animal(即: BoxVisitor<Plant>), Burn(即: BoxVisitor<Fuel>)消费,BoxVisitor<Bamboo>中的泛型BambooBoxVisitor的下界(lower bound),需用supper来定义BoxVisitor<T>T的取值范围,这个范围内的BoxVisitor都可用于消费Box<T>BoxVisitor<T>是消费者,remember: Consumer Super),BoxVisitor<T>是逆变的,逆变简单定义:

如果:

graph LR
A(Class A) -->|extends| B(Class B)

则:

BoxVisitor<Parent> extends BoxVisitor<Child>

graph LR
B(BoxVisitor &lt B &gt) -->|extends| A(BoxVisitor &lt A &gt)

Scala语法讲解

协变&逆变在Java中并没有上升到语法层面,而Scala在语法层面对协变&逆变进行的详细的语法设计,Scala中用类型参数[T]表示不变,[+T]表示协变,[-T]表示逆变,具体Scala中协变&逆变的语法特性&约束会在下文进行详细的分析讲解。

定义若干类结构:

class Fuel
class Plant extends Fuel
class Bamboo extends Plant

// 声明类型`Box`,其类型参数`T`是协变的
class Box[+T](ts:T*)
// 类型参数逆变
class BoxVisitor[-T]

协变(covariant)

一个类可被定义为协变的前提是:这个类型类似Producer Extends,并且是只读的(read-only),若非只读则会出现如下问题场景(ArrayStoreException异常):

// 假设Array是协变的
// 创建一个Circle数组并装入10个Circle对象
val circles: Array[Circle] = Array.fill(10)(Circle(..))
// 当Aarry[T]是协变时该行代码有效
val shapes: Array[Shape] = circles
// 将数组中第一个元素修改为Square对象(Square is a subtype of Shape)
// 若协变非read-only,此处编译通过,但运行会抛异常(向Array[Circle]添加Square对象,这是不被允许的)
shapes(0) = Square(..)

子类对象赋值给父类变量

因为Bamboo <: Plant <: Fuel而且Box[+T]协变的,所以Box[Bamboo] <: Box[Plant] <: Box[Fuel],同时由多态原则:子类可看做父类,最终得出下边有效代码:

val bamboos: Box[Bamboo] = new Box[Bamboo](new Bamboo())
// 根据协变特性,下列代码可正常运行
val plants: Box[Plant] = bamboos
val fuels: Box[Fuel] = bamboos

协变类型内部函数限定

/**
 * covariance:readonly, non-writable
 * 即:入参:限定不能将A & A的子类传递给Foo
 * 返回值:无限制
 */
trait Foo[+T] {
  // def fun[①](②)(op: ③): ④
  def fun0[U >: T](u: U)(op: T => U): U
  def fun1[U >: T](u: U)(op: T => U): U => T
}

定义的协变类Foo[+T],类内部定义函数的通式包含着组成函数的所有元素:①:类型参数定义,②:对象类型入参定义,③:函数类型的入参定义,④:返回值/函数定义,在协变类里每个位置的条件限制如下: ①类型参数:不能定义子类类型[U][U <: T]),原因:Covariant type T occurs in contravariant position in type T of value U; ②对象入参:入参的类型不能为T & subType of T,原因:Covariant type T occurs in contravariant position in type T of value u; ③函数入参:函数的出参不能为T & subType of T,原因:Covariant type T occurs in contravariant position in type U => T of value op; ④返回值:无限制; ④返回函数: 函数入参不能为T & subType of T,原因:Covariant type T occurs in contravariant position in type T => T of value fun;

In A Picture: covariance-class-inner 逆变位置:只允许定义或传入super T的类型,或是与T无父子关系的类型; 协变位置:对类型无限制,可任意定义; 个人理解:协变可看做生产者Producer Extends并且 readonly;所以协变类内部不能存在add(_ extends T)的函数,对函数的返回值则无限制,所以就有了上图中的语法约束;

逆变(contravariance)

逆变是与协变相对的一个概念,可以将逆变看做消费者Comsumer super,并且是write-only的,若非write-only会出现如下问题场景:

// 假设Array是逆变的
// 首先创建数组并装入Shape对象
val shapes: Array[Shape] = Array.fill(10)(Shape(..), Shape(..))
// Works only if Array is contravariant
val circles: Array[Circle] = shapes
// 编译异常,circles(0)实际是Shape对象,是Circle的父类
val circle: Circle = circles(0)

父类对象赋值给子类变量

  // 由于BoxVisitor是逆变的,所以下边代码可正常编译运行
  val fuelVisitor: BoxVisitor[Fuel] = new BoxVisitor[Fuel]
  val plantVisitor: BoxVisitor[Plant] = fuelVisitor
  val bambooVisitor: BoxVisitor[Bamboo] = fuelVisitor
  // 作为消费者,能消费类型`T`则必定能消费T的父类,但不一定能消费子类,
  // 因为子类在拥有所有父类可被外部访问的变量和方法的同时扩展或覆盖了父类的变量和方法

逆变类型的内部函数限定

/**
 * contravariance:write-only, non-readable
 * consumer super
 */
trait Foo[-T] {
  // def fun[①](②)(op: ③): ④
  def fun0[U <: T](u: U)(op: U => T): U
  def fun1[U <: T](u: U)(op: U => T): T => U
}

在逆变类Foo[-T]中函数通式(def fun[①](②)(op: ③): ④)的各位置的条件限制如下: ①类型参数:不能定义父类类型[U][U >: T]),原因:Contravariant type T occurs in covariant position in type T of value U; ②对象入参:无限制; ③函数入参:函数的入参不能为T & superType of T,原因:Contravariant type T occurs in covariant position in type T => U of value op;函数出参无限制; ④返回值:不能为T & superType of T,原因:Contravariant type T occurs in covariant position in type T of value fun; ④返回函数: 函数出参不能为T & superType of T,原因:Contravariant type T occurs in covariant position in type U => T of value fun1;

In A Picture: contravariance-class-inner 协变位置:只允许定义或传入extends T的类型; 逆变位置:对类型无限制,可任意定义; 个人理解:逆变可看做消费者Consumer super并且 writeonly;所以协变类内部不能存在return (_ extends T)的函数,对函数的入参则无限制,所以就有了上图中的语法约束;

不变(invariance)

不变就是平时常见的类型参数,比如ListBuffer[T], ArrayBuffer[T]等都是不变的,可以对它们进行 read & write等操作,只要满足严格的类型约束要求即可;

协变逆变应用

scala中常见的协变/逆变类有:List[+T], Seq[+A], Option[+A], Future[+T], Tuple[..], Map[A, +B], Function1[-T1, +R], CanBuildFrom[-From, -Elem, +To], etc.

Function1[-T1, +R]的另一种常见写法-T1 => +R(入参逆变,出参协变)

示例

def main(args: Array[String]): Unit = {
    // 协变
    val list = List[String]("A", "B", "CC")
    println(toInt(list))
    // 协变&逆变组合
    val fun = (v1: CharSequence) => v1.toString.map(_.toInt)
    println(toInt(fun, "ABC"))
  }
  // 协变,入参类型允许的范围`List[_ <: CharSequence]`
  def toInt(list: List[CharSequence]): Seq[Int] = list.map(_.length)
  // 协变&逆变组合使用,第一个入参类型允许的范围`Function1[_>:String, _<:Seq[Int]]`
  // 第一个入参类型范围的另一种表示:`_ >: String => _ <:Seq[Int]`
  def toInt(fun: String => Seq[Int], in: String): Seq[Int] = fun(in)

总结

本文主要对协变逆变进行了详细的语法讲解&场景演示,但这些只是开始,大家需在平时工作中多结合应用场景多练习才能达到灵活运用的目的;此外协变逆变只在scala编译期进行语法校验,不影响runtime,编译出的字节码会被部分裁切以满足JVM字节码规范。

本文示例的代码存储在工程:https://github.com/itinycheng/jvm-lang-tutorial ,包com.tiny.lang.java.generic.variancecom.tiny.lang.scala.generic.variance中。

参考


Similar Posts

Comments