Kotlin 基础09 - 委托机制、泛型

委托

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。

Kotlin 中将委托功能分为了两种:类委托和委托属性。

类委托

类委托的核心思想在于将一个类的具体实现委托给另一个类去完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MySet<T>(val helperSet: HashSet<T>) : Set<T>  {

override val size: Int
get() = helperSet.size

override fun contains(element: T) = helperSet.contains(element)

override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)

override fun isEmpty() = helperSet.isEmpty()

override fun iterator() = helperSet.iterator()
}

接收的 HashSet 参数相当于一个辅助对象,Set 接口中的所有的方法实现中,都是调用了辅助对象中相应的方法。

委托模式的意义在于让大部分方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么 MySet 就会成为一个全新的数据结构类。

Kotlin 中委托使用的关键字是 by ,在接口声明的后面使用 by 关键字,再接上受委托的辅助对象,就可以免去一大堆模板式的代码了。

1
2
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
}

新增方法,单独重写想要重写的方法即可。

1
2
3
4
5
6
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {

fun helloWorld() = println("Hello World")

override fun isEmpty() = false
}

委托属性

委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。

委托属性的语法结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass {
var p by Delegate()
}

class Delegate {
var propValue: Any? = null

operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
return propValue
}

operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
propValue = value
}
}

by 关键字连接了左边的 p 属性和右边的的 Delegate 实例。

这种写法就代表着将 p 属性的具体实现委托给了 Delegate 类去完成。当调用 p 属性的时候会自动调用 Delegate 类的 getValue() 方法,当给 p 属性赋值的时候会自动调用 Delegate 类的 setValue() 方法。

这是一种标准的代码实现模板,在 Delegate 类中必须实现 getValue()setValue() 方法,并且使用 operator 关键字进行声明。

getValue() 方法接收参数:第一个参数用于声明该 Delegate 类的委托功能可以在什么类中使用;第二个参数 KProperty<*> 是 Kotlin 中的一个属性操作类,可以用于获取各种属性相关的值。<*> 这种泛型的写法表示你不知道或者不关心泛型的具体类型。返回值可以声明成任何类型,根据具体的实现逻辑去写就行了。

setValue() 方法类似。前两个参数与 getValue() 方法相同,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和 getValue() 方法返回值的类型保持一致。

现在当给 MyClass 的 p 属性赋值时,就会调用 Delegate 类的 setValue() 方法,当获取 MyClass 的 p 属性的值时,就会调用 Delegate 类的 getValue() 方法。

如果 MyClass 中的 p 属性使用 val 关键字声明,可以不用在 Delegate 类中实现 setValue() 方法。

泛型

基础

Kotlin 中的泛型和 Java 中的泛型有同有异。

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是 <T>

1
2
3
4
5
6
7
8
class MyClass<T> {
fun method(param: T): T {
return param
}
}

val myClass = MyClass<Int>()
val result = myClass.method(123)
1
2
3
4
5
6
7
8
class MyClass {
fun <T> method(param: T): T {
return param
}
}

val myClass = MyClass()
val result = myClass.method(123)

Kotlin 允许我们对泛型的类型进行限制。可以通过指定上界的方式来对泛型的类型进行约束。

1
2
3
4
5
6
7
8
class MyClass {
fun <T : Number> method(param: T): T {
return param
}
}

val myClass = MyClass()
val result = myClass.method(123)

默认情况下,所有的泛型都是可以指定成可空类型的,因为泛型默认是 Any? ,如果要让泛型的类型不可空,指定成 Any 即可。

1
2
3
4
5
6
7
8
9
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
// 使用泛型
fun <T> T.build(block: T.() -> Unit): T {
block()
return this
}

对泛型进行实化

基于 JVM 的语言,泛型功能都是通过类型擦除机制来实现的。

不同的是,Kotlin 提供了内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题了,因为代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明。

Kotlin 中是可以将内联函数中的泛型进行实化的。

首先函数必须是内联函数才行,也就是需要用 inline 关键字来修饰函数。

其次,在声明泛型的地方必须加上 reified 关键字来表示该泛型要进行实化。

1
2
3
4
5
6
7
8
inline fun <reified T> getGenericType() = T::class.java

fun main() {
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
println("result1 is $result1")
println("result2 is $result2")
}

泛型实化的应用

泛型实化功能允许我们在泛型函数当中获得泛型的实际类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline fun <reified T> startActivity(context: Context) {
val intent = Intent(context, T::class.java)
context.startActivity(intent)
}
// 启动 TestActivity
startActivity<TestActivity>(contex)


// 通过函数类型参数传递启动服务时可能需要的参数
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
block(intent)
context.startActivity(intent)
}
// 启动 TestActivity
startActivity<TestActivity>(contex) {
putExtra("param1", "data")
putExtra("param2", 12345)
}

协变与逆变

Java 中的协变与逆变

泛型的协变

一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为 in 位置,而它的返回值是输出数据的地方,因此可以称它为 out 位置。

1
2
3
4
5
open class Person(val name: String, val age: Int)

class Student(name: String, age: Int) : Person(name, age)

class Teacher(name: String, age: Int) : Person(name, age)

假如定义了一个 MyClass<T> 的泛型类,其中 A 是 B 的子类型,同时 MyClass<A> 又是 MyClass<B> 的子类型,就可以称 MyClass 在 T 这个泛型上是协变的

如果一个泛型类在其泛型类型的数据上是只读的话,那么它是没有类型转换安全隐患的。也就是说 T 只能出现在 out 位置上,而不能出现在 in 位置上。

1
2
3
4
5
class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}

在泛型 T 的声明前面加上了 out 关键字,这就意味着 T 只能出现在 out 位置上,而不能出现在 in 位置上。SimpleData 在泛型 T 上是协变的。

构造函数中的泛型 T 尽管也在 in 位置上,但是使用了 val 关键字,所以构造函数中的泛型 T 任然是只读的,因此这样写是合法且安全的。即使使用了 var 关键字,只要给它加上 private 修饰符,保证这个泛型 T 对外部而言是不可修改的,那么都是合法的写法。

在 Java 中接收一个 List<Person> 类型的参数,是不允许传入 List<Student> 实例的。但是在 Kotlin 中这么做是合法的,因为 Kotlin 已经默认给许多内置的 API 加上了协变声明,其中就包括了各种集合的类与接口。

Kotlin 中的 List 本身就是只读的,如果想要给 List 添加数据,需要使用 MutableList 。

List 简化版的源码:

1
2
3
4
5
6
7
8
9
public interface List<out E> : Collection<E> {

override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
public operator fun get(index: Int): E
}

原则上在声明了协变后,泛型 E 就只能出现在 out 位置上,但是 contains() 方法中泛型 E 仍然出现在了 in 位置上。

泛型 E 出现在 in 位置上就意味着会有类型转换的安全隐患。但是 contains() 方法目的非常明确,它只判断当前集合中是否包含参数中传入的这个元素,而不会修改当前集合中的内容,因此这种操作实质上又是安全的。

为了能让编译器能够理解这里操作是安全的,所以使用了 @UnsafeVariance 注解。这样编译器就会允许泛型 E 出现在 in 位置上了。注意不要滥用。

泛型的逆变

假如定义了一个 MyClass<T> 的泛型类,其中 A 是 B 的子类型,同时 MyClass<B> 又是 MyClass<A> 的子类型,就可以称 MyClass 在 T 这个泛型上是逆变的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Transformer<in T> {
fun transform(t: T): String
}

fun main() {
val trans = object : Transformer<Person> {
override fun transform(t: Person): String {
return "${t.name} ${t.age}"
}
}
handleTransformer(trans)
}

fun handleTransformer(trans: Transformer<Student>) {
val student = Student("Tom", 19)
val result = trans.transform(student)
}

此时 Transformer<Person> 已经成为了 Transformer<Student> 的子类型。

在泛型 T 的声明前面加上了 in 关键字,这就意味着 T 只能出现在 in 位置上,Transformer 在泛型 T 上是逆变的。

逆变功能在 Kotlin 内置 API 中的应用,比较典型的例子就是 Comparable 的使用。

1
2
3
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}

如果使用 Comparable<Person> 实现了两个 Person 对象比较大小的逻辑,那么用这段逻辑去比较两个 Student 对象的大小显然也是成立的。因此让 Comparable<Person> 成为 Comparable<Student > 的子类合情合理,这也是逆变非常典型的应用。