Kotlin 的扩展是这门语言里非常实用的一组能力:遇到第三方类不能改、现有类型又缺少顺手方法时,不必继承,也不必额外套一层装饰器,就能直接补上需要的函数或属性。对日常业务开发来说,它既能减少样板代码,也会直接影响 API 的可读性。
这篇文章按“怎么写、适合什么时候用、常见扩展之间有什么区别”的顺序梳理 Kotlin 扩展。看完后,你可以判断一个场景更适合写普通工具函数,还是改成扩展函数;同时也能分清 let、run、apply 这些常见扩展在返回值和使用方式上的差别。
扩展函数是什么,适合解决什么问题
扩展函数的核心价值,是在不修改原类定义的前提下,为一个已有类型增加新的调用方式。最常见的场景是:类来自第三方库、系统 SDK,或者已经在项目中稳定使用,不方便直接改源码,但你又希望它像“原生成员函数”一样被调用。

在 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 标准库中定义了大量扩展函数,其中最常见、也最容易混淆的一组就是作用域函数。原文重点提到了 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:适合初始化对象,返回对象本身
apply 和 run 在结构上很像,区别主要在返回值: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 语义变得模糊。
如果这三个问题里,大多数答案都是肯定的,那么扩展通常就是合适选择。反过来,如果逻辑与接收者关系不强,写成普通顶层函数往往更清晰。







