【scala类型系统】协变与逆变

1. 协变与逆变

协变与逆变针对的是带类型参数的类型,例如List[T]

2. 协变

对一个带类型参数的类型,比如List[T],如果对A及其子类型B,满足List[B]是List[A]的子类型,那么称为covariance协变
在这里插入图片描述

trait A[+T] // 定义A为协变类型
class X // 定义具体类型X
class Y extends X // 定义具体类型Y,且Y是X的子类
val x:A[X] = new A[Y]{} // 因为Y是X的子类,因此A[Y]也是A[X]的子类,根据里氏替换原则,A[X]类型的变量x可以接受一个A[Y]类型实例

3. 逆变

对一个带类型参数的类型,比如List[T],如果对A及其子类型B,满足List[B]是List[A]的父类型,那么称为contravariance逆变
在这里插入图片描述

trait A[-T] // 定义A为逆变类型
class X // 定义具体类型X
class Y extends X // 定义具体类型Y,且Y是X的子类
val x: A[Y] = new A[X]{} // 因为Y是X的子类,因此A[Y]是A[X]的父类,根据历史替换原则,A[Y]类型的变量x可以接受一个A[X]类型实例

4. 可变类型

如果一个带类型参数的类型支持协变或逆变,则称这个类型为variance可变的或变型

5. 不可变类型

如果一个带参数类型的类型不支持协变或逆变,则成这个类型为invariant不可变的,java中的泛型类型都是不可变的
例如:List并不是List的子类型

6. 协变类型的定义

trait List[+T] // scala写法,此时List[String]作为List[Any]的子类型
List<? extends Object> list = new ArrayList<String>(); // java写法,使用点变型(use-site variance),所谓使用点,是在声明变量时指定类型范围
val a: List[_ <: Any] = List[String]("A") // scala兼容java泛型通配符的形式,引入存在类型(extential type),支持使用点变型

7. 可变类型不可被继承

可变类型不会被继承,父类为可变类型,子类为不变类型,如果子类需要保持可变类型,仍然需要声明

trait A[+T] // 定义A为协变类型
class X // 定义一个具体类型X
class Y extends X // 定义一个具体类型Y,且Y是X的子类
class C[T] extends A[T] // 语法:C带参数T,A带参数T,且A的参数不带+号
val t: C[X] = new C[Y] // 此时C为不可变类型,报错:Note: Y <: X, but class C is invariant in type T.You may wish to define T as +T instead
// 子类若想为可变类型
class C[+T] extends A[T] // 语法:C带参数+T,A带参数T,且A的参数不带+号
val t: C[X] = new C[Y] // 此时C为协变类型,正常返回

8. 函数类型中的协变与逆变

8.1 函数写法

写法:小括号中为入参类型,最多可以有22个,最少为0,右箭头右侧为返回类型

(T1, T2, ...) => R

8.2 通配符

_表示任意类型

val x: String => _ = null

8.3 函数参数类型的可变性

8.3.1 入参类型都是逆变

// 入参类型如果是协变类型,会报错
class In[+A]{def fun(x: A){}} // 报错error: covariant type A occurs in contravariant position in type A of value x
// 入参类型如果是逆变类型,则正常
class In[-A]{def fun(x: A){}} // 正常

8.3.2 结果类型都是协变

// 结果类型如果是逆变类型,会报错
class In[-A]{def fun():A=null.asInstanceOf[A]} // 报错:error: contravariant type A occurs in covariant position in type ()A of method fun
// 结果类型如果是协变类型,则正常
class In[+A]{def fun():A = null.asInstanceOf[A]} // 正常

9 协变点和逆变点

9.1 入参类型是逆变点

为什么入参类型是逆变点?

假设有协变类型In[+A]
父类型In[AnyRef]中的方法fun(x: AnyRef)
子类型In[String]中的方法fun(x: String)

  1. 根据里氏替换原则,父类型对象都可以被子类型对象替换,使用In[String]替换In[AnyRef]后,fun(x: String)只能接受String类型入参
  2. 原程序上下文中提供的入参变量类型仍为AnyRef,会超出子类型对象函数fun(x: String)的处理范围
  3. 子类型函数fun的参数类型应当不小于父类型函数参数类型,才能满足里氏替换原则
  4. 因此In[AnyRef]应当为In[String]的子类,所有使用In[String]的场合才可以替换为In[AnyRef],并且根据已知,String是AnyRef子类,因此类型参数A为逆变的
  5. 总结:为了满足里氏替换原则,函数参数类型必须为逆变类型

9.2 结果类型是协变点

为什么结果类型是协变点

假设有逆变类型In[-A]
子类型In[AnyRef]中的方法fun():AnyRef
父类型In[String]中的方法fun():String

  1. 根据里氏替换原则,父类型对象都可以被子类型对象替换,使用In[AnyRef]替换In[String]后,fun():AnyRef只能返回AnyRef类型结果
  2. 原上下文中需要提供的结果变量仍为String,没办法接受子类型对象函数fun():AnyRef的返回结果
  3. 子类型对象函数fun():A的返回结果必须是String的子类,才能满足里氏替换原则
  4. 因此In[AnyRef]应当为In[String]的父类型,所有使用In[AnyRef]的场合才可以被替换为In[String],并且根据已知,AnyRef是String父类,因此参数A为协变的
  5. 总结:为了满足里氏替换原则,函数返回类型必须为协变类型

9.3 函数类型的里氏替换原则

在这里插入图片描述

假设有2个函数: f1, f2; 
我们若能说f2是f1的子类,当且仅当:f2的定义域类型x2 “大于” f1的定义域类型x1,且f2的值域y2 “小于” f1的值域y1; 其中“大于”指“是父类”;“小于”指“是子类”;
进一步讲,f1(父类函数)能接受的任意参数,f2也能接受,且经由f2映射而得出的结果的类型,也一定在经由f1映射所得结果的类型的“范围内”(是其子类)
再进一步讲:当代码里有对f1的调用“f1(x)”时,你可以尽管将这里的f1换成f2而不会出现类型错误,这也就是关乎“函数类型”的“里氏替换原则(LSP)”
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鱼摆摆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值