泛型的基本使用
我们对泛型并不陌生,Java 在其 1.5 版本中就引入了泛型,Kotlin 自然也支持。但 Kotlin 中的泛型有部分和 Java 中的泛型不同,现在就只讲泛型的基本用法,也就是和 Java 中相同的部分。
什么是泛型呢?泛型允许我们在定义类、接口和方法时,将类型参数化。这个类型参数只有在创建类对象或者调用方法时才会被指定为具体的类型。
例如,List
是一个可以存放数据的列表,但它可存放任意类型,因为它使用了泛型,并没有在定义时就指定一个具体的类型。
那泛型该如何使用呢?
泛型有两种定义方式:一种是定义泛型类,另一种是定义泛型方法。使用的语法结构都是 <T>
,尖括号中的 T 是大家约定俗成的泛型写法。
例如,我们要定义一个泛型类,我们可以这样写:
kotlin
class MyClass<T> {
fun method(param: T): T {
return param
}
}
此时,MyClass
就是一个泛型类,在其内部,可以使用这个泛型。method
方法就使用了泛型参数和泛型返回值。
在具体实例化 MyClass
类和调用 method
方法时,就可以将泛型指定为具体类型,如下所示:
kotlin
val myClass = MyClass<Int>()
myClass.method(369)
由于 myClass
的泛型类型为 Int
,那么它的 method
方法参数类型就自动变为了 Int
,并且返回值类型也变为了 Int
。
如果你不想定义一个泛型类,只想定义泛型方法,只需要将定义泛型的语法结构写在方法上就行了,像这样:
kotlin
class MyClass {
fun <T> method(param: T): T {
return param
}
}
现在的调用就变为了:
kotlin
val myClass = MyClass()
myClass.method<Int>(369)
我们只是在调用 method
方法时,指定了泛型的具体类型。并且由于 Kotlin 的类型推导机制,该方法的泛型类型可以根据传入的参数推导出,所以这里我们可以省略泛型的指定:
kotlin
val myClass = MyClass()
myClass.method(369) // 编译器会自动推断出 T 是 Int 类型
Kotlin 还允许我们通过指定上界 的方式来对泛型的具体类型进行限制,比如我们将 method
方法的泛型上界设为 Number
类型:
kotlin
class MyClass {
fun <T : Number> method(param: T): T {
return param
}
}
那么我们只能将方法的泛型指定为数字类型,如 Int
、Float
、BigDecimal
等。当你指定为其他类型,如 String
,就会报错:Type argument is not within its bounds.
另外,默认情况下,所有泛型都可以指定为可空类型,因为在不指定泛型的上界时,默认上界就是 Any?
。如果不想要泛型类型可为空,需要手动指定泛型上界为 Any
。
kotlin
// 泛型 T 默认可空,相当于 <T : Any?>
fun <T> nullableMethod(param: T) {
// ... 具体实现 ...
}
// 指定上界为 Any,泛型 T 不可空
fun <T : Any> nonNullMethod(param: T) {
// ... 具体实现 ...
}
另外,我们之前有提到过官方的 apply
函数能被任何类型的对象调用,其中就使用到了 Kotlin 泛型。
kotlin
public inline fun <T> T.apply(block: T.() -> Unit): T {
// ...
block()
return this
}
现在你回头看看,应该觉得很容易理解吧:它将泛型定义在了 apply()
方法上,并且方法调用者(T
)、函数类型参数的接收者(T.()
)以及返回值(T
)的类型也都是这个泛型。
类委托和委托属性
委托是一种设计模式,其基本理念是:一个对象不自己去处理某个请求,而是将请求委托给另一个辅助对象去处理。
Kotlin 支持委托,并将它分为了两种:类委托和委托属性。
类委托
首先来看类委托,其核心思想是:将一个类的具体实现委托给另一个对象来完成。
例如,我们借助委托模式,来实现一个 Set
接口,代码如下:
kotlin
class MySetManual<T>(private 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
辅助对象。我们在所有方法实现中,都只是简单调用了该对象的同名方法,这就是一种委托模式。
那这有什么用?
既然都是调用辅助对象的方法来实现,我还不如直接使用辅助对象。这种模式的意义在于,我们选择性地重写一部分方法的实现,或是在委托的基础上加入新的方法,从而创建功能更加、丰富的"新"数据结构了。
但如果接口中待实现的方法太多的话,难道所有方法都需要像这样一个个重写吗?
这就要使用到 Kotlin 中委托使用的 by
关键字了。我们只需要在接口声明后使用 by
关键字,再加上被委托的辅助对象即可。
代码如下所示:
kotlin
class MyMutableSet<T>(
private val helperSet: MutableSet<T> = HashSet(),
) : MutableSet<T> by helperSet {
// 可以重写某个方法
override fun isEmpty(): Boolean {
println("Performing custom isEmpty check!")
return size == 0
}
// 也可以新增方法
// 从集合中随机抽取一个元素
fun getRandomOrNull(): T? {
return helperSet.randomOrNull()
}
}
使用 by
关键字,所有委托的样板代码都会由编译器自动生成,使代码量大大减少。
委托属性
委托属性的核心思想是:将一个属性(field)的读写操作委托给另一个类去完成。
其语法结构如下所示:
kotlin
class MyClass {
var p: String by Delegate()
}
它代表将 p
属性的 get()
和 set()
方法实现委托给了 Delegate
类。当访问 p
属性时,会自动调用 Delegate
中的 getValue()
方法;当给 p
属性赋值时,会自动调用 Delegate
中的 setValue()
方法。
我们来实现 Delegate
类,代码如下:
kotlin
class Delegate {
private var propValue: Any? = null
operator fun getValue(thisRef: Any?, prop: KProperty<*>): Any? {
println("getValue from $thisRef, property name is '${prop.name}'")
return propValue
}
operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: Any?) {
println("setValue to $thisRef, new value is $value")
propValue = value
}
}
这是标准的代码实现模板:
-
getValue()
方法- 第一个参数代表持有该委托属性的对象实例 。例如,在
MyClass
的实例中访问p
属性时,这个参数就是MyClass
实例。 - 第二个参数用于获取属性的元信息,比如属性名(
prop.name
)。 - 方法的返回值类型需要和委托属性的类型兼容。不是相同,因为返回值类型可以转为委托属性的类型即可。
- 第一个参数代表持有该委托属性的对象实例 。例如,在
-
setValue()
方法- 前两个参数和
getValue
相同 - 第三个参数代表要赋给委托属性的新值。注意:这个参数的类型要和 getValue 方法返回值的类型兼容。
- 前两个参数和
不过有时,可以不用实现 setValue
方法,那就是当委托的属性使用 val 声明时。
实现简化的 lazy 函数
了解了委托属性后,我们来实现一个自己的 lazy
(懒加载)函数。它会将属性的初始化代码延迟到它第一次被访问时,才被执行。
其语法结构如下:
kotlin
val p: String by lazy {
println("Initializing...")
"this is a lazy string"
}
其实看到这,你就应该能想到:
其中的关键在于 by
关键字,lazy
函数会返回一个 Delegate
对象。当我们第一次访问 p
属性时,就会调用这个 Delegate
对象中的 getValue()
方法,而在 getValue()
方法内部,又会执行我们传入的 Lambda 表达式来完成初始化操作。
下面我们来实现一个自己的懒加载函数 later
。
首先,实现符合委托规范的 Later
类。代码如下:
kotlin
class Later<T>(private val block: () -> T) {
private var value: Any? = null
operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
if (value == null) {
println("inside later block (initializing...)")
value = block()
}
@Suppress("UNCHECKED_CAST")
return value as T
}
}
然后,创建一个顶层 later
函数,用于返回 Later
类的实例,代码如下:
kotlin
fun <T> later(block: () -> T) = Later(block)
现在我们来测试一下:
kotlin
fun main() {
val p by later {
println("inside later block")
"this a later string"
}
println("before access p")
println("p is $p")
println("after access p")
}
运行结果:
scss
before access p
inside later block (initializing...)
inside later block
p is this a later string
after access p
可以看到 Lambda 表达式中的初始化代码,只有在第一次访问 p
属性时才会执行。这就是实现了懒加载的效果。
最后注意:这只是大致实现了 lazy
函数的功能。在真实项目中,还是要使用标准库中的 lazy
函数。因为它解决了线程安全的问题,防止在多线程的环境下,导致初始化操作被重复执行;解决了空值处理的问题,如果 block
块执行的结果是空(value = null
),那么下次访问委托的属性而调用 getValue
方法时,还是会进入 value == null
的判断,导致重复执行 block
块中的代码。