Scala的协变covariant(+),逆变contravariant(-),上界(:),下界(:)

2024-08-27 10:32

本文主要是介绍Scala的协变covariant(+),逆变contravariant(-),上界(:),下界(:),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文:https://my.oschina.net/xinxingegeya/blog/486671

Scala的协变(+),逆变(-),上界(<:),下界(>:)

协变covariant、逆变contravariant、不可变invariant

对于一个带类型参数的类型,比如 List[T],如果对A及其子类型B,满足 List[B]也符合List[A]的子类型,那么就称为covariance(协变) ,如果 List[A]是 List[B]的子类型,即与原来的父子关系正相反,则称为contravariance(逆变)。

如果一个类型支持协变或逆变,则称这个类型为variance(翻译为可变的或变型),否则称为invariance(不可变的)  

在Java里,泛型类型都是invariant,比如 List<String> 并不是 List<Object> 的子类型。

而scala支持,可以在定义类型时声明(用加号表示为协变,减号表示逆变),如: 

trait List[+T] // 在类型定义时声明为协变

这样会把List[String]作为List[Any]的子类型。

不过Java支持使用(use-site variance),也就是在声明变量时

List<? extends Object> list = new ArrayList<String>();

如下面的代码示例

List<String> aList = new ArrayList<String>();
List<? extends Object> covariantList = aList;
List<? super String> contravariantList = aList;
covariantList.add("d"); //wrong
Object a = covariantList.get(0);contravariantList.add("d"); //OK
String b = contravariantList.get(1); //wrong
Object c = contravariantList.get(2);

Java的上界和下界

使用extends关键字确定参数化类型的上界

在这里参数化类型是Date,也有可能是Date的子类,所以add方法受限制

@Test
public void upperBound(List<? extends Date> list, Date date) {Date now = list.get(0);System.out.println("now==>" + now);//list.add(date); //这句话无法编译,实际调用时传入的list可能是java.util.Date的某个子类的参数化类型
  list.add(null);//这句可以编译,因为null没有类型信息
}

使用super关键字确定参数化类型的下界

在这里,参数化类型有可能是Timestamp,有可能是其父类(包括Object)

public void lowerBound(List<? super Timestamp> list) {Timestamp now = new Timestamp(System.currentTimeMillis());list.add(now);//Timestamp time = list.get(0); //不能编译。方法返回的对象类型可能是Date甚至是Object,不能安全的向下转换到Timestamp,也就因此无法编译了
}

Scala的协变和逆变

object app_main_1 extends App {val t: Temp[Super] = new Temp[Sub]("hello world")print(t.toString)
}class Temp[+A](title: String)//支持协变

class Superclass Sub extends Super

但要注意的是,variance(变型)并不会被继承,父类声明为variance(可变类型),子类如果想要保持,仍需要声明成可变类型

如下所示,

scala> trait A[+T]
defined trait Ascala> class C[T] extends A[T]
defined class Cscala> class X; class Y extends X;
defined class X
defined class Yscala> val c:C[X] = new C[Y]
<console>:11: error: type mismatch;found   : C[Y]required: C[X]Note: Y <: X, but class C is invariant in type T.You may wish to define T as +T instead. (SLS 4.5)val c:C[X] = new C[Y]^scala>

You may wish to define T as +T instead. (SLS 4.5)

应该写成下面这样,

scala> class C[+T] extends A[T]
defined class Cscala> val c:C[X] = new C[Y]
c: C[X] = C@7241a47dscala>

Scala逆变和协变的实例分析

在Scala(以及其他许多编程语言)中,函数也是对象,可以使用、定义其他对象的地方,也可以使用、定义函数。Scala中的函数,具有apply方法的类的实例,就可以当做函数来使用。其中apply接受的参数就是函数的参数,而apply的返回值就是函数的返回值。

首先给出一个接受一个参数的函数的泛型定义

trait Function1[-T, +U] {def apply(x: T): U
}

这种函数接受一个参数,参数类型为泛型类型T,返回类型为泛型类型U。和其他支持泛型的语言一样,实际定义函数时T和U的类型会被确定下来,不过需要注意的是,这边的T之前有一个“-”,而U之前有一个“+”。

在这里引入关于这个符号的说明,在声明Scala的泛型类型时,“+”表示协变,而“-”表示逆变 

  1. C[+T]:如果A是B的子类,那么C[A]是C[B]的子类。>>>>>协变 

  2. C[-T]:如果A是B的子类,那么C[B]是C[A]的子类。>>>>>逆变 

  3. C[T]:无论A和B是什么关系,C[A]和C[B]没有从属关系。

根据Liskov替换原则,如果A是B的子类,那么能适用于B的所有操作,都适用于A。让我们看看这边Function1的定义,是否满足这样的条件。假设Bird是Animal的子类,那么看看下面两个函数之间是什么关系:

def f1(x: Bird): Animal // instance of Function1[Bird, Animal]

def f2(x: Animal): Bird // instance of Function1[Animal, Bird]

在这里f2的类型是f1的类型的子类。为什么?

我们先看一下参数类型,根据Liskov替换原则,f1能够接受的参数,f2也能接受 。在这里f1接受的Bird类型,f2显然可以接受,因为Bird对象可以被当做其父类Animal的对象来使用。

再看返回类型,f1的返回值可以被当做Animal的实例使用,f2的返回值可以被当做Bird的实例使用,当然也可以被当做Animal的实例使用。

所以我们说,函数的参数类型是逆变的,而函数的返回类型是协变的。

函数的参数类型是逆变的,而函数的返回类型是协变的 

那么我们在定义Scala类的时候,是不是可以随便指定泛型类型为协变或者逆变呢?答案是否定的。通过上面的例子可以看出,如果将Function1的参数类型定义为协变,或者返回类型定义为逆变,都会违反Liskov替换原则,因此,Scala规定,协变类型只能作为方法的返回类型,而逆变类型只能作为方法的参数类型。类比函数的行为,结合Liskov替换原则,就能发现这样的规定是非常合理的。

这种函数的泛型特性对于函数式编程非常有用。尽管C++的泛型在语法层面上不支持协变与逆变,但在C++11的function<U(T)>中,返回类型U和参数类型T也同样遵循与Scala相同的协变与逆变规则。

注: 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 

参考:http://blog.csdn.net/oopsoom/article/details/24773239

1. 协变

[+T], covariant (or “flexible”) in its type parameter T,类似Java中的(? extends T), 即可以用T和T的子类来替换T,里氏替换原则。

2. 不变

不支持T的子类或者父类,只知支持T本身。

3.逆变

[-T], contravariant, 类似(? supers T) 只能用T的父类来替换T。是逆里氏替换原则。


Scala上界(<:)和下界(>:)

1) U >: T

这是类型下界的定义,也就是U必须是类型T的父类(或本身,自己也可以认为是自己的父类)。

2) S <: T

这是类型上界的定义,也就是S必须是类型T的子类(或本身,自己也可以认为是自己的子类)。

如何使用上界和下界,看一个综合示例,

package com.usoft2//出版物类
class Publication(val title: String)//书籍类
class Book(title: String) extends Publication(title)//图书库类
object Library {//定义图书库内所有的书籍
  val books: Set[Book] = Set(new Book("Programming in Scala"),new Book("Walden"))//打印所有图书内容,使用外部传入的函数来实现
  def printBookList(info: Book => AnyRef) {//确认Scala中一个参数的函数实际上是Function1特征的实例
    assert(info.isInstanceOf[Function1[_, _]])//打印
    for (book <- books)println(info(book))}//打印所有图书内容,使用外部传入的GetInfoAction特征的实例来实现
  def printBokkListByTrait[P >: Book, R <: AnyRef](action: GetInfoAction[P, R]) {//打印
    for (book <- books)println(action(book))}}//取得图书内容特征,P类型参数的类型下界是BookR类型参数的类型上界是AnyRef
trait GetInfoAction[P >: Book, R <: AnyRef] {//取得图书内容的文本描述,对应()操作符
  def apply(book: P): R
}//单例对象,文件的主程序
object Customer extends App {//定义取得出版物标题的函数
  def getTitle(p: Publication): String = p.title//使用函数来打印
  Library.printBookList(getTitle)//使用特征GetInfoAction的实例来打印
  Library.printBokkListByTrait(new GetInfoAction[Publication, String] {def apply(p: Publication): String = p.title})
}

参考:

http://hongjiang.info/scala-covariance-and-contravariance/

http://fineqtbull.iteye.com/blog/477994

http://deltamaster.is-programmer.com/posts/48772.html?utm_source=tuicool

================================END================================


这篇关于Scala的协变covariant(+),逆变contravariant(-),上界(:),下界(:)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1111418

相关文章

【scala 安装错误】错误: 找不到或无法加载主类 scala.tools.nsc.MainGenericRunner

错误: 找不到或无法加载主类 scala.tools.nsc.MainGenericRunner 原因: Scala安装路径中包含空格。 解决办法:scala 不要安装在E:\Program Files 这种有空格的目录下,简直坑

Kotlin 范型之协变、逆变、不变

一. 前言 Kotlin 中类和类型是不一样的概念。型变是指类型转换后的继承关系。Kotlin 的型变分为逆变、协变和不变。 二. 协变 如果 A 是 B 的子类型,并且Generic<A> 也是 Generic<B> 的子类型,那么 Generic<T> 可以称之为一个协变类。 2.1Java 上界通配符<? extends T> Java 的协变通过上界通配符实现。 如果

stl-二分找上下界

下面介绍两个函数用来查找一个有序序列关键字的上下界 upper_bound返回第一个大于的元素的下标;  lower_bound返回第一个大于或等于元素的下标; 代码如下: #include<stdio.h>#include<algorithm>using namespace std;int main(){int data[10] ={0,1,1,2,2,2,2,3,4,7};//u

Scala模式匹配下提取器构造

示例代码: object :> {def unapply[A] (list:List[A]) = {Some( (list.init,list.last) )}}object Extractor_Advanced {def main(args: Array[String]): Unit = {(1 to 9).toList match{ case _ :> 9 => println(

从spark源码的角度思考scala中的模式匹配

1.scala中模式匹配 2.spark源码中的模式匹配思考 spark中master会收到worker发过来的akka的消息, 此消息是case class即(Master.class中): case class RegisterWorker(id:String,host:String,port:Int,cores:Int,memory:Int,webUiPort:int

Scala界面事件处理

示例代码: import scala.swing.SimpleSwingApplicationimport scala.swing.MainFrameimport scala.swing.Buttonimport scala.swing.Labelimport scala.swing.Orientationimport scala.swing.BoxPanelimpo

Scala界面Panel、Layout初探

示例代码: package com.dt.scala.guiimport scala.swing.SimpleSwingApplicationimport scala.swing.MainFrameimport scala.swing.Buttonimport scala.swing.Labelimport scala.swing.Orientationimport scal

scala界面GUI编程实战初步了解

示例代码: import scala.swing._//SimpleSwingApplication继承自SwingApplication类(此类中有main方法,因此可以运行显示界面)object Hello_GUI extends SimpleSwingApplication {def top = new MainFrame{ //顶级容器title = "Hello GUI"co

Scala并发编程react、loop代码实战详解

示例代码及注释: //scala并发编程中的react和loop,共同特点://通过线程存用的方式让性能有所提升。//Actor本身的运行,被actor子系统管理的时候,会有一个或者多个远程的线程让当前的actor使用//一般情况下每个Actor都有自己的线程。只有有自己的线程时,我们的Actor中的actor方法才会执行。//但是,这样线程的开销会非常大,所以为了共用线

scala并发编程原生线程Actor、Case Class下的消息传递和偏函数实战

参考代码: import scala.actors._case class Person(name:String,age:Int)class HelloActor extends Actor{def act(){while(true){receive{case Person(name,age)=>{ //偏函数println("Name: "+ name + ":" +"Age:"