Kotlin泛型

与 Java 类似,Kotlin 中的类也有类型参数:

class Box<T>(t: T) {
    var value = t
}

一般来说,要创建这样类的实例,我们需要提供类型参数:

val box: Box<Int> = Box<Int>(1)

但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:

val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box<Int>。

什么是型变

java 中的泛型是不型变的,List<String> 并不是 List<Object> 的子类型。为了保证运行安全,才这样设计,可以看看下面的代码

List<String> strs = new ArrayList<String>();
List<Object> objs = strs;
objs.add(1);
String s = strs.get(0);//在这里我们将面临 ClassCastException:无法将整数转换为字符串

但这样我们又会遇到这样的问题。例如,考虑 Collection 接口中的 addAll() 方法。

interface Collection<E> …… {
  void addAll(Collection<E> items);
}

但之后我们无法做到以下这个事情

void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from);
  // !!!对于这种简单声明的 addAll 将不能编译:
  // Collection<String> 不是 Collection<Object> 的子类型
}

这就是为什么 addAll() 的实际签名是以下这样:

interface Collection<E> …… {
  void addAll(Collection<? extends E> items);
}

熟悉 Java 肯定知道, Java 中在处理型变时使用的是通配符类型参数 extendsuper ,通配符的出现是为了保证类型安全。

Kotlin相关

声明处型变

Kotlin 中为什么不直接使用 Java 的通配符方式,而使用声明处型变的方式,我们可以看看下面这个例子:

定义一个泛型接口:

interface Source<T> {
  T nextT();
}

那么,在 Source<Object> 类型的变量中存储 Source<String> 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允许
  // ……
}

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。

out (协变)

所以在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source类型参数 Tout 来确保它仅从 Source<T> 成员中返回,并从不被消费。

interface Source<out T> {
    fun nextT(): T //T 只出现在返回位置
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
}

当一个类型参数 T 被声明为 out 时,它就只能出现在输出位置就是只能在函数的返回位置,作为回报 C<Base> 可以安全地作为 C<Derived>的超类进行转换。

由于被声明为 out 的 T 只存在于返回位置,所以在使用时如果我们定义Source<String>则返回的是 String ,定义的是Source<Any> 返回的则是 Any。这时我们把 Source<String>对象赋值给Source<Any>,则Source<Any>中真正意义上存放的是String,但函数返回的是 Any ,现在想想实际存的 String 是不是拥有所有 Any 的属性和方法,获取到返回的 Any 在使用过程中是没有任何问题的。

所以当一个类型参数 T 被声明为 out 时,只能出现在输出位置。他就有 C<Base> 可以安全地作为 C<Derived>的超类的能力。

in (逆变)

in 使得一个参数 T :只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
    val y: Comparable<Double> = x // OK!
}

in 使得一个参数 T 只能存在于消费位置,即方法中参数的类型。所以我们定义 Comparable<Number> 在使用这个类型参数时我们只能获取到 NumberComparable<Double> 在使用这个类型参数时我们只能获取到 Double,我们都知道 NumberDouble 父类,DoubleNumber 没有的属性和方法。在使用中我们是不是可以可以说 Double 拥有 Number 的所有能力。所以在传入 Number 参数的地方我们传个 Double 肯定是没有问题的。

所以我们可以用 Comparable<Double> 来接受一个 Comparable<Number>,因为在使用的时候我们当然是方法和参数更多的子类受用范围大。

类型投影

类型投影:使用处型变,在使用的地方使用 out/in 声明

看个例子:

class Array<T>(val size: Int){
    fun get(index: Int): T{ /*...*/}
    fun set(index: Int , value: T){/*...*/}
}

可以看出 Array 既不是协变的,也不是逆变的。如果他定义了如下的函数:

fun copy(from: Array<Any> , to: Array<Any>){
    assert(from.size == to.size)
    for(i in from.indices)
        to[i] = from[i]
}

在编辑如下代码是时会提示错误:

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val any = Array<Any>(3) { "" }
    copy(ints, any)//编辑器报错,Type mismatch Required:Array<Any> Found: Array<Int>
}

为什么编辑器报错,我们假设上面的 main 方法是可以执行的,这个时候如果由于编写错误,我们在 copy 函数中调用了 set 方法,这将导致结果错误,明明是 copy 但却得到一个全是 0 的数组。为了阻止 copy 做坏事,我们直接声明使用处型变,约束 from 只能调用返回函数。如下定义了 out 再去调用 from[i] = 0 编辑器提示错误。

image-20190306104501086.png

使用时的例子

fun copy(from: Array<out Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]

    println(to)
}

fun fill(dest: Array<in Number>, value: Number) {

}

fun main() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val anys: Array<Any> = arrayOf("", "", "")

    copy(ints, anys)
    fill(anys, 1.0)
}

使用 copy 函数 Array<out Any> 是协变的相当于用一个 Array<Any> 来接收 Array<Int>

使用 fill 函数 Array<in Number> 是逆变的相当于用一个 Array<Number> 来接受 Array<Any>

星投影

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。

Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo <out T : TUpper>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <*>
  • 对于 Foo <T : TUpper>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

注意:星投影非常像 Java 的原始类型,但是安全。

@UnsafeVariance

当我们的使用违反了 inout 的使用,但是我们自己是知道这样做事没有问题的,我们就可以使用 @UnsafeVariance 告诉编辑器,这个你不用管了。例子如下:

public interface List<out E> : Collection<E> {
    ...
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    ...
}

这是官方的 List 类 ,泛型是 <out E> 但是在 indexOf 中作为入参使用了,但我们知道这完全不会对我们的 list造成任何的影响,我们就可以用 @UnsafeVariance 让编辑器不要管了。

参考

[Kotlin 官网_泛型相关

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容