Skip to content

Latest commit

 

History

History
380 lines (292 loc) · 15.4 KB

2012-09-10-generics.md

File metadata and controls

380 lines (292 loc) · 15.4 KB
layout title original-doc
doc
泛型 | Generics

泛型类

Java中一样,Kotlin中的类也可以有类型参数: {% highlight java %} class Box(t : T) { var value = t } {% endhighlight %}

通常来说,若要新建这些类型的实力,需要提供具体的类型参数: {% highlight java %} val box : Box = Box(1) {% endhighlight %}

但是如果类型参数可以推导出来,即从构造函数的参数类型,或通过其他的方式推导,则可以省略掉类型参数:

{% highlight java %} val box = Box(1) // 1是Int类型,所以编译器可以推导出来我们在使用Box {% endhighlight %}

泛型类型在运行时是保留的

Java不一样的地方在于,Kotlin中类的类型信息在运行时是保留的。 这样我们就可以在运行时完整地检查对象的类型: {% highlight java %} if (b is Box) { // 'is' 替换了 Java 的 'isinstanceof' 符号 // b 保证是 Box, 而不仅仅是Box } {% endhighlight %}

我们甚至可以在is检查中使用类型参数: {% highlight java %} if (foo is T) { // 这是个有用的检查 // ... } {% endhighlight %}

同样,我们还可以新建泛型数组: {% highlight java %} class ArrayDemo { val array = Array() // Array就是一个普通类 } {% endhighlight %}

并且,不,Kotlin并没有 生肉类型(raw types),并且类型擦除也不像在Java中那样扮演重要角色。 比如,上述的Box<T>类,因为是泛型类,所以并不能使用不带类型参数的Box类。 这和Effective Java中的 第23条:不要在新代码中使用生肉类型 和第24条:消除unchecked警告相符合。

运行时泛型还没有实现

型变 (Variance)

Java的类型系统中最复杂的部分就是通配符类型(参看Java泛型FAQ)。 而Kotlin并没有这个特性,而是引入了另外两种特性:声明处型变(Declaration-site Variance)和类型映射(Type Projection)。

首先,我们思考一下为什么Java需要那些神秘的通配符。 这个问题在Effective Java第28条:使用限制的通配符增强API灵活性有所解释。 首先,Java中的泛型是不可型变(invariant)的,这意味着List<String>不是List<Object>的子型。 为何如此?如果List是型变的,那它就和Java的数组没什么区别了,因为下面的代码会编译通过,而在运行时抛出异常:

{% highlight java %} // Java List strs = new ArrayList(); List objs = strs; // !!! The cause of the upcoming problem sits here. Java prohibits this! objs.add(1); // Here we put an Integer into a list of Strings String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String {% endhighlight %}

所以,Java禁止这个特性,以保证运行时的类型安全。但这样也带来一些问题。 比如,考虑Collection接口的addAll()方法。这个方法的签名应该是什么样的? 直观地我们,我们会这么写: {% highlight java %} // Java interface Collection ... { void addAll(Collection item); } {% endhighlight %}

但是这样的话,我们就不能做下面的简单的操作了(这个操作是完全安全的): {% highlight java %} // Java void copyAll(Collection to, Collection from ) { to.addAll(from); // !!! 不能编译通过!因为 Collection 不是 Collection 的子型 } {% endhighlight %}

(在Java中,我们历经教训才学会了这一课,参考Effective Java第25条:优先选择List,而不是Array)

这就是为什么addAll()实际上的签名是: {% highlight java %} // Java interface Collection ... { void addAll(Collection<? extends E> items); } {% endhighlight %}

它和里,通配符参数? extens T表示这个方法接受一个集合,其元素的类型是T的某个子型,而不是T本身。 这意味着我们可以安全的从集合中读出T类型的元素(因为元素的类型是T的子型), 但是不能往其中写入T类型的参数,因为我们不知道具体是哪一个符合T子型的类型。 虽然有这个限制,但我们得到了期望的特性:Collection<String>Collection<? extends Object>的子型。 用更“技术”的话说,带extend边界(上边界)的通配符使类型变成协变类型(covariant types)。

理解这个技巧的原理,关键点其实很简单:如果你只能从集合中拿出东西,那么从String的集合中拿出Object是可以的。 相反地,如果你只能往一个集合里放东西,那么往一个Object集合中放置一个String:在Java中,我们有 List<? super String>,它是List<String>父型。 这被称为逆变类型,即你调用这种集合的方法时,只能使用String类型参数。 (比如List<? super String>集合,可以调用add(String)或者set(int, String)), 但是如果调用某个返回T的方法,只能得到Object,而不是String。

Joshua Bloch称那些你只能从中读取的对象为生产者(producers),而那些只能往其中写数据的对象为消费者(consumers)。 他建议:"为了获得最大的灵活性,在代表生产者和消费者的输入参数最好使用通配符类型", 并且他提出了下面的助记词:
PECS 表示producer-extends, consumer-super.

注意:如果你使用一个生产者对象,如List<? extends Foo>,不允许调用add()或者set()方法,但这并不代表这个集合是不可变(immutable)的。 比如,没谁阻止你调用clear()来删除列表中的全部元素,因为这个方法并不需要类型参数。 通配符(或其它的型变机制)唯一保证的事情是类型安全。不可变性是另一个故事。

声明处型变(Delaration-site variance)

假设我们有一个泛型接口Source<T>,但它没有带泛型参数T的方法,而只有一个方法返回T: {% highlight java %} // Java interface Source { T nextT(); } {% endhighlight %}

那么,在类型为Source<Object>的变量中存入一个Source<String>引用,应该是完全安全的-- 并没有可以调用的消费者方法。但是Java并不知道这一点,所以仍然禁止: {% highlight java %} // Java void demo(Source strs) { Source objects = strs; // !!! 在Java中不允许 // ... } {% endhighlight %}

为了解决这个问题,我们必须将objects声明为Source<? extends Object>类型。 但这样做没有太多意义,因为声明变复杂后,在这个变量上可以调用所有的方法,和之前是一样的。 即我们增加了类型的复杂度,却没有带来附加价值,仅仅是因为编译器并不知道才增加的。

在Kotlin中,有一种办法可以告诉编译器这个事情。这个机制被称为声明处型变: 我们可以给类Source的类型参数T加上标注,以保证它只从Source<T>类的成员中返回(生产),而从来不消费。 这里我们使用out修饰符。

{% highlight java %} abstract class Source { fun nextT() : T }

fun demo(strs : Source) { val objects : Source = strs // 这样是可以的,因为T是一个out参数 // ... } {% endhighlight %}

基本的规则是:如果类C的类型参数T被声明为out,那么T就只能出现在成员的out位置(out-position), 而加上这个限制后,这样得到的好处是,C<Base>C<Derived>的父型了。

用更“技术”的话来说,类C在T类型参数上是协变的,或者说T是一个协变类型参数。 这时候你可以把类型C看作T的生产者,而不是T的消费者

out修饰符被称为协变标注,而且因为它是在类型声明的地方标记的,因此称为声明处协变。 这和Java使用处协变不同,Java实在使用类型时确定一个类的型变性的。

out之外,Kotlin还提供了互补的型变标注:in。它会使一个类型参数变成逆变类型参数:这个类型只能消费,不能生产。 逆变型参的一个很好的示例是Comparable

{% highlight java %} abstract class Comparable { fun compareTo(other : T) : Int }

fun demo(x : Comparable) { x.compareTo(1.0) // 1.0 的类型是Double, 是Number的子型 // 所以我们可以把x赋值到类型为Comparable的变量y上 val y : Comparable = x // OK! } {% endhighlight %}

我们认为inout这两个词都自明其义(并且他们在C#中已经成功很久了), 所以上面提到的助记词并不是很有必要,但可以为了更高的目的改述它: **存在主义的转述:Consumer in, Producer out! **:-)

使用处型变: 类型映射

使用out标记类型参数T非常方便,并且不用再烦恼使用时类型转换。 是的,这样很方便,但只是当这个类确实只返回T(生产者)的时候。 但是如果不是呢?Array是个很好的例子: {% highlight java %} class Array(val length : Int) { fun get(index : Int) : T { /* ... / } fun set(index : Int, value : T) { / ... */ } } {% endhighlight %}

这个类在T参数上既不是协变也不是逆变。而什么都不指定,也会带来一些不灵活。 考虑下面的函数: {% highlight java %} fun copy(from : Array, to : Array) { assert {from.length == to.length} for (i in from.indices) to[i] = from[i] } {% endhighlight %}

这个函数的目的是从一个数组拷贝数据到另一个数组。我们试试实际使用它: {% highlight java %} val ints : Array = array(1, 2, 3) val any = Array(3) copy(ints, any) // Error: 期望 (Array, Array) {% endhighlight %}

这里我们遇到了似曾相识的问题:Array<T>对T是不可型变的。所以Array<Int>Array<Any>两个类型完全没有父子关系。 为什么? 因为如果不这样的话,copy就可以做一些坏事了。 比如它可以试着往from数组中写入一个String, 而如果我们传入的参数实际上是Array<Int>, 那么运行下去的某个时刻,总会抛出ClassCastException...

所以我们唯一想确保的事情就是copy()不做坏事。我们希望禁止它往from参数中写入数据,我们可以这么做: {% highlight java %} fun copy(from : Array, to : Array) { // ... } {% endhighlight %}

这里发生的事情称作类型映射: 我们说from不仅仅是个简单的数组,而且是个受限制的(映射了的)数组: 我们只能调用它的返回T的方法,在这个例子中即我们只能调用get()。 这就是我们的使用处型变的方式。它相当于JavaArray<? extends Object>,但是稍微简单一些。

你也可以用in来映射类型参数: {% highlight java %} fun fill(dest : Array, value : String) { // ... } {% endhighlight %}

Array<in String> 相当与Java的Array<? super String>,即你可以传入一个CharSequence数组,或者Object数组到fill()函数中。

星号映射

有时候你会说你不知道类型参数的信息,但仍然想安全地使用它。我们说的安全是指我们想使用out映射(对象不接受消费任何未知类型的值)。 而且这个映射是限制在对应的参数的类型上限里的(即 out Any?,在大多数情况下)。 这种功能,我们有一个简单的写法,称为星号映射Foo<*> 代表 Foo<out Bar>,其中Bar是Foo的类型参数的型上限。

注意:星号映射和Java的生肉类型很像,但不同的是,它是类型安全的。

泛型函数

不止类可以使用类型参数,函数也可以。通常,在函数名之后用尖括号放置函数的类型参数: {% highlight java %} fun singletonList(item : T) : List { // ... } {% endhighlight %}

但是对于扩展函数来说,在函数名只前声明类型参数是有必要的, 因此Kotlin也支持下面的替代语法:

{% highlight java %} fun T.basicToString() : String { return typeinfo.typeinfo(this) + "@" + System.identityHashCode(this) } {% endhighlight %}

如果在使用的时候指定类型参数,则只能跟在函数名称之后: {% highlight java %} val l = singletonList(1) {% endhighlight %}

泛型限制

一个类型参数能够选择使用的类型的集合,是可以通过泛型限制来进行限制的。

上界

最常用的限制是上界,和Javaextends关键字类似: {% highlight java %} fun sort<T : Comparable>(list : List) { // ... } {% endhighlight %}

类型参数后面的冒号之后就是该参数的上界:即只有Comparable<T>的子型才能置换T。 比如:

{% highlight java %} sort(list(1, 2, 3)) // OK. Int是Comparable的子型 sort(list(HashMap<Int, String>())) // Error: HashMap<Int, String> 不是 Comparable<HashMap<Int, String>>的子型 {% endhighlight %}

如果不提供上届的话,默认上届是Any?。在尖括号里只能指定一个上界,如果需要更多的上界的话,需要一个单独的where语句: {% highlight java %} fun cloneWhenGreater<T : Comparable>(list : List, threshold : T) : List where T : Cloneable { return list when {it > threshold} map {it.clone()} } {% endhighlight %}

类对象

还有一种泛型限制:类对象的泛型限制。 它们用来限制类对象的属性,以使之在替换T参数时,保证有某个属性。

考虑下面的例子。假设我们有一个类Default,它有一个属性default,用来存放该类的默认值

{% highlight java %} abstract class Default { val default : T } {% endhighlight %}

比如,Int类可以这样扩展继承Default: {% highlight java %} class Int { class object : Default { override val default = 0 } // ... } {% endhighlight %}

现在我们考虑 一个函数,接受一系列可空的T参数,即T?,并且将所有的null替换成默认值。 {% highlight java %} fun replaceNullsWithDefaults(list : List<T?>) : List { return list map { if (it == null) T.default // 现在我们还不知道T的类对象有没有这个属性 else it } } {% endhighlight %}

为了让这个函数通过编译,我们需要指定一个类型限制:T的类对象应该是Default<T>的子型。 {% highlight java %} fun replaceNullsWithDefaults(list : List<T?>) : List where class object T : Default { // ... {% endhighlight %}

现在编译器就知道T(当作类对象来引用时)一定有default这个属性,所以我们就可以访问它了。 这个功能能够实现,是因为在Kotlin中,泛型类型是保留到运行时

类对象的边界还没实现
参看相关的issue

接下来