Kotlin 中的泛型介绍

泛型类型参数

泛型允许你定义带类型形参的类型。当这种类型的实例被创建出来的时候,类型形参被替换成称为类型实参的具体类型。
使用方式和 Java 一样:List<String>,Map<K, V>。
同样,Kotlin 也可以自动推导类型:

val authors = listOf("Dmitry", "Svetlana")

和 Java 不同的是,Kotlin 始终要求类型实参要么被显示的说明,要么能被编译期推导出来。

泛型函数和属性

反省函数有自己的类型形参,这些类型形参在每次函数调用时都必须替换成具体的类型实参。

fun <T> List<T>.slice(indices: IntRange): List<T>

调用时可以显示的指定类型实参,也可以自动推导:

val letters = ('a' .. 'z').toList()
println(letters.slice<Char>(0 .. 2))
println(letters.slice(10 .. 13))

声明泛型类

Kotlin 声明泛型类的方法与 Java 一样:

interface List<T>{
    operator fun get(index: Int): T
}

如果继承了泛型类,就得为基础类型的泛型形参提供一个类型实参。

class StringList: List<String>{
    override fun get(index: Int): String = //...
}
class ArrayList<T>: List<T>{
    override fun get(index: Int): T = //...
}

类型参数约束

类型参数约束可以限制作为(泛型)类和(泛型)函数的类型实参的类型。
使用冒号放在类型参数名称之后,作为类型形参上界的类型紧随其后,等同于 Java 中的 extends 关键字的作用:

fun <T: Number> T sum(List<T> list)

让类型形参非空

没指定上界的类型形参将会使用 Any? 作为默认上界。
如果想保证替换类型形参的始终是非空类型,可以通过指定一个约束来实现。如果出了可空性没有任何限制,可以使用 Any。

class Processor<T: Any>{
    fun process(value: T){
        value.hashCode()
    }
}

运行时的泛型:擦除和实化类型参数

运行时的泛型:类型检查和装换

和 Java 一样,Kotlin 的泛型在运行时也被擦除了。
因为类型实参没有被保存下来,所以不能检查他们:

if(value is List<String>){...}   
Error: Cannot check for instance of erased type: List<String>

那么如果想检查一个值是否是列表,而不是 set 或者其他对象,可以使用特殊的星号投影语法来做检查:

if(value is List<*>){...} 

声明带实化类型参数的函数

虽然泛型在运行时会被擦除,但可以使用内联函数避免这种限制,内联函数的类型形参能够被实化。
inline 函数除了可以提高性能,内联代码之外,另一种场景就是类型参数可以被实化。
如果把函数声明成 inline 并且用 reified 标记类型参数,就可以实化该类型参数。

inline fun <reified T> inA(value: Any) = value is T

使用实化类型参数代替类引用

另一种实化类型参数的常见使用场景是为接收 java.lang.Class 类型参数的 API 构建适配器。
例如:

inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

变型:泛型和子类型化

变型的概念描述了拥有想用基础类型和不同类型实参(泛型)类型之间是如何关联的;例如,List<String> 和 List<Any> 之间如何关联。

为什么存在变型:给函数传递实参

fun addAnswer(list: MutableList<Any>){
    list.add(42)
}
>>val strings = mutableListOf("abc", "bac")
>>addAnswer(strings)
Error: Type mismatch: inferred type is MutableList<String> but MutableList<Any> was expected

上述例子展示了 MutableList<Any> 与 MutableList<String> 之间转换存在的问题。

类、类型和子类型

如果需要的是 A 类型的值,你都能够使用类型 B 的值(当做 A 的值),类型 B 就成为类型 A 的子类型。
例如:Int 是 Number 的子类型,但不是 String 的子类型。
一个非空类型是它的可空版本的子类型。
一个泛型类,例如 MutableList ——如果对于任意两种类型 A 个 B,MutableList<A> 既不是MutableList<B> 的子类型也不是它的超类型,他就被称为在该类型参数上是不变型的。
而对于另外一些类,例如 List,如果 A 是 B 的子类型,那么 List<A> 就是 List<B> 的子类型。这样的类或者接口被称为协变的。

协变:保留子类型化关系

一个协变类是一个泛型类(我们以 Producer<T> 为例,对这种类来说,下面的描述是成立的:如果 A 是 B 的子类型,那么 Producer<A> 就是 Producer<B> 的子类型。我们说,子类型化被保留了。
Kotlin 中,要声明类再某个类型参数上是可以协变的,在该类型参数的名称前加上 out 关键字即可。

interface Producer<out T>{
    fun producer(): T
}

将一个类的类型参数标记为协变的,在该类型实参没有精确匹配到函数中定义的类型形参时,可以让该类的值作为这些函数的实参传递,也可以作为这些函数的返回值。
你不能把任何类都变成协变的:这样不安全。让类在某个类型参数变为协变,限制了该类中对该类型参数使用的可能性。要保证类型安全,他只能用在所谓的 out 位置,意味着这个类只能生产类型 T 的值而不能消费它们。
在类成员的声明中类型参数的使用可以分为 in 位置和 out 位置。如果函数把 T 当成返回类型,我们说它在 out 位置。这种情况下,该函数生产类型为 T 的值。如果 T 用作函数参数的类型,他就在 in 位置。这样的函数消费类型为 T 的值。
参数类型 T 上的关键字 out 有两层含义:

  • 子类型化会被保留(Producer<Cat> 是 Producer<Animal>)的子类型
  • T 只能用在 out 位置

类型形参不光可以直接当做参数类型或者返回类型使用,还可以当做另一个类型的类型实参:

逆变:反转子类型化关系

逆变的概念可以看成是协变的镜像:第一个逆变来说,它的子类型化关系与作用理性实参的类的子类型化关系是相反的。

interface Comparator<in T>{
    fun compare(e1: T, e2: T): Int
}

这个接口只是消费了类型为 T 的值。
一个为特定类型的值定义的比较器显然可以比较该类型任意子类型的值。

val anyComparator = Comparator<Any>{
    e1, e2 -> e1.hashCode() - e2.hashCode()
}
val strings = listOf("a", "b", "c")
strings.sortedWith(anyComparator)

sortedWith 函数期望一个 Comparator<String> ,传递给它一个能比较更一般的类型的比较器是安全的。如果你要在特定类型的对象上执行比较,可以使用能处理该类型或者它的超类型的比较器。这说明 Comparator<Any> 是 Comparator<String> 的子类型,其中 Any 是 String 的超类型。不同类型之间的子类型关系和这些类型的比较器之间的子类型化关系截然相反。
一个在类型参数上逆变的类是这样的一个泛型类(我们以 Consumer<T> 为例),对这种类来说,下面的描述是成立的:如果 B 是 A 的子类型,那么 Consumer<A> 就是 Comsumer<B> 的子类型,类型参数 A 和 B 交换了位置,所以我们说子类型化被反转了。
in 关键字的意思是,对应类型的值是传递进来给这个类的方法的,并且被这些方法消费。和协变的情况类似,约束类型参数的使用将导致特定的子类型化关系。在类型参数 T 上的 in 关键字意味着子类型化被反转了。
一个类可以在一个类型参数上协变,同时在另一个类型参数上逆变:

interface Function1<in P, out R>{
    operator fun invoke(p: P): R
}

使用点变形:在类型出现的地方制定变型

在 Java 中每一次使用带类型参数的类型的时候,还可以指定这个类型参数是否可以用它的子类型或者超类型替换。这叫做使用点变形。
Kotlin 也支持使用点变型,允许在类型参数出现的具体位置指定变型,即使在类型声明时它不能被声明称协变的或者逆变的。

fun <T:R, R> copyData(source: MutableList<T>,
                      destination: MutableList<R>){
    for(item in source){
        destination.add(item)
    }
}
>>val ints = mutableListOf(1,2, 3)
>>val anyItems = mutableListOf<Any>()
>>copyData(ints, anyItems)

但是 Kotlin 提供了一种更优雅的表达方式。当函数的实现调用了那些类型参数只出现在 out 位置(或只出现在 in 位置)的方法时,可以充分利用这一点,在函数定义中给特定用途的类型参数加上变型修饰符。

fun <T> copyData(source: MutableList<out T>,
                      destination: MutableList<T>){
    for(item in source){
        destination.add(item)
    }
}

可以为类型声明中类型参数任意的用法指定变型修饰符,这些用法包括:形参类型、局部变量类型、函数返回类型,等等。这里发生的一切被称作为类型投影:我们说 source 不是一个常规的 MutableList,而是一个投影(受限)的 MutableList。

星号投影:使用 * 代替类型参数

星号投影语法可以用来表名你不知道关于泛型实参的任何信息。例如,一个包含未知类型的元素的列表用这种语法表示为 List<*>。
MutableList<*> 和 MutableList<Any?> 不一样,MutableList<Any?> 这种列表包含的是任意类型的元素,而 MutableList<*> 是包含某种特定类型元素的列表。
MutableList<*> 投影成了 MutableList<out Any?> :当你没有任何元素类型信息的时候,读取 Any? 类型的元素仍然是安全的,但是向列表中写入元素是不安全的。谈到 Java 通配符,Kotlin 的 AnyType<*> 对应于 Java 的 MyType<?>.

参考文献

[1]Kotlin 实战(Kotlin in Action).北京:电子工业出版社,2017.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259