一篇文章教你搞清楚——Kotlin- 进阶 --- 不变型,android 开发框架 mvp
2.List<Sring>中方法返回值的范围 不得大于 List<CharSequence>的方法
List 定义如下:
// List 带泛型的定义 public interface List<E> extends Collection<E> {boolean add(E e);E get(int index);}
// List<String>定义如下:public interface List<String> extends Collection<String> {boolean add(String e);String get(int index);}
// List<CharSequence>定义如下:public interface List<CharSequence> extends Collection<String> {boolean add(CharSequence e);CharSequence get(int index);}
虽然 String get(int index);返回值的范围比 CharSequence get(int index);小,满足了第二个条件。
但 boolean add(String e);接收参数的范围明显比 boolean add(CharSequence e);要小,不满足第一个条件。
所以 List<Sring>中不是 List<CharSequence>的子类型。换句话说,把程序中的 List<CharSequence>都替换成 List<Sring>是不安全的,因为可能会存在这样的代码:
val spannable = Spannable()val list: List<CharSequence> = mutableListOf()list.add(spannable)
如果把这里的 List<CharSequence>换成 List<String>,编译器就会报错。因为 boolean add(String e);只会处理 String 类型的实参,Spannable 超出了这个范围。
不变型
把上面的例子表达成更抽象的定义如下:
假设 泛型 类 A 包含 类型参数 T,即 class A<T>,而 Type1 是 Type2 的子类,如果 A<Type1>和 A<Type2>不存在父子关系,则称 类 A 在类型参数上是不变型的。
Kotlin 和 Java 中的类都是不变型的。
这样会造成一些限制,辛辛苦苦抽象了一个方法,它接收一个 List<CharSequence>作为参数:handle(chars: List<CharSequence>),想当然地把 List<String>传递进入时,编译器会报错。。。难道需要为 List<String>重新定义一个相同的方法吗?
协变
不变型描述的是泛型类之间没有子类型关系,泛型类之间还有一种子类型关系叫协变。
协变的意思是:类与其类型参数的抽象程度具有相同的变化方向。
(试图总结某个抽象概念时,总会说出一些让人听不懂话。。。)
换句话说:当类型参数变得更具体时,类也变得更具体。当类型参数变得更抽象时,类也变得更抽象。
比如,从 List<String>到 List<CharSequence>,类型参数从 String 变为更抽象的 CharSequence,如果 List<String>到 List<CharSequence>的变化方向也是更抽象(前者是后者的子类),就称 List<T>在类型参数 T 上是协变的。(显然这个例子不是协变的而是不变型的)
如果一个泛型类是协变的,就意味着它在类的层面保留了类型参数的子类型关系
Kotlin 中,声明类在类型参数上是协变的,需要添加 out 保留字:
class MyList<out T>{ ... }
虽然将泛型类声明为协变可以让其子类型化关系更符合直觉,但这需要付出代价:
class MyList<out T> {fun set(item: T) {}//报错: Type parameter is declare as "out" but occur at "in" position in type Tfun get(): T {...}}
若 T 出现在方法的参数位,称 set(item: T)消费类型为 T 的值。
若 T 出现在返回值位时,称 get(): T 生产类型为 T 的值。
当 T 被 out 修饰后,它只能出现在返回值位,即它只能被泛型类生产而不能被消费。所以 out 会产生两个效果:
1.它保留了泛型类的子类型化。2.它限制了类型参数只能出现在返回值位。
这两点是相辅相成的:正因为它限制了类型参数不能出现在参数位,所以子类型化得以保留。正因为它保留了子类型化,所以类型参数只能出现在返回值位。
假设类型参数出现在了参数位,就会出现在这样的情况:
class MyList<String> {fun set(itme: String)fun get(): String}
class MyList<CharSequence> {fun set(itme: CharSequence)fun get(): CharSequence}复制代码因为 fun set(itme: String)可以接收的参数范围比 fun set(itme: CharSequence)小,不符合第一条退推论,所以 MyList<String>不是 MyList<CharSequence>的子类型。而添加了 out 之后,相当于告诉编译器把出错的方法删掉以保留子类型化:class MyList<out T> {fun get(): T {...}}
class MyList<String> {fun get(): String}
class MyList<CharSequence> {fun get(): CharSequence}
此时 fun get(): String 返回值的范围比 fun get(): CharSequence 小,符合第二条推论,所以 MyList<String>是 MyList<CharSequence>的子类型。
逆变
除了不变型、协变,泛型类之间还有一种子类型关系:逆变。
逆变的意思是:类与其类型参数的抽象程度具有相反的变化方向。
换句话说:当类型参数变得更具体时,类却变得更抽象。当类型参数变得更抽象时,类却变得更具体。逆变有一点反直觉,它想实现的效果是:List<CharSequence>成为 List<String>是的子类型。
如果一个泛型类是逆变的,就意味着它在类的层面反转了类型参数的子类型关系
Kotlin 中,声明类在类型参数上是逆变的,需要添加 in 保留字:
class MyList<in T>{ ... }
同样地,这需要付出代价:
class MyList<in T> {fun set(item: T) {}fun get(): T {...}//报错: Type parameter is declare as "in" but occur at "out" position in type T}
当 T 被 in 修饰后,它只能出现在参数位,即它只能被泛型类消费而不能被生产。由此可见:
out 和 int 不仅限定了参数可以出现的位置,还限定了什么类可以成为子类型。
类型投影
生活中的投影,是把一个三维物体变成二维物体,投影看上去还是那个物体只是降了一维。程序中的类型投影也是类似的意思:
将类型投影意味着保留该类型的有些能力,去掉另一些能力。通过类型投影可以动态地改变泛型类的子类型关系。
类型投影通常应用于将不变型的泛型类动态地转换成逆变或协变。比如,MutableList 就是不变型的:
public interface MutableList<E> : List<E>, MutableCollection<E> {// 类型参数出现在 out 位置 public fun removeAt(index: Int): E// 类型参数出现在 in 位置 public fun add(index: Int, element: E): Unit...}
MutableList<E>是不变型,所以泛型参数可随意出现在 in 或 out 位置。
但不变型有时候会缩小方法的适用范围,比如:
fun <T> copy(source: MutableList<T>, destination: MutableList<T>){for (item in source){destination.add(item)}}
这是一个拷贝集合的方法,引入泛型是为了避免为每一种
具体的类型都重新定义一遍方法。现在这个方法可以在任何数据类型相同的两个列表见拷贝内容。
但如果我想把一个字符串集合拷贝到可以包含任意对象的集合中怎么办?
val strings = mutableListOf( "a", "b", "c" )val anys = mutableListOf<Any>()
copy( strings, anys )// 报错
因为 copy()的定义要求源和目的集合具有相同的类型。
为了让 copy()方法能适用于这种情况,可以这样改写:
fun <R: T, T> copy(source: MutableList<R>, destination: MutableList<T>){for (item in source){destination.add(item)}}
评论