Kotlin-扩展

本文深入探讨Kotlin中的扩展功能,包括扩展函数的定义、静态解析特性、成员函数与扩展函数的关系、如何处理NULL接收者,以及扩展属性的声明和使用。示例展示了扩展如何增加类的功能,而无需继承或使用Decorator模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

在Kotlin中,允许对类进行扩展,不需要继承或使用 Decorator 模式,通过一种特殊形式的声明,来实现具体实现某一具体功能。扩展函数是静态解析的,并未对原类增添函数或者属性,也就是说对其本身没有丝毫影响。

扩展函数

扩展类的函数, 即 Extension Function , 可以在已有类中添加新的方法, 比继承更加简洁和优雅.

定义形式

扩展函数定义形式:

fun receiverType.functionName(params){
    body
}
  • receiverType:表示函数的接收者,也就是函数扩展的对象
  • functionName:扩展函数的名称
  • params:扩展函数的参数,可以为NULL

扩展函数并没有对原类做修改,而是为被扩展类的对象添加新的函数。

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' 指代 list 实例
    this[index1] = this[index2]
    this[index2] = tmp
}

fun main(args: Array<String>) {

    val l = mutableListOf(1, 2, 3)
    l.swap(0, 2) // 'swap()' 函数内的 'this' 将指向 'l' 的值

    println(l.toString())
}

// Log
[3, 2, 1]

先对MutableList声明一个扩展函数swap,用来调换不同位置的值.在测试实例中,声明了一个MutableList实例,其值为1,2,3.然后调用该扩展函数swap(0, 2),在Log中,可以清晰的看出,位置0和2的值做了互换。在实际开发时,如果对一个类某一功能并不通用,只有某些特殊场景使用时,可以使用扩展函数,从而显得灵活多变。

注:

this关键字指代接收者对象(receiver object)(也就是调用扩展函数时, 在点号之前指定的对象实例).

扩展函数是静态解析的

之前说过,扩展函数是静态解析的,也就是,它并不是接收者类型的虚拟成员,意味着调用扩展函数时,调用扩展函数时,具体被调用的的是哪一个函数,由调用函数的的对象表达式来决定的,而不是动态的类型决定的。比如先声明了两个类Person和Teacher,其中teacher继承于Person,
在测试实例中,分别对Person和Teacher声明了扩展函数doFly(),又声明了fly()方法,其参数是一个Person对象,在其方法体内,调用了Person的扩展函数。现在测试实例中,创建了一个Teacher对象teacher,调用fly()方法并将teacher过去,实际上会调用哪个扩展函数呢?

open class Person {
}

class Teacher: Person() {
}

// 测试实例
fun Person.doFly() {
    println("Person do fly")
}

fun Teacher.doFly() {
    println("Teacher do fly")
}

fun fly(person: Person) {
    person.doFly()
}

fun main(args: Array<String>) {

    var teacher: Teacher = Teacher()

    fly(teacher)
}

// Log
Person do fly

从Log打印中,可以清晰的看到的是,fly(teacher)调用的是Person的扩展函数。因为调用哪个函数, 仅仅是由参数person声明的类型决定, 这里参数person的类型为Person类.

成员函数和扩展函数

当成员函数和扩展函数想碰时,就像一个亲生的和私生的,当私生的碰到亲生的会怎么样呢?在一个类中,成员函数和扩展函数的函数名一致时,如果调用此函数,会调用成员函数还是扩展函数呢?不妨我们试一下

open class Person {
    fun doFly() {
        println("亲生的")
    }
}

fun Person.doFly() {
    println("后生的")
}

fun main(args: Array<String>) {

    val person: Person = Person()
    person.doFly()
}

// Log打印
亲生的

在Person中声明了函数doFly(),并对其声明了扩展函数doFly()。在Log打印中,可以清晰的看到,当person调用doFly()函数时,调用的是成员函数,而不是扩展函数,意味着,如果类中存在成员函数, 同时又在同一个类上定义了同名的扩展函数, 并且与调用时指定的参数匹配, 这种情况下,总是会优先使用成员函数。

接收者可为NULL

在扩展函数内, 可以通过this来接收者是否为NULL,这样,即使接收者为NULL,也可以调用扩展函数。例如

fun Person.doFly() {

    if (null == this) {
        println("null")
    }
    println("doFly")
}

扩展属性

扩展属性, 即 Extension Property , 即把某些函数添加为数据, 使用”=”, 直接设置或使用。

val List.lastIndex: Int
get() = size - 1


注:
由于扩展属性实际上不会向类添加新的成员, 因此无法让一个扩展属性拥有一个后端域变量. 所以,对于扩展属性不允许存在初始化器. 扩展属性的行为只能通过明确给定的取值方法与设值方法来定义,也就意味着扩展属性只能被声明为val而不能被声明为var.如果强制声明为var,即使进行了初始化,在运行也会报异常错误,提示该属性没有后端域变量。

伴随对象的扩展

Kotlin-伴随对象中了解到,伴随对象通过“类名.”形式调用伴随对象。假如对伴随对象声明了扩展函数该怎么调用呢?其调用方式与伴随对象一样,都是通过用类名限定符来调用。当然,扩展属性也是这样的。

fun Person.Companion.doSwim() {
    println("伴随对象的扩展函数")
}

val Person.Companion.no: Int
get() = 10


fun main(args: Array<String>) {

    println("age:${Person.age}")
    Person.doSwim()
}

将扩展定义为成员

在类的内部,可以为另外一个类定义扩展,在这个扩展中,有个多个隐含的接受者,其中扩展方法定义所在类的实例称为派发接受者,而扩展方法的目标类型的实例称为扩展接受者。

open class Person {

    fun doFly() {
        println("Person do fly")
    }
}

class MyInfo {

    fun doRun() {
        println("MyInfo do run")
    }

    fun Person.doSwim() {
        doFly()
        doRun()
    }

    fun doSomething(person: Person) {
        person.doSwim()
    }
}

fun main(args: Array<String>) {

    val myInfo: MyInfo = MyInfo()
    val person: Person = Person()
    myInfo.doSomething(person)
}
// Log  
Person do fly
MyInfo do run

在MyInfo类内,创建了Person类的扩展。此时,MyInfo被成为派发接受者,而Person为扩展接受者。从上例中,可以清楚的看到,在扩展函数中,可以调用派发接收者的成员函数。假如在调用某一个函数,而该函数在派发接受者和扩展接受者均存在,此时扩展函数将会调用哪一个呢?写段代码测试下:

class MyInfo {

    fun doRun() {
        println("MyInfo do run")
    }

    fun doFly() {
        println("MyInfo do fly")
    }

    fun Person.doSwim() {
        doFly()
        doRun()
    }

    fun doSomething(person: Person) {
        person.doSwim()
    }
}

// 测试实例
fun main(args: Array<String>) {

    val myInfo: MyInfo = MyInfo()
    val person: Person = Person()
    myInfo.doSomething(person)
}
// Log  
Person do fly
MyInfo do run

现在MyInfo类中,声明了一个函数doFly(),其函数名与Person中的doFly()一致。从Log打印中,我们可以清晰的看到,扩展函数实际上调用的是Person类中的doFly()函数,也就是,意味着,当派发接受者与扩展接受者的成员名称发生冲突时, 扩展接受者的成员将会被优先使用.如想同时调用Person和MyInfo中的doFly()函数或者是优先调用MyInfo中的doFly()函数,我们该怎么处理呢?在这里,我们不由想到了this@label,来指定this所属的范围,也就说我们可以这么做,this@MyInfo.doFly(),此时我们指定了this属于MyInfo类,从而达到了我们所想效果。

class MyInfo {

    fun doRun() {
        println("MyInfo do run")
    }

    fun doFly() {
        println("MyInfo do fly")
    }

    fun Person.doSwim() {
        doFly()
        this@MyInfo.doFly()
        doRun()
    }

    fun doSomething(person: Person) {
        person.doSwim()
    }
}

// 测试实例
fun main(args: Array<String>) {

    val myInfo: MyInfo = MyInfo()
    val person: Person = Person()
    myInfo.doSomething(person)
}

// Log
Person do fly
MyInfo do fly
MyInfo do run

以成员的形式定义的扩展函数, 可以声明为 open , 而且可以在子类中覆盖. 也就是说, 在这类扩展函数的派
发过程中, 针对派发接受者是虚拟的(virtual), 但针对扩展接受者仍然是静态的。

open class D {
} 

class D1 : D() {
} 

open class C {
    open fun D.foo() {
        println("D.foo in C")
    } 

    open fun D1.foo() {
        println("D1.foo in C")
    } 

    fun caller(d: D) {
        d.foo() // 调用扩展函数
    }
} 

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    } 

    override fun D1.foo() {
        println("D1.foo in C1")
    }
} 

C().caller(D()) // 打印结果为 "D.foo in C"
C1().caller(D()) // 打印结果为 "D.foo in C1" - 派发接受者的解析过程是虚拟的
C().caller(D1()) // 打印结果为 "D.foo in C" - 扩展接受者的解析过程是静态的
### 如何在 Android 项目中应用 `kotlin-android`、`kotlin-kapt` 和 `kotlin-parcelize` 插件 #### 使用 `plugins {}` 块的方式 现代 Gradle 构建脚本推荐使用 `plugins {}` 块来声明插件。这种方式更加简洁明了,能够减少配置冲突的可能性。 以下是完整的 `build.gradle` 文件中的插件部分: ```gradle plugins { id 'com.android.application' // 或者 com.android.library 如果是库模块 id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'kotlin-parcelize' } ``` 上述方法适用于大多数场景,并且可以有效避免传统 `apply plugin:` 方式的潜在问题[^1]。 --- #### 调整插件顺序的重要性 如果仍然选择使用传统的 `apply plugin:` 方法,则需要注意插件的应用顺序。某些情况下,特定插件的加载依赖于其他插件已经完成初始化的过程。例如,在引入 `kotlin-android-extensions` 的时候,通常需要先加载 `kotlin-android` 插件[^2]。 以下是一个典型的例子: ```gradle apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-parcelize' // apply plugin: 'kotlin-android-extensions' (已废弃) ``` 注意:`kotlin-android-extensions` 已被官方标记为弃用,建议改用 Jetpack Compose 或 View Binding 替代其功能[^3]。 --- #### 数据绑定支持 (`kotlin-kapt`) 当涉及到数据绑定(Data Binding)或者 Room 数据库等注解处理器时,必须显式地添加 `kotlin-kapt` 插件并正确配置相关依赖项。这是因为这些工具通过编译期生成代码实现运行时的功能扩展[^5]。 示例配置如下所示: ```gradle dependencies { kapt "androidx.databinding:databinding-compiler:<version>" } plugins { id 'kotlin-kapt' } ``` 其中 `<version>` 应替换为当前项目的 Data Binding 版本号。 --- #### 组件化开发下的特殊需求 对于采用组件化架构的大型项目来说,可能需要动态判断当前模块是否属于应用程序入口点还是普通的库模块。此时可以通过根项目的全局变量控制插件的选择逻辑。 具体实现可参考以下片段: ```groovy if (rootProject.ext.android.isApplication) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } plugins { id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'kotlin-parcelize' } ``` 此方案允许开发者灵活管理不同类型的子模块,而无需重复定义相同的构建规则。 --- #### 总结 为了确保最佳实践,请优先考虑利用 `plugins {}` 声明所需插件;同时留意各插件之间的兼容性和加载次序关系。针对高级特性如数据绑定或 Parcelize 功能,则务必集成对应的注解处理器支持。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值