Kotlin 泛型和委托

泛型的基本用法

在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是 <T>,使用任何英文字母或单词都可以,通常情况下 T 是一种约定俗成的泛型写法。

定义一个泛型类:

class MyClass<T> {
fun method(param: T): T {
return param
}
}

val myClass = MyClass<Int>()
val result = myClass.method(123)

定义一个泛型方法:

class MyClass{
fun <T> method(param: T): T {
return param
}
}

val myClass = MyClass()
val result = myClass.method<Int>(123)

由于 Kotlin 拥有出色的类型推导机制,这里可以直接省略泛型的指定:

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

Kotlin 还允许对泛型的类型进行限制,可以以通过指定上界的方式来对泛型的类型进行约束,如指定成数字类型:

class MyClass {
fun <T : Number> method(param: T): T {
return param
}
}

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

类委托和委托属性

委托是一种设计模式:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。Kotlin 中将委托功能分为了两种:类委托和委托属性。

类委托

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

Set 是一个接口,使用它需要用它具体的实现类比如 HashSet。借助委托模式可以轻松实现一个自己的实现类,比如定义一个 MySet 并让它实现 Set 接口:

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(): Iterator<T> = helperSet.iterator()

}

MySet 的构造函数中接收了一个 HashSet 参数,这就相当于一个辅助对象。Set 接口所有的实现方法中都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这其实就是一种委托模式。

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

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

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

上面两段代码效果一样,借助了类委托的功能后代码大大简化,如果要对某个方法重新实现只需要单独重写哪一个方法就可以了。

委托属性

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

class MyClass {

var p by Delegate()
}

这里使用 by 关键字连接左边的 p 属性和右边的 Delegate 实例,代表着将 p 属性的具体实现委托给 Delegate 类去完成。当调用 p 属性的时候会自动调用 Delegate 类的 getValue () 方法,当给 p 属性赋值的时候会自动调用 Delegate 类的 setValue () 方法。

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
}
}

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

getValue () 方法要接收两个参数:第一个参数用于声明该 Delegate 类的委托功能可以在什么类中使用;第二个参数KProperty<*> 是 Kotlin 中的一个属性操作类,可用于获取各种属性相关的值,必须在方法参数上进行声明。<*> 这种泛型的写法表示不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,有点类似 Java 中 <?> 的写法。返回值可以声明成各种类型。

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

MyClass 中的 p 属性使用 val 关键字声明时,意味着 p 属性是无法在初始化之后杯重新赋值,因此没有必要实现 setValue () 方法,只需要实现 getValue () 方法就可以了。

实现一个自己的 lazy 函数

by lazy 代码块是 Kotlin 提供的一种懒加载技术,它的基本语法结构如下:

val p by lazy { ... }

代码块中的代码一开始并不会执行,只有当 p 变量首次被调用的时候才执行,并且会将代码块中最后一行代码的返回值赋值给 p 变量。by 是 Kotlin 中的关键字,lazy 是一个高阶函数。在 lazy 函数中会创建并返回已给 Delegate 对象,调用 p 属性时其实调用的是 Delegate 对象的 getValue () 方法,然后 getValue () 方法又会调用 lazy 函数传入的 Lambda 表达式,这样表达式中的代码就可以得到执行了,并且调用 p 属性后得到的值就是 Lambda 表达式中最后一行代码的返回值。

class Later<T>(val block: () -> T) {

var value: Any? = null

operator fun getValue(any: Any?, prop: KProperty<*>): T {
if (value == null) {
value = block()
}
return value as T
}

}

这里定义了一个 Later 类并指定为泛型类,Later 的构造函数中接收一个函数类型参数,这个函数类型参数不接收任何参数,并且返回值类型就是 Later 类指定的泛型。getValue () 方法的第一个参数指定成 Any? 类型,表示我们希望 Later 的委托功能在所有类中都可以使用。然后使用了一个 value 变量对值进行缓存,如果 value 值为空就调用构造函数中传入的函数类型参数去获取值,否则就直接返回。懒加载技术不会对属性机型赋值,因此这里不用实现 setValue () 方法。

为了让用法更类似 lazy 函数,可以再定义一个顶层函数:

fun <T> later(block: () -> T) = Later(block)

这里将顶层函数也定义成了泛型函数,并接收一个函数类型参数,这个顶层函数的作用很简单:创建 Later 类的实例,并将接收的函数类型参数传给 Later 类的构造函数。现在可以用 later 简单替代 lazy 函数了:

val p by later { ... }

参考