76.Kotlin扩展函数简介

第一部分:Kotlin 扩展函数简介

Kotlin 语言自带很多扩展函数,它们通过在已有类型上扩展新的作用域函数(scope function)来改善代码可读性与简洁性。这些扩展函数不仅能够改善代码风格,同时还能提供链式调用的便利。常见的扩展函数有:

  • let
  • also
  • apply
  • run
  • with

这些扩展函数可以让我们在对象上执行一些额外操作,同时还允许我们避免样板代码(boilerplate)和显式的数据传递。每个函数在设计时都有其侧重点,以下将分别介绍。


第二部分:let 函数

2.1 let 函数的基本概念

let 是 Kotlin 中最常使用的扩展函数之一,其标准定义如下(简化示意):

public inline fun  T.let(block: (T) -> R): R { ... }

简单说,let 接受调用对象作为接收者,但在 lambda 表达式中,它以参数 it(如果不定义参数名的话)传入。let 会返回 lambda 表达式中的最后一行表达式的值。这说明它的返回值由 lambda 决定。

2.2 let 的参数传递方式

在 let 中,调用对象会被作为 lambda 的唯一参数传递进去,该参数默认命名为 it(你也可以自己命名)。因此在 lambda 内部,若你需要引用调用对象,就需要使用 it。例如:

val name = "Alice"
val length = name.let { 
    println("当前字符串:$it")
    // 返回 it 的长度
    it.length
}

在这个例子中,字符串 "Alice" 就是 let 调用的对象,并以 it 的形式传入 lambda,最后返回值为 length。

2.3 let 的使用场景

  • 非空判断与安全调用
    let 常常与空安全操作符(?.)搭配使用,可以避免空指针异常。例如:

val nullableName: String? = "Bob"
nullableName?.let {
    println("非空字符串:$it")
    // 可在空不为null时执行更多操作
}
  • 局部变量的作用域限制
    let 函数可以用来将一段代码包裹在一个局部作用域内,从而避免在代码中引入过多的局部变量。

  • 链式调用中的转换
    可以使用 let 在链式调用中转换某个值,而不影响后续链式调用,比如计算值并返回最后结果。

2.4 let 的代码示例

下面是一份完整的 Kotlin 文件,展示了 let 的使用以及详细注释说明:

package com.example.extensions

fun main() {
    // 示例1:对非空字符串执行操作
    val name: String? = "Alice"
    name?.let { nonNullName ->
        println("Inside let: 当前字符串是 $nonNullName")
        // 返回处理结果,这里返回的是字符串长度
        val length = nonNullName.length
        println("字符串长度为 $length")
    }
    
    // 示例2:用于链式调用,转换数据类型
    val originalValue = "12345"
    val processedValue: Int = originalValue.let {
        println("转换前的字符串:$it")
        // 可以做一些转换操作,比如转换成 Int
        it.toIntOrNull() ?: 0
    }
    println("转换后的整型值为 $processedValue")
    
    // 示例3:用作局部计算
    val result = "Hello, World!".let {
        println("字符串是:$it")
        // 可以返回任意类型
        "$it - Processed"
    }
    println("let 返回的结果:$result")
}

2.5 总结 let

  • let 的参数以 it 的形式传递,也可以指定名称。
  • 返回 lambda 表达式最后一行的值。
  • 主要用途在于进行空值判断、值转换和局部作用域的封装。

第三部分:also 函数

3.1 also 函数的基本概念

also 的定义如下:

public inline fun  T.also(block: (T) -> Unit): T { ... }

与 let 类似,also 通过 lambda 的参数 it 接收对象。但与 let 不同的是,also 返回调用对象本身,而非 lambda 中最后行的值。因此也适用于链式调用。主要用于执行额外操作,例如日志记录、调试信息、或对调用对象进行一些额外不影响主要流程的处理。

3.2 also 的参数传递方式

在 also 中,调用对象同样作为参数传递至 lambda 中,默认名称为 it。例如:

val number = 42
val sameNumber = number.also {
    println("Logging number: $it")
}

这里,it 表示调用该扩展函数的对象,而整个 also 调用最终返回 number 本身。

3.3 also 的使用场景

  • 调试和日志记录
    also 常用于插入调试代码或日志记录,不改变对象数据,但让链式调用更易于阅读。

  • 附加操作
    当需要在对象上执行额外操作,而不期望返回值变化时,使用 also 是理想的选择。例如,在对象构造完成时记录信息,再继续使用该对象。

3.4 also 的代码示例

如下代码展示了 also 的一般用法:

package com.example.extensions

data class User(var username: String, var age: Int)

fun main() {
    // 创建一个 User 对象并在 also 中记录日志
    val user = User("Bob", 25).also {
        println("创建 User 对象:$it")
    }
    
    // 也可以在链式调用中使用 also 保存中间结果
    val result = "Kotlin".also { 
        println("原始字符串 $it")
    }.uppercase()
    println("经 uppercase 后结果:$result")
}

3.5 总结 also

  • 参数采用 it 传递,表示当前对象。
  • 用于执行额外操作(如日志记录、验证等)。
  • 返回对象本身以便于链式调用,不会改变原始对象的值。

第四部分:apply 函数

4.1 apply 函数的基本概念

apply 是一个非常常用的扩展函数,其定义如下:

public inline fun  T.apply(block: T.() -> Unit): T { ... }

在 apply 中,调用对象作为 lambda 表达式的接收者(即 this),而不是作为参数传递。这意味着你可以直接使用 this 来访问对象的属性和方法,而且通常可以省略 this 关键字,因为在 lambda 内部默认就是当前对象。apply 总是返回调用对象本身,非常适合于对象的初始化或配置。

4.2 apply 的参数传递方式

在 apply 中,lambda 的接收者为调用 apply 的对象。也就是说,lambda 内部可以直接访问对象的属性和方法,而不用通过 it 来调用。例如:

val person = Person().apply {
    name = "Charlie"
    age = 30
}

在这里,name 和 age 是直接设置的,因为 this 被隐式地引用为 Person 对象。

4.3 apply 的使用场景

  • 对象初始化和配置
    apply 最常见场景就是在创建对象后对其属性进行初始化或配置。这样可以减少样板代码并提高代码可读性。

  • 构建对象
    当构建一个复杂对象时,可以利用 apply 进行属性赋值操作,并返回构建好的对象。

4.4 apply 的代码示例

下面给出一个完整描述 apply 典型用法的示例代码:

package com.example.extensions

data class Person(var name: String = "", var age: Int = 0)

fun main() {
    // 使用 apply 配置 Person 对象
    val person = Person().apply {
        // 在 lambda 内部直接访问对象的属性,这里的 this 指代 Person 对象
        name = "Diana"
        age = 28
        println("Inside apply: name = $name, age = $age")
    }
    println("Configured Person: $person")
    
    // 常用于链式调用初始化
    val list = mutableListOf().apply {
        add(1)
        add(2)
        add(3)
        println("Inside apply, list: $this")
    }
    println("Final list: $list")
}

4.5 总结 apply

  • 使用 this 作为 lambda 接收者,可以直接访问对象的属性。
  • 主要用于对象初始化和配置,不改变返回值,始终返回同一对象。
  • 应用场景包括构建对象、设置属性等操作。

第五部分:run 函数

5.1 run 函数的基本概念

run 是一个有两种形式的扩展函数。扩展形式如下:

public inline fun  T.run(block: T.() -> R): R { ... }

其非扩展形式则是例如:

public inline fun  run(block: () -> R): R { ... }

在扩展形式中,run 与 apply 类似,也使用 this 作为 lambda 的接收者,但不同的是 run 返回 lambda 的最后一行表达式结果,而不是调用对象本身;这使得 run 更适合执行一些计算,然后返回一个值。

在非扩展形式中,run 可以用来执行一段代码块,并返回计算结果,适用于那些需要将局部代码块的结果传递出去的场景。

5.2 run 的参数传递方式

  • 在扩展形式的 run 中,lambda 接收者为调用对象,使用 this 来引用对象,可以省略显式的 this。
  • 在非扩展形式中,代码块没有接收者,纯粹执行一段代码并返回值。

例如扩展形式:

val person = Person("Eric", 35)
val description = person.run {
    // this 默认为 person
    "Person: name = $name, age = $age"
}

非扩展形式:

val sum = run {
    val a = 10
    val b = 20
    a + b // 返回计算结果 30
}

5.3 run 的使用场景

  • 对象上下文下计算结果
    当需要在对象上下文中计算并返回一个结果时,用 run 更适合。例如,对象的状态运算或格式化结果返回。

  • 临时作用域封装
    非扩展版的 run 可以作为一个代码块,把局部变量和计算封装起来,防止污染外部作用域,并返回一个计算结果。

5.4 run 的代码示例

下面展示 run 的扩展和非扩展使用场景:

package com.example.extensions

data class Person(var name: String, var age: Int)

fun main() {
    // 扩展形式 run:使用对象的上下文,返回 lambda 最后一行的值
    val person = Person("Frank", 40)
    val info: String = person.run {
        println("Inside run: name = $name, age = $age")
        "Person info: $name ($age years old)"
    }
    println("Run result: $info")
    
    // 非扩展形式 run:执行一段代码块,返回代码块的计算结果
    val result: Int = run {
        val x = 100
        val y = 50
        println("Inside plain run: x = $x, y = $y")
        x - y
    }
    println("Plain run result: $result")
}

5.5 总结 run

  • 扩展形式:与 apply 类似,传入对象上下文,返回 lambda 计算结果。
  • 非扩展形式:无接收者,仅执行一段代码块并返回结果。
  • 适用于需要在对象上下文中计算并返回结果或封装一段独立逻辑的场景。

第六部分:with 函数

6.1 with 函数的基本概念

with 是 Kotlin 中的一个标准库函数,其定义如下:

public inline fun  with(receiver: T, block: T.() -> R): R { ... }

与 run 类似,with 将传入的对象作为 lambda 的接收者(this)提供给代码块,但它不是作为扩展函数调用,而是一个普通函数。它返回 lambda 表达式最后一行的结果。

6.2 with 的参数传递方式

在 with 中,第一个参数是传入的对象,作为 lambda 的接收者(this)。在 lambda 内部,不像 let 或 also 那样依赖 it,而是通过 this(可以省略)直接访问对象。

例如:

val person = Person("Grace", 27)
val result = with(person) {
    println("Accessing within with: name = $name, age = $age")
    "Person details: $name is $age years old"
}

6.3 with 的使用场景

  • 对象操作封装
    当需要对同一个对象进行多次操作,且返回计算结果时 with 是一种不错的选择。与 run 的扩展形式极为相似,但区别在于 with 是一个普通函数,传入对象后再调用代码块,可以让代码逻辑更清晰。

  • 避免重复引用对象
    在代码块内直接使用对象,各个属性及方法都可以直接操作,避免重复的对象引用。

6.4 with 的代码示例

下面给出一个完整的示例,展示 with 的使用方法:

package com.example.extensions

data class Person(var name: String = "", var age: Int = 0)

fun main() {
    val person = Person("Helen", 33)
    // 使用 with 来对 person 对象进行多次操作并返回计算结果
    val detail = with(person) {
        println("Inside with: name = $name, age = $age")
        // 返回一个描述字符串
        "Person Details: $name is $age years old."
    }
    println("With result: $detail")
}

6.5 总结 with

  • 通过普通函数传入对象作为 receiver,将对象作为 lambda 中的 this。
  • 与 run(扩展版)功能相似,但使用方式略有不同。
  • 适合用于封装对单个对象的多次操作,并返回计算结果。

第七部分:各扩展函数的对比与区别

经过对 let、also、apply、run 和 with 的讲解,我们可以总结出各扩展函数主要的区别如下:

扩展函数 参数传递方式 返回值 主要用途 常见场景
let 以 it 传递 Lambda 最后行值 对对象进行转换、处理、非空安全操作 处理可空对象、数据转换、局部变量作用域
also 以 it 传递 调用对象本身 执行副作用,比如日志、调试及额外操作 记录调试信息、检查值后返回原对象
apply 以 this 传递 调用对象本身 对对象进行初始化或配置 构建或设置对象属性、初始化配置
run 以 this 传递 Lambda 最后行值 执行代码块并返回计算结果 对象上下文下的结果计算、封装代码块
with 以 this 传递 Lambda 最后行值 同 run,但非扩展函数调用 组合操作同一对象,减少重复对象引用

参数区别

  • let 与 also 中,lambda 函数的参数默认命名为 it,这样在 lambda 内部引用调用对象须写成 it.someMethod() 或 it.property
  • apply、run(扩展版)和 with 中,lambda 内部的 this 隐式代表调用对象,可以直接调用或读取对象的属性,而无需额外的参数前缀。注意:在 apply 中,由于是扩展函数,传入的 lambda 表达式不会返回新值,而是始终返回调用对象本身;而 run 和 with 返回的是 lambda 表达式最后一行的结果。

使用场景比较

  • 当你需要对一个可能为 null 的对象进行安全调用时,let 是最佳选择;
  • 如果你只是想在不改变对象的情况下监控或记录一些值,那么 also 很适合;
  • 当你创建一个对象并需要在同一代码块中配置多个属性时,apply 极大地提升代码可读性,避免重复引用变量;
  • 如果你需要在对象上下文下计算某个表达式,并返回一个结果,run(扩展版)会使代码更为流畅;
  • 如果不想链式调用,但需要对一个对象执行多个操作而最终返回一个值,with 是个不错的选择。

第八部分:综合示例与实际应用

为了将上述扩展函数的概念与使用场景更好地结合起来,我们将用一个复杂点的示例来综合展示它们。假设我们正在构建一个简单的用户注册系统,需要对用户对象进行配置、检测与最终返回注册信息。

我们通过以下完整的 Kotlin 文件展示如何采用各种扩展函数来完成工作,并在每一步详细注释说明每种扩展函数的作用和使用场景。

package com.example.registration

data class User(
    var username: String = "",
    var email: String = "",
    var age: Int = 0,
    var isActive: Boolean = false
)

fun main() {
    // 使用 apply 进行对象创建和初始化
    val user = User().apply {
        username = "john_doe"
        email = "[email protected]"
        age = 27
        isActive = false
        println("Inside apply: Initialized user with username = $username, email = $email, age = $age, isActive = $isActive")
    }
    
    // 使用 also 记录用户创建后的调试信息,不改变对象
    user.also {
        println("Logging user creation: $it")
    }
    
    // 使用 let 检查 email 是否为空并转换处理,返回处理结果
    val emailStatus = user.email.let { emailStr ->
        if (emailStr.isNotEmpty()) {
            "Email is valid: $emailStr"
        } else {
            "Email is empty"
        }
    }
    println("Result from let: $emailStatus")
    
    // 使用 run 进行用户数据加工,返回格式化后的用户描述信息
    val userDescription = user.run {
        // 进行一些逻辑处理,比如把用户名大写
        username = username.uppercase()
        isActive = age >= 18
        "User: $username, Email: $email, Age: $age, Active: $isActive"
    }
    println("Formatted user description from run: $userDescription")
    
    // 使用 with 来对用户对象进行综合操作,比如验证并拼接信息
    val registrationSummary = with(user) {
        println("Inside with block: Verifying user details...")
        // 假设这里进行一些复杂逻辑判断
        val status = if (isActive) "Active" else "Inactive"
        "Registration Summary: Username = $username, Status = $status"
    }
    println("Summary from with: $registrationSummary")
}

在这个示例中,展示了每种扩展函数的具体应用:

  1. 使用 apply 完成对象的初始化和属性设置,简化重复引用。
  2. 使用 also 记录日志,但返回的是原始 user 对象,便于后续链式调用。
  3. 使用 let 对对象的单一属性(email)进行检查,并返回一个字符串描述。
  4. 使用 run 在对象上下文中进行逻辑处理,同时返回用户描述信息。
  5. 使用 with 封装对 user 对象的综合操作,并最后返回一个详细的注册摘要信息。

第九部分:深入讨论各扩展函数的优缺点

9.1 let 的优缺点

优点

  • 简单易用。let 能够让你清晰地以非空状态使用对象并返回处理后的结果。
  • 理想的链式调用工具,可以将计算结果传递给下一个函数。
  • 可以将可空对象安全处理、转换和后续逻辑局限在一个闭包内。

缺点

  • 如果滥用 let(比如在不需要转换的时候),可能会使代码阅读性变差,过多嵌套的 let 可能会难以理解。

9.2 also 的优缺点

优点

  • 非常适合用于执行副作用(如调试或记录日志),使得代码更加整洁。
  • 不会改变对象,始终返回调用对象本身,利于链式调用。

缺点

  • 仅用于副作用操作,如果主要逻辑依赖返回的额外信息,则不能使用也不适用。

9.3 apply 的优缺点

优点

  • 简洁地对对象进行初始化、设置属性,使代码整洁而直观。
  • 代码中可以直接引用对象成员,无需前缀,非常有利于对象构造。

缺点

  • 总是返回对象本身,不适用于需要其他返回值的场景。
  • 当对象属性较多或逻辑较复杂时,apply 代码块可能会变得冗长。

9.4 run 的优缺点

优点

  • 灵活,既可以使用对象上下文进行计算,也可以作为非扩展函数直接执行代码块。
  • 可用于封装一段局部逻辑,并返回计算结果,适用于临时变量逻辑封装。

缺点

  • 与 apply 相比,代码块内不能直接修改返回值为对象本身,而是返回 lambda 最后一行的值,一定程度上局限了链式操作。

9.5 with 的优缺点

优点

  • 非扩展函数形式,使得函数调用更直观,传入对象后集中对对象的引用和操作。
  • 同样适用于封装对单个对象的多次操作,降低代码重复调用对象名称的复杂度。

缺点

  • 与 run 无太大差异,但缺乏链式调用的流畅性,因为它不是扩展函数。

第十部分:实战案例解析

为使扩展函数的用法更加贴近日常开发,下面以一个实际的业务场景举例,展示如何使用这些扩展函数来编写清晰而高效的代码。假设我们需要编写一个图片处理模块,功能包括:

  • 将输入图片进行非空判断后转换成灰度图。
  • 在处理过程中记录日志。
  • 对图像对象进行一系列配置。
  • 最后返回一份处理报告。

下面是详细的代码示例和解释:

package com.example.imageprocessor

import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

// 假设我们定义一个简单的图片处理函数
fun processImage(inputFile: File): String {
    // 1. 使用 let 进行非空判断和转换:加载图片文件
    val image: BufferedImage? = inputFile.let {
        println("Attempting to load image from ${it.absolutePath}")
        // 如果文件存在则加载图片,否则返回 null
        if (it.exists()) ImageIO.read(it) else null
    }
    
    // 2. 使用 also 在加载后记录日志并检查状态
    image?.also {
        println("Image successfully loaded: width = ${it.width}, height = ${it.height}")
    }
    
    // 3. 使用 apply 对图片对象进行一系列配置(模仿配置操作)
    val configuredImage = image?.apply {
        // 模拟配置:例如调整图像亮度,截取部分区域等
        println("Applying configuration to the image")
        // ... 这里实际会包含图像处理逻辑
    }
    
    // 4. 使用 run 对处理结果作进一步计算,返回处理报告
    val report = configuredImage?.run {
        println("Processing image to generate report")
        // 返回处理后图片的基本信息字符串
        "Processed image: width = $width, height = $height"
    } ?: "Image processing failed. Image is null."
    
    // 5. 使用 with 封装一段多操作逻辑,比如生成额外的报告内容
    val finalReport = with(inputFile) {
        "File: ${name}, Size: ${length()} bytes. Report: $report"
    }
    return finalReport
}

fun main() {
    val imageFile = File("sample.jpg")
    val processingReport = processImage(imageFile)
    println("Final Report:\n$processingReport")
}

在这个案例中:

  • 使用 let 对传入的 File 对象进行处理,确保在读取图片前可以记录日志及进行简单判断。
  • also 用来做额外的日志记录,不影响后续链式调用。
  • apply 用于模拟对图片对象的配置处理,直接设定处理逻辑。
  • run 用于计算处理结果,并返回最终数据;
  • with 则整合文件信息和处理报告,生成综合性的最终报告。

通过这样的实战案例,我们可以看到扩展函数如何协同工作,使得代码逻辑清晰、结构分明,同时减少了重复的代码和冗长的引用操作。


第十一部分:总结与归纳

经过前面详细的讲解,下面将各扩展函数的核心要点进行再次归纳:

  1. 基本概念

    • let:以 it 作为参数,返回 lambda 最后一行值,适合数据转换、局部判断。
    • also:以 it 作为参数,返回调用对象本身,适合插入副作用操作。
    • apply:以 this 作为lambda 接收者,直接操作对象属性,返回对象本身,适合对象初始化。
    • run:以 this 作为lambda 接收者,返回 lambda 的最后表达式结果,既能操作对象又能返回数据。
    • with:不是扩展函数,而是普通函数,传入对象后封装操作,返回计算结果。
  2. 参数区别

    • let、also 需要通过 it 引用对象;
    • apply、run(扩展形式)和 with 则直接使用 this 进行操作,可以省略 this 关键字,调用对象隐式作为 lambda 的接收者。
  3. 使用场景

    • 针对可空对象的判断做 let;
    • 记录日志和打印调试信息使用 also;
    • 对象初始化和构造使用 apply;
    • 对象内操作计算返回特定数值使用 run;
    • 多次操作同一对象,并返回操作最终结果时使用 with。
  4. 实际应用
    在日常开发中,根据场景需求选择合适的扩展函数,可以使代码简洁流畅,明确代码执行顺序,减少冗余。同时,这些扩展函数可以一起使用,例如在一个业务流程中同时对对象进行配置、检查和结果返回。

总体来说,Kotlin 的扩展函数是其语言设计中很强大的特性,通过理解每个扩展函数的作用和区别,开发者可以编写出更简洁、可读性更强的代码。同时,掌握这些扩展函数也有助于更好地理解 Kotlin 的编程范式与最佳实践。

你可能感兴趣的:(kotlin,开发语言,android)