大众号「稀有猿诉」 原文链接 Kotlin Generics Revisited

在前面的文章中学习Kotlin泛型的基本知识,而且又用了一篇文章来复习了一下Java语言的泛型,有了这些根底咱们就能够持续深化的学习Kotlin的泛型了。看它是如何解决Java泛型的遗留问题,再学习一下它的高档特性,最终再总结泛型的最佳实践。

再次深化解析Kotlin泛型

本文是作为前面文章的连续和深化,为了更好的阅览效果,主张先回忆一下Java泛型根底,和Kotlin泛型根底

泛型类型参数边界(Upper bounds)

咱们在前面讲解Java泛型根底时提到了在声明泛型的时分是能够指定类型参数的边界的,比方用Caculator<T extends Number>能够指定在运用时能够传入的类型参数要是Number或许Number的子类。

在Kotlin中也是能够指定泛型类型参数的边界的,也是用承继符号:来表明,如:

class Calculator<T : Number> { ... }

与Java相同,也能够指定多个边界,要运用where关键字

class Calculator<T> where T : Number, T : Runnable, T : Closable { ... }
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

留意:面向对象的承继体系是基类在上面,子类在下面,所以上界的意思是以某个类A为根的承继树,这颗树都能够当成A来运用;下界的意思是从根A到以某个类C为止的一个途径,这个途径上都是C的基类,C都能够当成它们来用。

再次深化解析Kotlin泛型

更高雅的泛型改变(Variance)

与Java相同,Kotlin的泛型也是不可变的Invariant,比方尽管String是Any的子类,但List<String>并不是List<Any>的子类。泛型改变Variance的目的便是让两个泛型发生与类型参数协同的改变,比方类型C是类A的子类,那么运用它的泛型<C>也应该是<A>的子类,能运用<A>的方,传入<C>必定要是答应的,并要能够是安全的。

再次深化解析Kotlin泛型

运用点改变(Use-site variance)

依据面向对象的基本特性,只要向上转型(Upcasting)是安全的。详细就分为两种场景,从一个生产者中读取对象时,只要生产者的输出声明的T是基类(T是一个上限),无论生产者输出的是T仍是它的子类,关于运用者来说(当T来用)便是安全的。这时生产者的泛型要能够进行协变,在Java顶用上界边界通配符<? extends T>来进行协变,详细运用时传入T的子类的泛型也是合法的;同理,向一个顾客中写数据时,顾客声明为T的某个基类(这时T是一个下限),向其传入T,关于运用者来说便是安全的。这时顾客的泛型要能进行逆变,在Java中运用下界边界通配符<? super T>来进行逆变,详细运用时传T的基类的泛型也是合法的。

Kotlin中提供了十分简略了解和运用的关键字out来进行协变(covariance)和in进行逆变(contravariance),能够实现Java中的边界通配符相同的成效。Java边界通配符的规则是PECS(Producer Extends Consumer Super),out正好能够更形象的描绘一个生产者,而in能够更形象的描绘一个顾客,所以Kotlin的关键字更简略了解和记忆。

open class Animal
class Dog : Animal()
class MyList<E> {
    fun addAll(from: MyList<out E>) {}
    fun getAll(to: MyList<in E>) {}
}
fun main() {
    val animals = MyList<Animal>()
    val dogs = MyList<Dog>()
    animals.addAll(dogs)
    dogs.getAll(animals)
}

这种泛型改变是发生在调用者调用时,因此也叫做『运用点改变』(Use-site variance)。在Kotlin中也被称作类型映射,由于相当所以用<out T>把T给映射成了一个T的生产者,只能调用其get办法;用<in T>映射成一个T的顾客,只能调用set办法。而且呢,关于同一个函数中既有生产者和顾客时,in和out只写一个就行了,如:

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

声明点改变(Declaration-site variance)

Java边界通配符的一个大问题是只能用于办法的参数但不能是回来值,也便是只能是『Use-site variance』。但in和out没有这个约束,因此它们能够用于回来值。只要给类和接口的泛型声明为out或许in就能让类型参数在其所有的办法发生variance,这便是『declaration-site variance』。

但是要恪守out进行协变,也便是说out是用于生产者的,只能作为办法的回来值,或许确保不能set,如:

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

同理,用in进行逆变,只能用于顾客,只能作为办法的参数,或许确保不get,如:

interface Comparable<in T> {
    operator 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, you can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

小结一下,Kotlin运用关键字in和out让泛型的协变和逆变变得简略了解得多了,由于它们能够十分清楚的表达出顾客和生产者,只需求记住一个泛型的生产者要用out来润饰,而一个泛型的顾客要用in来润饰就不会出错,这比Java中的边界通配符简略太多了。

星号映射(Star projections)

除了use-site variance是一种类型映射外,还有星号映射。首要来说星号是无界泛型,也便是说不指定详细的类型参数,意思是恣意类型的泛型,换句话说Foo<*>是任何其他泛型的基类(Foo<String>, Foo<Number>等)。但依据不同的上下文,Foo<*>会映射为不同的详细意义的泛型类型:

  • 关于Foo<out T : TUpper>,这儿的T是一个受上界TUpper约束的协变类型参数,那么Foo<*>就等同于Foo<out TUpper>。
  • 关于Foo<in T>,这儿T是逆变类型参数,Foo<*>等同于Foo<in Nothing>。这意思是无法向Foo<*>中写。
  • 关于Foot<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?>

换句话来了解,便是当不指定详细的类型参数,用星星就代表着不知道详细的类型参数,那么视详细的上下文不同星号会被解释不同的意思。不过这玩意儿可读性较差,除非必不得已,否则仍是能不用就用它。

留意:在Kotlin中,根基类是Any它是所有其他类的基类(the root of Kotlin class hierarchy)。而Nothing是不能有实例的类,能够用它来表明不存在的对象(a value that never exists)。比方说,假如 一个函数回来值类型声明为Nothing,那它就不会回来(always throws an exception),留意是不会回来(never returns),并不是没有回来值,没有回来值要声明为类型Unit

绝不为空类型(Definitely non-null type)

为了坚持对Java的互通性,Kotlin还支持把泛型类型参数声明为『绝不为空类型』definitely non-null type。能够用& Any来声明,如<T & Any>来声明T是『绝不为空类型』。

这是为了坚持与Java的彼此调用,有些Java的类和接口是用注解@NonNull润饰的,如:

public interface Game<T> {
    public T save(T x) {}
    @NotNull
    public T load(@NotNull T x) {}
}

这时在Kotlin里边就要用到**『绝不为空类型』& Any来声明泛型**:

interface ArcadeGame<T1> : Game<T1> {
    override fun save(x: T1): T1
    // T1 is definitely non-nullable
    override fun load(x: T1 & Any): T1 & Any
}

留意,在纯Kotlin代码中是用不到这个特性的。只要当触及Java的@ NonNull时才需求『绝不为空类型』。

下划线操作符

当编译器能揣度出泛型的类型参数时是能够省掉掉类型参数的,比方val names = listOf(“James”, “Kevin”),这儿得到的类型是List<String>,但咱们并没有显示的指定类型参数,这是由于编译器从listOf的参数中就能揣度出类型参数是String,所以listOf的回来便是List<String>。

但有些时分,泛型类型太复杂了,没有办法揣度出所有的类型,比方有多元泛型参数时。但依据指定的某一个参数,能够揣度出剩余的参数时,这时就没有办法彻底省掉类型参数,剩余的参数却又能够揣度出来,写了又浪费。这时就能够用下划线操作符来代表那些能够揣度出来的参数。这儿的下划线用法跟在lambda中,用下划线代替不运用的参数是相同的。

abstract class SomeClass<T> {
    abstract fun execute() : T
}
class SomeImplementation : SomeClass<String>() {
    override fun execute(): String = "Test"
}
class OtherImplementation : SomeClass<Int>() {
    override fun execute(): Int = 42
}
object Runner {
    inline fun <reified S: SomeClass<T>, T> run() : T {
        return S::class.java.getDeclaredConstructor().newInstance().execute()
    }
}
fun main() {
    // T is inferred as String because SomeImplementation derives from SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")
    // T is inferred as Int because OtherImplementation derives from SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}

参考资料

欢迎查找并重视 大众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「分享」 总要有一个吧!