【对比Java学Kotlin】作用域函数

2023-10-26

什么是作用域函数?

首先,落脚点是函数,什么的函数呢?能在某个上下文对象(可能是普通对象,也可能是个 Unit)的作用域内执行代码的函数。这里的作用域和 Java 的作用域有所不同,Java 的作用域更多的是指一对闭合的 {} 的内部区域:

void func() {
    String variable;

    {
       String variable;
    }
}

变量独享其所在{}的区域,在这个区域内其他变量或方法都可以引用到这个变量,我们称之为作用域。
而 Kotlin 作用域函数的作用域,则更加灵活了,既可以用 lambda 表达式的方法引用上下文对象,也可以直接在上下文对象内部进行操作:

Person().apply {
    this.name = "zhangsan"
    this.age = 10
}

当然,不会使用作用域函数不影响使用 Kotlin,但是在某些情况下使用作用域函数会使我们的代码更加简洁,具有更好的可读性和可维护性。

使用方法

我们举个最常见的例子,有一个 Person 类如下:

class Person(val name: String, val age: Int, val addr: String) {
    fun moveTo(anotherAddr: String) {}
    fun incrementAge() {}
}

正常情况下我们可以执行如下操作:

val zhangsan = Person("Zhangsan", 20, "Shanghai")
zhangsan.moveTo("Suzhou")
zhangsan.incrementAge()

等效地,我们可以使用作用域函数将上面的操作改写如下:

val zhangsan = Person("Zhangsan", 20, "Shanghai").let {
    it.moveTo("Suzhou")
    it.incrementAge()
}

改写前后二者的执行结果完全一致。同时后者具有更好的可阅读性和简洁性,以及降低出错概率。之所以说可以降低出错概率,是因为往往我们在复制代码的过程中会手误造成错误,比如如下代码:

// zhangsan stuff
val zhangsan = Person("Zhangsan", 20, "Shanghai")
zhangsan.moveTo("Suzhou")
zhangsan.incrementAge()

// lisi stuff
val lisi = Person("Lisi", 20, "Shanghai")
zhangsan.moveTo("Shenzhen")
lisi.incrementAge()

当我们写完 zhangsan,习惯性的复制出 lisi 的代码,但是在改名的时候却遗漏了其中的 zhangsan.moveTo("Shenzhen"),从而导致代码运行出错,而使用作用域函数则可以很好的避免这个问题。这也是“代码越少,错误越少”的体现。

除了 let,还有其他几个作用域函数,其主要区别在于在作用域内能引用当前对象的方式以及返回值。

函数 引用对象 返回值 是否为扩展函数 备注
let it Lambda 结果 判空,不返回最后一行的值,而是返回整个对象,其余与 let 相同
run this lambda 结果 在内部也能引用到当前对象的 this,该点与 apply 相同,不同点是返回最后一行的值
run - lambda 结果
with this lambda 结果 等效于 run,相当于其另一种写法
apply this 当前对象 用于对象属性的赋值等操作,在内部能引用到当前对象的 this,以方便对属性赋值以及调用对象内部方法,返回整个对象
also it 当前对象 打印日志,不返回最后一行的值,而是返回整个对象,其余与 let 相同

使用场景

如下述代码:

var name : String ? = "ZhangSan"
if (!TextUtils.isEmpty(name)) {
    print(name.length)
}

IDE 会报错:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

这是因为,虽然我们给 name 赋了一个非空的值,但是由于代码块不是原子性的,不保证在执行 name.length 时 name 已经是 null 了,可能发生空指针异常。

这个时候,我们可以使用 let 函数对上述代码改造以避免报错:

var name: String? = "ZhangSan"
name?.let { 
    print(it.length)
}

之所以这么写不会报错,是因为 let 代码块里面的 it 变量是 final 的,我们可以运行如下代码进行试验:

var name : String ? = "zhangsan"
name?.let {
    it = null // 编译报错
    println(name == it) // True
    println(name === it) // True,说明二者是同一个引用
}

直接给 it 赋值会导致编译报错,报错信息如下:

Val cannot be reassigned
Null can not be a value of a non-null type String

也就是说,当执行完 ?. 之后传入 let {} 内的是 name 变量非空时的快照,就是一个值,不会为 null,且不能改变。

大家有没有联想到什么?是不是跟 Java 里面的匿名内部类引用外部的变量必须是 final 的原理有点类似?详见 Java 匿名内部类中的外部引用为什么必须是 final 的?

这种写法也一定程度上消灭了可恶的 if-else。

let

let 一般用于非空的对象上,比如上面我们提到的 name?.let{}。在 let 中引用上下文对象一般是通过 it 来实现,在 IDE 中我们可以清楚的看到提示:
在这里插入图片描述
但是为了更好的可读性,我们也可以自己指定名称:

    val zhangsan = Person("Zhangsan", 20, "Shanghai").let { person ->
        person.moveTo("Suzhou")
        person.incrementAge()
        println(person)
    }

注意,let 返回的是 lambda 表达式的结果,上述代码返回的是 Unit。如果我们想让 zhangsan 是 moveTo() 以及 incrementAge() 之后的 Person,我们应该:

    val zhangsan = Person("Zhangsan", 20, "Shanghai").let { person ->
        person.moveTo("Suzhou")
        person.incrementAge()
        person
    }

apply

apply 主要用于配置对象内的属性,比如初始化,在内部以 this 指针引用当前对象,或者我们也可以把 this 指针省略掉。因为其返回值是当前对象,所以特别适合在新建对象的时候使用,字面意思,apply 的意思是 “apply the following assignments to the object”,比如:

    val zhangsan = Person("Zhangsan", 20, "Shanghai").appy { 
        this.moveTo("Suzhou")
        this.incrementAge()
    }
    print(zhangsan)

also

使用 this 引用上下文对象,返回值是该对象本身。根据其含义 “and also do the following with the object.”,多用于打印日志或执行方法。

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

run

使用 this 引用上下文对象,返回值是 lambda 表达式的结果。
run 的作用与 with 类型,但是使用方法与 let 类似。适用场景包括既要在 lambda 内初始化对象,又要做一些方法执行的功能。

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

with

with 通过 this 引用对象,返回的是 lambda 的结果。最好使用 with 调用对象的方法,而不要返回值,with 的意思是 “with this object, do the following”:

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

注意,我们在讨论的过程中,是没有限定上下文对象非空的,即,在空对象上也能执行这些作用域函数:

    fun foo() {
        val name : String? = null
        name.let {println("let")}
        name.run {println("run")}
        name.apply {println("apply")}
        name.also {println("also")}
        with(name) {
            println("with")
        }
    }

输出:
let
run
apply
also
with

而如果我们加上对其中一个加上?,则对应的作用域函数不会执行:

    fun foo() {
        val name : String? = null
        name?.let {println("let")}
        name.run {println("run")}
        name.apply {println("apply")}
        name.also {println("also")}
        with(name) {
            println("with")
        }
    }

输出:
run
apply
also
with

作用域函数不仅可以作用在变量上,也可以作用在 Unit 上:

        fun foo() {
            val name : String? = null
            name.let {println("let")} // let 返回 Unit
                .run {println("run")}
                .apply {println("apply")}
                .also {println("also")}
            with(name) {
                println("with")
            }
        }

输出:
run
apply
also
with

takeIf & takeUnless

出了上述作用域函数,Kotlin 的标准库还提供了 takeIf() 和 takeUnless() 方法,以方便在链式调用中检查对象的状态。 如果为 true,takeIf 返回当前对象,否则返回 null。相反的,如果为 false,takeUnless 返回当前对象,否则返回 null:

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

takeIf 和 takeUnless 一般和作用域函数结合使用,先用 takeIf 和 takeUnless 过滤出我们关心的对象,然后使用 ?.let 去操作这些对象。

fun displaySubstringPosition(input: String, sub: String) {
    input.indexOf(sub).takeIf { it >= 0 }?.let {
        println("The substring $sub is found in $input.")
        println("Its start position is $it.")
    }
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

参考文章

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【对比Java学Kotlin】作用域函数 的相关文章

随机推荐

  • Linux -- 构建KVM虚拟化环境

    硬件系统的配置 硬件和BIOS中虚拟化技术的支持是KVM运行的先决条件 在x86 64架构的处理器中 KVM必需的硬件虚拟化扩展分别为 Intel的虚拟化技术 Intel VT 和AMD的AMD V技术 一般在BIOS中 VT的选项通过 A
  • Github + Hexo 搭建个人博客

    文章目录 Github Hexo 搭建个人博客 快速搭建 安装Node js 添加国内镜像源 安装 Git 注册 Github 安装Hexo 连接Github与本地 写文章 发布文章 图片添加水印 修改样式 文章基本操作 发表文章 修改文章
  • matlab 三法求矩阵权重(算数平均法求权重,几何平均法求权重,特征值法求权重)

    三法求矩阵权重 例子为3 3的矩阵 可根据需要修改 算数平均法求权重 clc clear judge 1 6 9 7 8 9 6 1 9 7 8 7 7 9 1 w zeros 3 3 for i 1 3 for j 1 3 w i j j
  • C 结构体字节对齐规则

    参考 https blog csdn net xiaoxiangyuhai article details 79192781
  • mysql数据库加密密码如何解密_MySQL加密和解密实例详解

    MySQL加密和解密实例详解 数据加密 解密在安全领域非常重要 对程序员而言 在数据库中以密文方式存储用户密码对入侵者剽窃用户隐私意义重大 有多种前端加密算法可用于数据加密 解密 下面我向您推荐一种简单的数据库级别的数据加密 解密解决方案
  • merge与update区别---->你一定要看!

    merge与update区别 注 就因为这2个方法的区别还得我花了太多时间项目迟迟不能做完 但是让我解决了 学到了东西了 这是一段代码 public void updateData Object obj try tx this getSes
  • Mysql的七种join

    对于SQL的Join 在学习起来可能是比较乱的 我们知道 SQL的Join语法有很多inner的 有outer的 有left的 有时候 对于Select出来的结果集是什么样子有点不是很清楚 Coding Horror上有一篇文章 实在不清楚
  • C++技能系列 ( 5 ) - 详解函数入参/返回参使用(值传递/引用传递/指针传递/智能指针传递)

    系列文章目录 C 技能系列 Linux通信架构系列 C 高性能优化编程系列 深入理解软件架构设计系列 高级C 并发线程编程 期待动动小手 点击关注哦 当你休息的时候 一定要想到别人还在奔跑 When you rest we must thi
  • C++学习(六十九)国产先看GPU

    在CPU处理器领域 国产已经涉足了ARM MIPS甚至X86等多种指令集架构 不论自研还是授权都已经有所成就 但在GPU领域 国内厂商面对AMD NVIDIA两座大山恐怕连影都追不上 GPU市场的门槛太高 所以国内搞GPU自主研发的公司也有
  • Spring boot升级为Spring cloud配置修改(服务注册和服务发现简单案例)

    一 Spring Boot 升级Spring Cloud 1 导入Spring Cloud相关jar包 修改pom xml文件 备注 可能不同的Spring Boot版本会对应不同的Spring Cloud版本号 以下配置的Spring B
  • Android学习笔记——列表和适配器

    文章目录 一 ArrayAdapter适配器 二 下拉列表 Spinner 三 XML文件的引用 四 列表视图 listview 五 网格视图 GridView 六 自定义适配器 总结 一 ArrayAdapter适配器 适配器的角色就是将
  • 漏洞深入分析-2021

    前言 随着cve 2021 40444的披露 随机引爆了全球的网络安全 虽然最近微软发布了补丁 但是cve 2021 40444的利用却越发猖狂 0x00 0day样本分析 拿到样本的第一时间 便在自己的沙箱环境下面运行了下 并且从网上下载
  • 操作系统课设之Linux 进程间通信

    前言 课程设计开始了 实验很有意思 写博客总结学到的知识 白嫖容易 创作不易 学到东西才是真 本文原创 创作不易 转载请注明 本文链接 个人博客 https ronglin fun archives 175 PDF链接 见博客网站 CSDN
  • ESP32 开发的坑 太多了,满地都是

    jlink 死活 不能正常工作 1 无法连接到gdb 2连接上了 也无法下载程序 Warn Failed to send data to device LIBUSB ERROR TIMEOUT OpenOCD Error Sending d
  • 算法 常见数学问题

    一 最大公约数 gcd int gcd int a int b if b 0 return a else return gcd b a b 非递归形式 int gcd int a int b int tmp while b 0 tmp a
  • ubuntu20下Qt5.14.2+OpenCV(含Contrib)-4.5.0环境搭建

    Qt若要能处理图片和视频 就必须安装OpenCV 而OpenCV中很多的高级功能如人脸识别等都包含在Contrib扩展模块中 需要将Contrib与OpenCV一起联合编译 目前所用这两个版本都是4 5 0版 一 下载OpenCV Open
  • hd630 黑苹果_黑苹果(hackintosh)技术整合指南

    更新 2019 5 15日 优化结构 去除了扩展 把他们合并到正文 把无关的东西扔后面 2019 4 10号 链接了本站大神翟海生的纯净安装教程 一位简书大佬解释和配置config文件的文章 非常详细 十分推荐 第一 常用的论坛 工具网站介
  • three.js加载管理器LoadingManager实现对纹理图片的加载监听

    一 加载管理器LoadingManager使用介绍 LoadingManager是three js中的加载管理器 用于监控和管理加载资源的过程 通过使用LoadingManager 我们可以在应用程序中方便地加载各种类型的数据 例如模型 纹
  • POJ 1811 Prime Test —— Miller_Rabin+Pollard_Rho

    http poj org problem id 1811 题意 给定一个64位整数 问是否为质数 如果不是 则输出其最小因子 分析 Miller Rabin素数判定 若不是 则Pollard Rho分解质因子 找到最小即可 Miller r
  • 【对比Java学Kotlin】作用域函数

    什么是作用域函数 首先 落脚点是函数 什么的函数呢 能在某个上下文对象 可能是普通对象 也可能是个 Unit 的作用域内执行代码的函数 这里的作用域和 Java 的作用域有所不同 Java 的作用域更多的是指一对闭合的 的内部区域 void