Kotlin语法(十二)-泛型(Generics)

         参考原文:http://kotlinlang.org/docs/reference/generics.html

     泛型类

         跟Java一样,Kotlin也支持泛型类:

classBox<T>(t: T) {

  var value = t

}

         

         在具体使用的时候,需要传入具体的类型:

valbox: Box<Int> = Box<Int>(1)

         

         另:通过值可以推断出类型,也可以省略类型参数:

// 1 has type Int, so the compiler figures out that we are talking about Box<Int>
val box = Box(1) 

 

     变异(Variance)

       Java通配符(wildcard types)

         该部分都是讲的Java泛型中的通配符。

         在java泛型使用中,提供类型通配符“?”,这块只是简单介绍java通配符的设计目的及基础,详细可以自行去了解下“Java 泛型通配符?”,如:

//Java

voidparseList(List<? extends Object> dataList) {

   //… …

}

         《 EffectiveJava》中解析,使用通配符为了提高API的使用灵活性(Use bounded wildcards to increase APIflexibility)。

 

         因为在java中,泛型类型是不可变的,比如:“ List<String>”不是“ List<Object>”的子类型,而是两个独立的类型,如下:

//Java

List<String>strs = new ArrayList<String>();

List<Object> objs = strs; //编译错误,类型不匹配(imcompatible type)
         假设前面的方式可行的话,会带来更多的新问题,导致在使用时类型不匹配问题:

//Java
objs.add(1); // Here we put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String

         所以,为了确保在运行时类型安全,Java的泛型设计成了不可变。

 

         但是这种不可变的泛型类型,又会带来下面的问题:

         现在定义一个泛型,是为“ Collection  ”增加“ addAll()”方法,应该是下面实现方式:

// Java

interfaceCollection<E> ... {

  void addAll(Collection<E> items);

}
         那么在使用的时候,往“Collection<Object>”中添加“Collection<String>”,该方法就没法使用了;而理论上应该是合法的,String是Object的子类,String实例是可以添加到Object的集合中的:

// Java

voidcopyAll(Collection<Object> to, Collection<String> from) {

  to.addAll(from); // !!! Would not compilewith the naive declaration of addAll:

               //Collection<String> is not a subtype ofCollection<Object>

}

 

         为了解决上面的问题,Java中使用了类型通配符方式,如“ ? extends T”表示T 及T的子类参数都可以使用,实现如下:

// Java

interfaceCollection<T> ... {

  void addAll(Collection<? extends T>items);

}

 

         通配符的上界

         通配符类型参数( wildcard type argument):“ ? extends T”(T表示通配符的上界),表示该方法可以接收T及T的子类参数。意味着可以安全的读“T” (所有的实例都是T的子类)的实例;但不能向其添加元素,因为没法确定添加的实例类型跟定义的类型是否匹配,无法检验这个操作的安全性:

//Java

//Bird Cat都是Animal的子类

public voidtestAdd(List<? extends Animal> list) {

//下面都会报编译错误的

        //因为testAdd方法传入的list的类型不确定的,就没法确保它可以添加下面的全部类型

        //若传入“List<Bird>”,只能add(bird),只有②可以添加

        //若传入“List<Dog>”,都不能添加

           list.add(new Animal("animal")); //①

           list.add(new Bird("bird"));    //②

           list.add(new Cat("cat"));      //③

}

 

         可以理解成:“Collection<String> ”是“Collection<? extends Object>”的子类型。

         这种通配符( wildcard )是通过继承一个范围类(通配符上界,upper bound)来实现类型协变。

 

         通配符的下界

         通配符类型参数(wildcard type argument):“? super T”(T表示通配符的下界)。

 

         如:“Collection<? super String> ”是“Collection<String>”的父类型;可以调用集合将String作为参数的方法(如:add(String) or set(int, String));但当从集合中获取元素时,得到是Objec对象而不是String对象。

 

         PECS

         PECS stands for Producer-Extends,Consumer-Super

         1)  通配符的上界方式,只能从中读取元素,不能添加元素,形象的称为生产者(Producers

         2)  通配符的下界方式,只能添加元素,没法直接读取下界类型的元素,形象的称为消费者(Consumers

 

         Kotlin泛型

         Kotlin没有提供相关的类型通配机制,而是通过下面两种方式:

         Ø  声明位置变异(declaration-site variance

         Ø  类型推测(type projections

 

       声明位置变异(declaration-site variance)

         在Java中,假设有一个泛型类“ Source<T>”,只有一个方法,返回参数T,没有使用T作为参数的方法:

// Java

interfaceSource<T> {

  T nextT();

}

 

         现在,“ Source<String>”实例作为一个“ Source<Object>”参数应该是类型安全的,因为“Source”类没有消费者方法( consumer-methods ,见 PECS 部分的解析);但是Java编译器不清楚这点,所以判定为非法:

// Java

voiddemo(Source<String> strs) {

  Source<Object> objects = strs;  // !!! Not allowed in Java

  // ...

  // Source<? extends Object> objs = strs; //valid

}

         在Java中,为解决这种情况,需要声明为“Source<? extends Object>”类型;实际上这种方式没什么意义,新实例调用的还是原来实例一样的方法,定义的更复杂类型并没有添加新值。但是java的编译器不知道。

 

         在Kotlin中,使用声明位置变异( declaration-site variance)方式来处理这种问题。

         声明位置变异:通过将参数T注解成只能作为返回值而不是作为传入参数;使用“out ”关键字标识。

abstract class Source<out T> {
  abstract fun nextT(): T
  //下面的函数编译错误: type parameter T is declared as 'out'
  // abstract fun add(value: T)
}


fun demo(strs: Source<String>) {
  val objects: Source<Any> = strs // This is OK, since T is an out-parameter
  // ...
}

         通用规则:当一个类C的参数化类型使用“out”关键字修饰,那么该参数化类型只能作为类C中函数的返回值;“C<Base>”可以作为“C<Derived>”的父类型返回。

         即,类C在参数T上是协变的,T是一个协变类型参数;可以理解C是参数T的生产者(producer)而非消费者(consumer)。

 

         “out”修饰符称为异变注解;当在类型参数的声明位置使用它,称之为声明位置变异(declaration-site variance)。跟Java使用位置变异( use-site variance)对比,Java是在使用位置使类型协变。

 

         有“ out”对应,Kotlin还提供一个互补的变异注解:“ in”,它使类型参数逆变( contravariant),即修饰的类型参数只能作为一个消费品( consumed )而不能作为一个生产品( produced)。“ Comparable”类就是一个很好使用“ in”的逆变实例:

abstract class Comparable<in T> {
  abstract fun compareTo(other: T): Int
}


fun demo(x: Comparable<Number>) {
  x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
  // Thus, we can assign x to a variable of type Comparable<Double>
  val y: Comparable<Double> = x // OK!
}

 

       类型推测(Type projections)

         使用位置变异:类型推测

         将类型参数T使用“out”修饰,可以非常方便解决使用位置泛型子类问题。但是向下面这种,既有做返回参数又有做函数入参的:

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) // Error: expects(Array<Any>, Array<Any>)

         现在出现了前面java部分关于泛型参数传递的问题。

         泛型类型参数是不可变的,那么“Array<Int>”与“Array<Any>”不是子父类关系,故无法将“Array<Int>”的实例当做“Array<Any>”使用。

 

         只需要确保“ copy()”能正常即可;那么需要禁止“ from”不能添加,可以这样:

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

 // ...

}

 

         这种实现方式,称为类型推测(type projection):参数“from ”不是一个简单的数组,而是受限制的(限制为一个生产者,projected),它只能调用返回参数为T的方法,意味着只能调用“get()”函数。

         就是一种使用位置变异(use-site variance),类似于Java中的“Array<? extends Object>”,实现起来较为简单些。

 

         另外与“out”对应的是“in”,用法如下:

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

  // ...

}
         “ Array<inString>”类似于Java中的“ Array<? superString>”。

 

         主角推测(Star-projections)

         有时候,对类型参数没有任何了解,但又需要安全的使用。一种安全的方式是,定义一个该泛型类型的推测(projection)类型,使泛型类型的每一个具体实例应该是推测(projection)类型的子类型。

 

         在Kotlin中,称之为主角推测(star-projection),语法规则:

         Ø  对于“Foo<out T>”,“T”是一个协变类型参数;“TUpper”表示类型参数上界(upper bound);那么“Foo<*>”等同于“Foo<out TUpper>”。当无法知道“T”时,可以从“Foo<*>”安全的读取得到“TUpper”。

         Ø  对于“Foo<in T>”,“T”是一个逆变类型参数;“Foo<*>”等同于“Foo<in Nothing>”;意味着当“T”不可知时,不能往“Foo<*>”添加任何元素。

         Ø  对于“Foo<T>”,“T”是一个不可变的类型参数;“TUpper”表示类型参数上限;当从中读取数据时“Foo<*>”同等于“Foo<out TUpper>”;当向其添加数据时,“Foo<*>”等同于“Foo<in Nothing>”。        

 

         若一个泛型类型,有几个类型参数,每个类型参数都可以单独进行推测(projected)。比如,一个泛型定义“interface Function<in T, out U>”,那么可以得到下面的主角推测(Star-projections):

         Ø  “Function<*, String>”等同于“Function<in Nothing, String>

         Ø  “Function<Int, *>”等同于“Function<Int, out Any?>

         Ø  “Function<*, *>”等同于“Function<in Nothing, out Any?>

 

         注:主角推测(Star-projections)跟Java中的原始类型( raw types)非常相似,但是更安全。

 

泛型函数(Genericfunctions)

         不仅类可以有类型参数,函数也可以。下面是一个使用类型参数的函数使用:
fun <T> singletonList(item: T): List<T> {
  // ...
}


fun <T> T.basicToString() : String {  // extension function 扩展函数
  // ...
}
         要使用一个泛型函数,在函数名称后面指定具体的类型参数:

vall = singletonList<Int>(1)

 

     泛型约束条件(Generic constraints)

      上界(Upper bounds)

         在Kotlin中,泛型约束大部分是类型上界限制,对应于Java中的“ extends  ”,如:

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

  // ...

}
         该函数限制只有“ Comparable<T>”的子类才能够作为“ T”:

sort(listOf(1, 2, 3))  // OK. Int is a subtype of Comparable<Int>


// Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>
sort(listOf(HashMap<Int, String>())) 

 

         默认的类型上界(未明确定义上界)是“ Any?”。在“<>”一次只能定义一个上界,如果相同类型参数需要定义多个上界,可以通过使用“ where”关键字来实现:

fun <T>cloneWhenGreater(list: List<T>, threshold: T): List<T>

    whereT : Comparable,

          T : Cloneable {

  return list.filter { it > threshold }.map{ it.clone() }

}



版权声明:本文为tangxl2008008原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。