Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

Kotlin学习之-5.8 泛型

$
0
0

Kotlin学习之-5.8 泛型

和Java中一样,Kotlin 也可以使用类型参数:

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

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

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

但是如果参数的类型可以推断出来,例如,从构造器的参数或者其他方法可以推断出类型,这种情况下可以神略类型参数:

val box = Box(1) // 参数‘1’是Int类型,所以编译器可以推断出类型参数是Int类型的

变量

在Java类型系统中比较难理解的是wildcard类型。详见Java泛型。 然而Kotlin中没有wildcard。 在Kotlin中,有两个其他东西来替代:declaration-site variancetype projections.
首先,我们想一下为什么Java需要这些谜一样的wildcard。 这个问题在《Effective Java》中解释过,第28条:Use bounded wildcards to increase API flexibility。第一,Java中的泛型类型是invariant的,意味着List<String>类型不是List<Object>类型的子类型。为什么这样?如果List不是invariant的,那么它就比Java 的数组Array好不到哪去,因为如下的代码就可以编译通过,但是导致运行时错误。

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // **注意** 这里会编译失败,Java不支持
objs.add(1);
String s = strs.get(0); // **ClassCastException**:Cannot cast Integer to String

因此,Java禁止这样的操作是为了保证运行时的安全。 但是这有一些暗示。例如,考虑Collection接口中的函数addAll()。 这个方法的签名应该是怎么的呢?直觉上说,我们会这样写:

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

但是这样的话,我们就无法使用下面简单的例子(这个例子是百分百安全的)

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
    to.addAll(from); // **注意**无法和addAll 的声明编译通过,Collection<String> 不是Collection<Object> 的自雷
}

这就是为什么addAll()真正的签名是这样的:

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

这里的Wildcard type argument ? extends E 表明这个方法接受一个集合,这个集合的元素的类型都是E 类型的子类,而不是E 本身。这意味着我们可以安全的从元素中读取E(这个集合的元素是E的子类的实例),但是不能写入,因为我们不知道什么对象可以和那个不知道类型的E 的子类的类型匹配。 因为这个问题的限制,我们有了想要的行为:Collection<String>Collection<? extends Object>的子类。行话就是,对于继承受限的通配符可以让一个类型协变covariant

理解为什么这样工作的关键非常简单。当你只能从集合中取元素,然后使用一个String的集合并且从它里面读取Object。 相反的, 如果你只能存元素到集合中,去一个Object的集合的元素然后存一个String元素到集合中:在Java中我们让List<? super String>List<Object>的父类。

后者叫做超协变 contravariance, 并且你只能调用哪些在List<? super String>上使用String当做参数方法的函数。 例如, 你可以调用add(String) 或者set(int, String)。当你想要调用那种从List<T> 中返回T 类型的方法,你无法得到一个String ,而只能得到一个Object

Joshua Bloch 说这些对象你只能从生产者中读取, 只能写到消费者中。

“For maximum flexibility, use wildcard types on input parameters that represent producers or consumers”

并且提出了下面的助记方法

PECS stands for Producer-Extends, Consumer-Super.

注意:如果你使用一个生产者对象,假如是List<? extends Foo>, 你不能调用它的add()set() 方法, 但是这并不意味着这个对象是不可变的。例如,没什么可以阻止你调用clear()方法来移除所有列表中的元素。因为clear()方法没有任何参数。Wildcard 只是用来保证类型安全的。可变性是另外一个不同的问题。

声明站变量(Declaration-site variance)

假设我们有一个泛型接口Source<T>,这个接口没有任何方法使用T作为参数,只是有方法返回T

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

这样就可以非常安全的使用Source<Object>引用一个Source<String>对象,因为没有消费者方法可以调用。但是Java并不知道这些,仍然禁止这样使用。

void demo(Source<String> strs) {
    Source<Object> objects = strs; // **Java中禁止**
}

为了解决这个问题,我们必须使用Source<? extends Object>类型来声明对象,但他其实没有意义,因为我们可以用和之前一样的对象调用它所有的方法,所以没有任何意义把这个弄成复杂的类型。但是编译器并不知道这些。

Kotlin中,有一个办法可以给编译器解释这种类型的问题。这就叫做“声明站变量”declaration-site variance,我们可以给参数类型T的代码做注解,来保障它只会被Source<T>的成员返回或者生产,并且永远不会被消费。我们使用out修饰符来完成这个功能。

abstract class Source<out T> {
    abstract fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs
}

基本规则是:当一个类C的参数类型T被声明成out后,它就只能出现在类C成员的out位置,这样C<Base>就可以安全当做C<Derived>的父类了。

简单的说,在参数T中类C是协变的,或者T是一个协变类型的参数。你可以把C当做T的生产者,而不是一个T的消费者。

修饰符out被称作变量注解,并且因为它用在类型参数声明站,所以我们称为声明站变量。 这和Java中的使用站变量相反的,在使用站变量中在类型中使用wildcard会让类型编程协变的。

除了out以外,Kotlin还提供一种辅助的变量注解in。 它可以让一个类型参数成为协变的:只能被消费不能被生产。一个超协变类的例子是Comparable:

abstract class Comparable<in T> {
    abstract 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!
}

我们相信关键则inout可以很好的自解释(因为他们已经在C#中成功地使用了一段时间了),因此上面的助记实际并不需要,然后可以重写成:

The Existential Transformation: Consumer in, Producer out! :-)

类型映射

使用站变量:类型映射

现在非常方便地可以把一个类型参数T定义成out,并且避免在使用站中遇到子类的问题,但是有些类实际上无法被限制成只返回T类型。下面数组的使用时一个很好的例子:

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

这个类在T中既不能是协变的也不能是超协变的。并且这强加了一些不灵活的地方。考虑下面这个函数:

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

这个函数本来应该可以把一个数组的元素全部拷贝到另一个数组中。

val ints: array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any) // 错误,期望的参数(Array<Any>, Array<Any>)

现在考虑一个类似的问题:Array<T> 中的T是不可变的, 并且Array<Int>Array<Any>都不是对方的子类。为什么?还是同样的,copy函数中可能会做一些坏事情,例如它可能会去写一个String到from中,并且如果我们实际上传递一个Int的数组,将来就很可能会出现ClassCastException.

然后,唯一我们想要保障的是copy()函数不做任何坏事。我们想要禁止它给from写值,我们可以这样:

fun copy(from: Array<out Any>, to: Array<Any>) {
    // ...
}

这里发生的事情叫作类型映射,我们认为from在这里不仅仅是一个数组,而是一个受限(映射)的数组:我们只能这样称那些返回参数类型T的方法,在这种情况下,它意味着我们只能调用get()。这就是我们使用用户站变量的方法,对应Java中的Array<? extands Object>, 但是稍微简单一些。

你也可以用in映射一个类型:

fun fill(dest: Array<in String>, val: String) {
    // ...
}

Array<in String>对应Java中的Array<? super String>,例如你可以传一个CharSequence的数组或者一个Object的数组给fill()函数。

星号映射

有时候你想要说你一点都不知道类型参数,但是仍然想要安全地使用它。安全使用它的方法是定义这样一个泛型类型的映射,这样每一个泛型的实体实例就会是这个映射的子类型。

Kotlin提供叫作星号映射的语法:

  • 对于Foo<out T>T是一个协变类型参数并且有一个上界类型TUpper, Foo<*>等价于Foo<out TUpper>. 这意味着当T是未知的时候,你可以安全的从Foo<*>中读取TUpper类型的值
  • 对于Foo<in T>T是一个超协变类型参数,Foo<*>等价于Foo<in Nothing>,这意味着如果T是未知的,你无法安全地写任何内容到Foo<*>中。
  • 对于Foo<T>,当T是一个不可变类型参数,并且有一个上界类型TUpperFoo<*>在读取值得时候等价于Foo<out TUpper>,在写入值得时候等价于Foo<in Nothing>

如果一个泛型参数有很多参数类型,并且每一个都可以被独立地映射。例如,如果类型被定义成interface Function<in T, out U> 我们可以想象下列星号映射

  • Funcion<*, String> 意思是Function<in Nothing, String>
  • Function<Int, *> 意思是Function<Int, out Any?>
  • Funcion<*, *> 意思是Function<in Nothing, out Any?>

注意,星号映射和Java中的元类型非常像, 但是是类型安全的

泛型函数

不止类可以有类型参数,函数也可以有。类型参数放置在函数名的前面:

fun <T> singletonList(item: T): List<T> {

}

// 扩展函数
fun <T> T.basicToString() : String {

}

调用一个泛型函数,在调用的时候需要在函数名后面,明确类型参数。

泛型约束

所有可能被用来替换指定类型参数的类型集合都约定成泛型约束

类型上界

最常见的类型约束就是类型上界,对应Java中的extends关键字

fun <T : Comparable<T>> sort(list: List<T>) {

}

在冒号后面指定的类型是类型上界,意味着只有Comparable<T>的子类型可以被替换成T。例如:

// 正确。Int 是Comparable<Int> 的子类型
sort(listOf(1, 2, 3))
// 错误。HashMap<Int, String> 不是Comparable<HashMap<Int, String>>的子类型
sort(listOf(HashMap<Int, String>()))

如果没有指定上界的话,那么默认的上界是Any?.在尖括号中只能指定一个类型上界。如果同一个类型参数需要多于一个类型上界,我们需要一个独立的where分支语句:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable,
            T: Cloneable {
    return list.filter { it > threshold }.map { it.clone() }
}
---

PS,我会坚持把这个系列写完,有问题可以留言交流,也关注专栏Kotlin for Android Kotlin安卓开发
这篇泛型好长,里面有很多内容发现自己Java部分就理解的不全,甚至不正确。 通过本文,还发现一个关于泛型超级详尽的文档。 收益匪浅。
本文中有很多用词可能不是官方的用语, 毕竟kotlin官方中文版还没有出,我认为可能会不准确的地方都尽量同时写上了原版英文,有问题可以交流沟通。

作者:farmer_cc 发表于2017/7/4 15:45:52 原文链接
阅读:22 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>