大众号「稀有猿诉」 原文链接 Kotlin Generics Revisited
在前面的文章中学习Kotlin泛型的基本知识,而且又用了一篇文章来复习了一下Java语言的泛型,有了这些根底咱们就能够持续深化的学习Kotlin的泛型了。看它是如何解决Java泛型的遗留问题,再学习一下它的高档特性,最终再总结泛型的最佳实践。
本文是作为前面文章的连续和深化,为了更好的阅览效果,主张先回忆一下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都能够当成它们来用。
更高雅的泛型改变(Variance)
与Java相同,Kotlin的泛型也是不可变的Invariant,比方尽管String是Any的子类,但List<String>并不是List<Any>的子类。泛型改变Variance的目的便是让两个泛型发生与类型参数协同的改变,比方类型C是类A的子类,那么运用它的泛型<C>也应该是<A>的子类,能运用<A>的方,传入<C>必定要是答应的,并要能够是安全的。
运用点改变(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)
}
参考资料
- Generics: in, out, where
- Kotlin Generics
- Understanding Kotlin generics
- Kotlin generics explained with code examples
- 深化解析Kotlin 泛型
- Kotlin(六)深化了解Kotlin泛型
欢迎查找并重视 大众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!