Kotlin 委托与泛型核心技巧

泛型的基本使用

我们对泛型并不陌生,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
    }
}

那么我们只能将方法的泛型指定为数字类型,如 IntFloatBigDecimal 等。当你指定为其他类型,如 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 块中的代码。

相关推荐
移动开发者1号4 小时前
Kotlin协程超时控制:深入理解withTimeout与withTimeoutOrNull
android·kotlin
移动开发者1号5 小时前
Java Phaser:分阶段任务控制的终极武器
android·kotlin
哲科软件13 小时前
跨平台开发的抉择:Flutter vs 原生安卓(Kotlin)的优劣对比与选型建议
android·flutter·kotlin
移动开发者1号1 天前
Android 同步屏障(SyncBarrier)深度解析与应用实战
android·kotlin
移动开发者1号1 天前
深入协程调试:协程调试工具与实战
android·kotlin
fundroid2 天前
Kotlin 协程:Channel 与 Flow 深度对比及 Channel 使用指南
android·kotlin·协程
移动开发者1号2 天前
深入理解原子类与CAS无锁编程:原理、实战与优化
android·kotlin
移动开发者1号2 天前
深入理解 ThreadLocal:原理、实战与优化指南
android·kotlin
Devil枫2 天前
Kotlin高级特性深度解析
android·开发语言·kotlin
ChinaDragonDreamer2 天前
Kotlin:2.1.20 的新特性
android·开发语言·kotlin