位置:首页 > Kotlin > Kotlin 扩展怎么用:从扩展函数到 let、run、apply 一次讲清

Kotlin 扩展怎么用:从扩展函数到 let、run、apply 一次讲清

时间:2026-08-23  |  作者:云端旅人  |  阅读:0

目录

  1. 扩展函数是什么,适合解决什么问题
  2. 泛型扩展:把能力做成可复用工具
  3. 如何给伴生对象添加扩展
  4. 常见扩展函数:let、run、apply 怎么区分
  5. 写扩展时,重点看这三个判断标准

前言

Kotlin 的扩展机制,解决的是“类不能改,但我还想把调用写得像原生成员一样自然”这个很实际的问题。本文从扩展函数的基础语法讲起,接着说明泛型扩展、伴生对象扩展的写法,再对 let、run、apply 这组最常用也最容易混淆的扩展函数做对比,帮助你判断它们各自适合放在什么场景里。

Kotlin 的扩展是这门语言里非常实用的一组能力:遇到第三方类不能改、现有类型又缺少顺手方法时,不必继承,也不必额外套一层装饰器,就能直接补上需要的函数或属性。对日常业务开发来说,它既能减少样板代码,也会直接影响 API 的可读性。

这篇文章按“怎么写、适合什么时候用、常见扩展之间有什么区别”的顺序梳理 Kotlin 扩展。看完后,你可以判断一个场景更适合写普通工具函数,还是改成扩展函数;同时也能分清 let、run、apply 这些常见扩展在返回值和使用方式上的差别。

扩展函数是什么,适合解决什么问题

扩展函数的核心价值,是在不修改原类定义的前提下,为一个已有类型增加新的调用方式。最常见的场景是:类来自第三方库、系统 SDK,或者已经在项目中稳定使用,不方便直接改源码,但你又希望它像“原生成员函数”一样被调用。

Kotlin 扩展函数的定义与调用关系图,包含 Kotlin 调用、Java 调用和伴生对象扩展的区别
Kotlin 扩展的调用关系用一张关系图看清扩展函数在 Kotlin 与 Java 中的调用形态。

在 Kotlin 中,扩展函数的定义方式就是在函数名前加上接收者类型前缀。调用时看起来和普通成员函数一致,这也是它可读性高的原因之一。

基本写法与调用方式

下面这段代码演示了如何为 Jump 添加一个 doubleJump 扩展函数:

class Jump{
    fun test(){
        println("jump test")
        // 在被扩展的类中使用
        doubleJump(1f)
    }
}
// 扩展方法的定义,就是在方法的前面加上类前缀
fun Jump.doubleJump(value: Float) : Boolean{
    println("value = $value")
    return  true
}

fun main() {
    // 使用对象调用
    val  ju = Jump()
    /// 外部调用 扩展
    ju.doubleJump(2f)
}

这里有两个关键点:

  • fun Jump.doubleJump(...) 表示给 Jump 增加扩展函数。
  • 调用时直接使用 ju.doubleJump(2f),语法上和成员方法没有区别。

从使用体验上看,扩展函数像是“给类补方法”;但从实现本质看,它并没有真的把方法写进这个类里,而是 Kotlin 编译器提供的一种语法能力。

在 Java 中如何调用 Kotlin 扩展

如果项目里同时存在 Java 和 Kotlin,需要注意:Kotlin 扩展函数在 Java 看起来并不是实例方法,而是普通静态方法调用。

public class Ja vaLession {
    public static void main(String[] args) {
        ///  在Ja va中调用 Kotlin 的扩展
        Lesson04Kt.doubleJump(new Jump(),1f)
    }
}

也就是说:

  • 在 Kotlin 中,它表现得像 jump.doubleJump(1f) 这样的成员调用。
  • 在 Java 中,它会被编译到对应的 Kt 类里,以静态方法形式存在。

这也是判断“扩展是否真能替代成员函数”的一个重要依据:它主要改善的是 Kotlin 侧的调用体验,而不是改变原类本身的结构。

泛型扩展:把能力做成可复用工具

如果扩展函数只服务于某一个具体类型,写法通常比较直接;但当你希望它适用于一整类对象时,就该考虑配合泛型使用。这样做的价值在于,把一个只适用于 MutableList 的能力,推广为适用于任意元素类型的列表操作。

原文给出的例子是列表元素交换:

fun  MutableList.jl_swap(index1: Int, index2: Int){
    var  tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}

fun main() {
    val test2 = mutableListOf("Android o","Android N", "Android M")
    test2.jl_swap(3,5)
}

这段代码展示了泛型扩展的基本形式:

  • fun MutableList.jl_swap(...) 表示扩展接收者是任意元素类型的可变列表。
  • T 让这个函数不再依赖具体元素类型,因此既能处理字符串列表,也能处理其他对象列表。

不过从实际使用角度看,调用 test2.jl_swap(3,5) 时还需要自行保证索引合法,否则会出现越界问题。也就是说,泛型解决的是“适用范围”,并不自动解决“边界安全”。

如何给伴生对象添加扩展

Kotlin 不仅能给普通类实例写扩展,也可以给伴生对象(companion object)增加扩展函数或属性。这样的用法,适合把一些“类级别”的辅助能力,组织到看起来更自然的位置上。

class  Jump{
    companion object{
    }
}
fun Jump.Companion.run(str: String){
    println(str)
}

fun main() {
    Jump.run("aa")
}

上面的写法等价于“给 Jump 的伴生对象补了一个 run 方法”。因此在调用时,可以直接写成 Jump.run("aa")

这种形式的实用点在于:

  • 让工具能力挂在类名上,调用位置更集中;
  • 适合放置工厂方法、格式化方法、类级别辅助逻辑等;
  • 比散落在顶层函数里更容易被发现。

常见扩展函数:let、run、apply 怎么区分

Kotlin 标准库中定义了大量扩展函数,其中最常见、也最容易混淆的一组就是作用域函数。原文重点提到了 letrunapply。它们都能把一段逻辑收进对象上下文里,但返回值和适用场景不同。

let、run、apply 三个 Kotlin 常见扩展函数的用途与返回值对比图
let、run、apply 用法对比这组作用域函数最容易混淆,图中重点对比它们的返回值、典型用途和示例场景。

let:限制作用域,也常用于判空

let 常被用在两类场景里:一类是把变量限制在一个局部作用域内,减少外部污染;另一类是配合可空类型做判空调用。

// let扩展函数,类后面加上代表参数可能为空,使用的时候注意判空
fun testLet(str: String){
    str.let {
        var str2 = "let 扩展"
        println(it + str2)
    }
    // str2 外部无法访问

    // 判空用法,当str为空,则不会触发闭包里面的逻辑
    str.let {
        println(it.length)
    }

}

可以这样理解:

  • str.let { ... } 会把 str 作为 it 传入闭包;
  • str.let { ... } 则是 Kotlin 中很常见的空安全写法,只有在 str 不为空时才执行块内逻辑。

如果你想少写显式的 if (x != null)let 往往是最先想到的工具。

run:直接在对象上下文中执行,并返回结果

run 的特点是:在对象上下文里执行一段代码,并把闭包最后一行的值,或者显式 return 的表达式,作为结果返回。

// 返回值为最后一行的值或者指定的return的表达式,在run函数中可以直接访问实例的公有属性和方法。
fun  testRun(jump: Jump): String{
    jump.run {
        test()
        println("ddd")
        return  "222"
    }
}

它更适合“我想基于这个对象执行一段逻辑,并顺手得到一个结果”的场景。和 let 相比,run 更强调返回值;和 apply 相比,它返回的不是对象本身,而是代码块的计算结果。

apply:适合初始化对象,返回对象本身

applyrun 在结构上很像,区别主要在返回值:run 返回闭包结果,apply 返回调用它的对象本身。

这使得 apply 特别适合对象初始化、属性批量赋值、View 绑定等场景。

fun main() {
    // apply函数的作用是:调用某对象的apply函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象。
    // 从结构上来看apply函数和run函数很像,唯一不同点就是它们各自返回的值不一样,run函数是以闭包形式返回最后一行代码的值,
    ArrayList().apply {
        add("dd")
        add("333")
    }.run {
        for (s in this){
            println(s)
        }
    }

}

从这段示例也能看出一个很常见的组合方式:先用 apply 完成对象构建,再用 run 或其他函数继续处理结果。

写扩展时,重点看这三个判断标准

回到实际开发,扩展函数并不是“任何工具方法都该改写成扩展”。是否适合用扩展,通常可以先看下面三个判断标准:

  • 这个能力是否天然依附于某个类型,比如“字符串格式化”“列表交换元素”“View 绑定行为”。
  • 你是否无法修改原类源码,或者不希望为了一个小能力引入继承层次。
  • 调用体验是否会因为扩展而明显更自然,而不是让 API 语义变得模糊。

如果这三个问题里,大多数答案都是肯定的,那么扩展通常就是合适选择。反过来,如果逻辑与接收者关系不强,写成普通顶层函数往往更清晰。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多