知识图库
Java知识库
JDK线程池实现原理
Java中的强、软、弱、虚引用
深入拆解Java虚拟机
01 开篇词 | 为什么我们要学习Java虚拟机?
02 Java代码是怎么运行的?
03 Java的基本类型
04 Java虚拟机是如何加载Java类的?
05 JVM是如何执行方法调用的?(上)
06 JVM是如何执行方法调用的?(下)
7 JVM是如何处理异常的?
Java面试常见问题整理
Java面试常见问题-Java 基础篇
Java面试常见问题-Jvm篇
Java面试常见问题-并发篇
Android知识库
Kotlin编程第一课
1 开篇词 | 入门Kotlin有多容易,精通Kotlin就有多难
2 Kotlin基础语法:正式开启学习之旅
3 面向对象:理解Kotlin设计者的良苦用心
4 Kotlin原理:编译器在幕后干了哪些“好事”?
5 实战:构建一个Kotlin版本的四则运算计算器
6 object关键字:你到底有多少种用法?
7 扩展:你的能力边界到底在哪里?
8 高阶函数:为什么说函数是Kotlin的“一等公民”?
9 实战:用Kotlin写一个英语词频统计程序
10 加餐一 | 初识Kotlin函数式编程
11 委托:你为何总是被低估?
12 泛型:逆变or协变,傻傻分不清?
13 注解与反射:进阶必备技能
14 实战:用Kotlin实现一个网络请求框架KtHttp
15 加餐二 | 什么是“表达式思维”?
16 加餐三 | 什么是“不变性思维”?
17 加餐四 | 什么是“空安全思维”?
18 春节刷题计划(一)| 当Kotlin遇上LeetCode
19 春节刷题计划(二)| 一题三解,搞定版本号判断
20 春节刷题计划(三)| 一题双解,搞定求解方程
21 春节刷题计划(四)| 一题三解,搞定分式加减法
22 什么是“协程思维模型”?
23 如何启动协程?
24 挂起函数:Kotlin协程的核心
25 Job:协程也有生命周期吗?
26 Context:万物皆为Context?
27 实战:让KtHttp支持挂起函数
28 期中考试 | 用Kotlin实现图片处理程序
29 题目解答 | 期中考试版本参考实现
30 Channel:为什么说Channel是“热”的?
31 Flow:为什么说Flow是“冷”的?
32 select:到底是在选择什么?
33 并发:协程不需要处理同步吗?
34 异常:try-catch居然会不起作用?坑!
35 实战:让KtHttp支持Flow
36 答疑(一)| Java和Kotlin到底谁好谁坏?
37 集合操作符:你也会“看完就忘”吗?
38 协程源码的地图:如何读源码才不会迷失?
39 图解挂起函数:原来你就是个状态机?
40 加餐五 | 深入理解协程基础元素
41 launch的背后到底发生了什么?
42 Dispatchers是如何工作的?
43 CoroutineScope是如何管理协程的?
44 图解Channel:如何理解它的CSP通信模型?
45 图解Flow:原来你是只纸老虎?
46 Java Android开发者还会有未来吗?
47 Kotlin与Jetpack简直是天生一对!
48 用Kotlin写一个GitHub Trending App
49 结课测试 | “Kotlin编程第一课”100分试卷等你来挑战!
50 结束语 | 不忘初心
Android Framework 教程—基础篇
01 Ubuntu 使用快速入门
02 Make 构建工具入门
03 理解 Unicode UTF-8 UTF-16 UTF-32
04 Linux Shell 脚本编程入门1——核心基础语法
05 SeAndroid 使用极速上手
06 理解 C++ 的 Memory Order
07 AOSP 极速上手
08 系统开发工具推荐
09 添加 Product
运动相关知识
爱上跑步
01 开篇词 | 跑步,不那么简单的事儿
02 跑两步就喘了,是不是我不适合跑步?
03 正确的跑步姿势是什么样的?
04 为什么跑步要先热身?
05 怎样制定你的第一个10公里跑步计划?
06 快跑和慢跑,哪个更燃脂?
07 普通跑步者应该如何选择跑鞋?
08 买跑步装备,不要踩这些坑儿
09 跑步前到底应不应该吃东西?
10 跑步到底伤不伤膝盖?
11 参加了20场马拉松,我是如何准备的?
12 除了马拉松,还能参加哪些跑步赛事?
13 热点问题答疑 :跑完第二天浑身疼,还要不要继续跑?
健身房计划
[DeepSeek]减脂塑形计划
【DeepSeek】训练周期安排
每日餐饮热量控制
减脂期间食物推荐避坑指南
HarmonyOS知识库
其他知识类目
心理学相关
如何学点心理学——关于非专业人士学心理学的一点建议
投射性认同
-
+
首页
15 加餐二 | 什么是“表达式思维”?
在开篇词当中,我曾经说过,学好Kotlin的关键在于思维的转变。在上一次加餐课程当中,我给你介绍了Kotlin的函数式编程思想,相信你对Kotlin的“函数思维”已经有了一定的体会。那么今天这节课,我们就来聊聊Kotlin的表达式思维。 所谓编程思维,其实是一种非常抽象的概念,很多时候是只可意会不可言传的。不过,从某种程度上看,学习编程思维,比学习编程语法还要重要。因为**编程思维决定着我们的代码整体的架构与风格**,而具体的某个语法反而没那么大的影响力。当然,如果对Kotlin的语法没有一个全面的认识,编程思维也只会是空中楼阁。 所以,准确地来说,掌握Kotlin的编程思维,是在掌握了Kotlin语法基础上的一次升华。这就好比是,我们学会了基础的汉字以后开始写作文一样。学了汉字以后,如果不懂得写作的技巧,是写不出优美的文章的。同理,如果学了Kotlin语法,却没有掌握它的编程思维,也是写不出优雅的Kotlin代码的。 好,那么接下来,我们就来看看Kotlin的表达式思维。 ### 表达式思维 在正式开始学习表达式思维之前,我们先来看一段简单的Kotlin代码。 ```kotlin var i = 0 if (data != null) { i = data } var j = 0 if (data != null) { j = data } else { j = getDefault() println(i) } var k = 0 if (data != null) { k = data } else { throw NullPointerException() } var x = 0 when (data) { is Int -> x = data else -> x = 0 } var y = 0 try { y = "Kotlin".toInt() } catch (e: NumberFormatException) { println(e) y = 0 } 这些代码,如果我们用Java的思维来分析的话,是挑不出太多毛病的。但是站在Kotlin的角度,就完全不一样了。 利用Kotlin的语法,我们完全可以将代码写得更加简洁,就像下面这样: val i = data ?: 0 val j = data ?: getDefault().also { println(it) } val k = data?: throw NullPointerException() val x = when (data) { is Int -> data else -> 0 } val y = try { "Kotlin".toInt() } catch (e: NumberFormatException) { println(e) 0 } ``` 这段代码看起来就简洁了不少,但如果你有Java经验,你在写代码的时候,脑子里第一时间想到的一定不是这样的代码模式。这个,也是我们需要格外注意培养表达式思维的原因。 不过,现在你心里可能已经出现了一个疑问:Kotlin凭什么就能用这样的方式写代码呢?其实这是因为:if、when、throw、try-catch这些语法,在Kotlin当中都是表达式。 那么,这个“表达式”到底是什么呢?其实,与表达式(Expression)对应的,还有另一个概念,我们叫做语句(Statement)。这两者的准确定义其实很复杂,你可以点击我这里给出的链接去看看它们之间区别。 不过我们可以先简单来概括一下:表达式,是一段可以产生值的代码;而语句,则是一句不产生值的代码。这样解释还是有些抽象,我们来看一些例子: ```kotlin val a = 1 // statement println(a) // statement // statement var i = 0 if (data != null) { i = data } // 1 + 2 是一个表达式,但是对b的赋值行为是statement val b = 1 + 2 // if else 整体是一个表达式 // a > b是一个表达式 // a - b是一个表达式 // b - a是一个表达式。 fun minus(a: Int, b: Int) = if (a > b) a - b else b - a // throw NotImplementedError() 是一个表达式 fun calculate(): Int = throw NotImplementedError() ``` 这段代码是描述了常见的Kotlin代码模式,从它的注释当中,我们其实可以总结出这样几个规律: - 赋值语句,就是典型的statement; - if语法,既可以作为语句,也可以作为表达式; - 语句与表达式,它们可能会出现在同一行代码中,比如val b = 1 + 2; - 表达式还可能包含“子表达式”,就比如这里的minus方法; - throw语句,也可以作为表达式。 但是看到这里,你的心中应该还是有一个疑问没有解开,那就是:calculate()这个函数难道不会引起编译器报错吗? ```kotlin // 函数返回值类型是Int,实际上却抛出了异常,没有返回Int // ↓ ↓ fun calculate(): Int = throw NotImplementedError() ``` 确实,在刚开始接触Kotlin的时候,我也无法理解这样的代码。直到我弄清楚Kotlin整个类型系统以后,我才真正找到答案。 所以,为了让你能真正理解Kotlin表达式背后的原理,接下来,我们就来系统学习一下Kotlin的类型系统吧。 ### 类型系统 在课程的第1讲我们就学过,在Kotlin当中,Any是所有类型的父类,我们可以称之为根类型。同时,我们也学过,Kotlin的类型还分为可空类型和不可空类型。举个例子,对于字符串类型,就有String、String?,它们两者分别代表了不为空的字符串、可能为空的字符串。 在这个基础上,我们很容易就能推测出,Kotlin的类型体系应该是这样的:  也就是,Any是所有非空类型的根类型;而Any?是所有可空类型的根类型。那么现在,你可能会想到这样的一个问题:Any与Any?之间是什么关系呢? #### Any与Any?与Object 从表面上看,这两个确实没有继承关系。不过,它们之间其实是存在一些微妙的联系的。 在Kotlin当中,我们可以把“子类型”赋值给“父类型”,就像下面的代码一样: ```kotlin val s: String = "" val any: Any = s ``` 由于String是Any的子类型,因此,我们可以将String类型赋值给Any类型。而实际上,Any和“Any?”之间也是类似的,我们可以将Any类型赋值给“Any?”类型,反之则不行。 ```kotlin val a: Any = "" val b: Any? = a // 通过 val c: Any = b // 报错 ``` 类似的,String类型可以赋值给“String?”类型,反之也不行。你可能会想这是为什么呢? 其实,任何类型,当它被“?”修饰,变成可空类型以后,它就变成原本类型的父类了。所以,**从某种程度上讲,我们可以认为“Any?”是所有Kotlin类型的根类型**。它的具体关系如下图所示:  因此,我们可以说:虽然Any与Any?之间没有继承的关系,但是我们可以将Any看作是Any?的子类;String类型可以看作是String?的子类。 而由于Any与“Any?”之间并没有明确的继承关系,但它们又存在父子类型的关系,所以在上面的示意图中,我们用虚线来表示。 所以到这里,我们就弄明白了一个问题:Kotlin的Any与Java的Object之间是什么关系? 那么,答案也是显而易见的,Java当中的Object类型,对应Kotlin的“Any?”类型。但两者并不完全等价,因为Kotlin的Any可以没有wait()、notify()之类的方法。因此,我们只能说Kotlin的“Any?”与Java的Object是大致对应的。Intellij有一个功能,可以将Java代码转换成Kotlin代码,我们可以借此印证。 这是一段Java代码,它有三个方法,分别是可为空的Object类型、不可为空的Object类型,以及无注解的Object类型。 ```kotlin public class TestType { @Nullable // 可空注解 public Object test() { return null; } public Object test1() { return null; } @NotNull // 不可空注解 public Object test2() { return 1; } } ``` 上面的代码转换成Kotlin以后,会变成这样: ```kotlin class TestType { fun test(): Any? { return null } fun test1(): Any? { return null } fun test2(): Any { return 1 } } ``` 由此可见,在没有注解标明可空信息的时候,Object类型是会被当作“Any?”来看待的。而在有了注解修饰以后,Kotlin就能够识别出到底是Any,还是“Any?”。 #### Unit与Void与void 在Kotlin当中,除了普通的Any、String的类型之外,还有一个特殊的类型,叫做Unit。而Unit这个类型,经常会被拿来和Java的Void、void来对比。 那么在这里,你首先需要知道的是:在Java当中,Void和void不是一回事(注意大小写),前者是一个Java的类,后者是一个用于修饰方法的关键字。如下所示: ```kotlin public final class Void { public static final Class TYPE = (Class) Class.getPrimitiveClass("void"); private Void() {} } ``` 从语法含义上来讲,Kotlin的Unit与Java的void更加接近,但Unit远不止于此。在Kotlin当中,Unit也是一个类,这点跟Void又有点像。比如,在下面的代码中,Unit是一个类型的同时,还是一个单例: ```kotlin public object Unit { override fun toString() = "kotlin.Unit" } ``` 所以,我们就可以用Unit写出很灵活的代码。就像下面这样: ```kotlin fun funUnit(): Unit { } fun funUnit1(): Unit { return Unit } ``` 可以看到,当返回值类型是Unit的时候,我们既可以选择不写return,也可以选择return一个Unit的单例对象。 另外,在使用泛型编程的时候,当T类型作为返回值类型的时候,我们传入Unit以后,就不再需要写return了。 ```kotlin interface Task { fun excute(any: Any): T } class PrintTask: Task { override fun excute(any: Any) { println(any) // 这里写不写return都可以 } } ``` 更重要的是,Unit还有助于我们实现函数类型。 ```kotlin val f: () -> Unit = {} ``` 所以,Kotlin的Unit与Java的Void或者void并不存在等价的关系,但它们之间确实存在一些概念上的相似性。至此,我们也可以更新一下前面那个类型系统关系图了:  可见,Unit其实和String类型一样,就是一个普通的类。只是因为Kotlin编译器会特殊对待它,当Unit作为返回值类型的时候,可以不需要return。 好了,接着,我们再来看看Kotlin当中经常被提到的Nothing类型。 #### Nothing 在有了前面的基础以后呢,Nothing就很容易理解了。其实,**Nothing就是Kotlin所有类型的子类型**。 Nothing的概念与“Any?”恰好相反。“Any?”是所有的Kotlin类型的父类,Nothing则是所有类型的子类。如果用一张图来概括,大概会是这样的:  事实上,像Nothing这样的概念,在函数式编程当中,也被叫做底类型(Bottom Type),因为它位于整个类型体系的最底部。 而了解了Kotlin的Nothing类型以后,我们其实就可以尝试着来解答前面例子中留下来的疑问了: ```kotlin // 函数返回值类型是Int,实际上却抛出了异常,没有返回Int // ↓ ↓ fun calculate(): Int = throw NotImplementedError() // 不会报错 // 函数返回值类型是Any,实际上却抛出了异常,没有返回Any // ↓ ↓ fun calculate1(): Any = throw Exception() // 不会报错 // 函数返回值类型是Unit,实际上却抛出了异常,没有返回Unit // ↓ ↓ fun calculate2(): Unit = throw Exception() // 不会报错 ``` 根据这段代码可以发现,不管函数的返回值类型是什么,我们都可以使用抛出异常的方式来实现它的功能。这样我们其实就可以推测出一个结论:throw这个表达式的返回值是Nothing类型。而既然Nothing是所有类型的子类型,那么它当然是可以赋值给任意其他类型的。 可是,我们如何才能印证这个结论是否正确呢?很简单,我们可以把两个函数的返回值类型都改成Nothing,然后看看编译器会不会报错: ```kotlin // 不会报错 fun calculate(): Nothing = throw NotImplementedError() // 不会报错 fun calculate1(): Nothing = throw Exception() // Nothing构造函数是私有的,因此我们无法构造它的实例 public class Nothing private constructor() ``` 可见,编译器仍然不会报错。这也就印证了我们前面的猜测:throw表达式的返回值类型是Nothing。 另外,我们应该也注意到了Nothing类的构造函数是私有的,因此我们无法构造出它的实例。而当Nothing类型作为函数参数的时候,一个有趣的现象就出现了: ```kotlin // 这是一个无法调用的函数,因为找不到合适的参数 fun show(msg: Nothing) { } show(null) // 报错 show(throw Exception()) // 虽然不报错,但方法仍然不会调用 ``` 这里我们定义的这个show方法,它的参数类型是Nothing,而由于Nothing的构造函数是私有的,这就导致我们将无法调用show这个函数,除非我们抛出异常,但这没有意义。这个概念在泛型星投影的时候是有应用的,具体你可以点击这个链接去查看详情。 而除此之外,Nothing还有助于编译器进行代码流程的推断。比如说,当一个表达式的返回值是Nothing的时候,就往往意味着它后面的语句不再有机会被执行。如下图所示:  在了解了Unit与Nothing这两个不可空的类型以后,我们再来看看它们对应的可空类型。 #### Unit?与Nothing? 也许你也注意到了,Unit对应的还有一个“Unit?”类型,那么这个类型有什么意义吗? 我们可以看看下面的代码: ```kotlin fun f1(): Unit? { return null } // 通过 fun f2(): Unit? { return Unit } // 通过 fun f3(): Unit? { throw Exception() } // 通过 fun f4(): Unit? { } // 报错,缺少return ``` 可见,Kotlin编译器只会把Unit类型当作无需返回值的类型,而Unit?则不行。 所以,Unit?这个类型其实没有什么广泛的应用场景,因为它失去了原本的编译器特权后,就只能有3种实现方式,即null、Unit单例、Nothing。也就是说,当Unit?作为返回值的时候,我们的函数必须要return一个值了,它返回值的类型可以是null、Unit单例、Nothing这三种情况。 好,接下来我们再来看看“Nothing?”这个类型。 ```kotlin fun calculate1(): Nothing? = null fun calculate2(): Nothing? = throw Exception() ``` 由以上代码示例可知,当Nothing?作为返回值类型的时候,我们可以返回null,或者是抛出异常。这一切都符合预期,而当它作为函数参数的时候,也会有一些有趣的变化。 ```kotlin // 变化在这里 // ↓ fun show(msg: Nothing?) { } show(null) // 通过 show(throw Exception()) // 虽然不报错,但方法仍然不会调用 ``` 可以看到,当参数类型是Nothing?的时候,我们的函数仍然是可以调用的。这其实就能进一步说明一个问题:**Nothing才是底类型,而“Nothing?”则不是底类型**。 这一点其实在前面的类型关系图中就有体现,现在你就可以真正理解了:  到这里相信你也明白了,“Unit?”“Nothing?”这两个类型,其实并没有太多实际的应用场景,不过由于它们是Kotlin类型系统当中特殊的类型,因此我们也应该对它们有个清晰的认识。 这样,在系统学习了Kotlin的类型系统以后,我们对表达式理解就可以更上一层楼了。 ### 表达式的本质 我们再来看看表达式的定义:表达式,是一段可以产生值的代码;而语句,则是一句不产生值的代码。 也许你听说过这样一句话:在Kotlin当中,一切都是表达式。注意!这句话其实是错的。因为Kotlin当中还是存在语句的,比如while循环、for循环,等等。 不过,如果我们换个说法:在Kotlin当中,大部分代码都是表达式。这句话就对了。Kotlin的类型系统当中的Unit和Nothing,让很多原本无法产生返回值的语句,变成了表达式。 我们来举个例子: ```kotlin // statement println("Hello World.") // println("Hello World.") 变成了表达式 val a = println("Hello World.") // statement throw Exception() // throw 变成了表达式 fun test1() = throw Exception() ``` 从上面的代码案例中,我们可以总结出两个规律。 - 由于Kotlin存在Unit这个类型,因此println(“Hello World.”)这行代码也可以变成表达式,它所产生的值就是Unit这个单例。 - 由于Kotlin存在Nothing这个类型,因此throw也可以作为表达式,它所产生的值就是Nothing类型。 注意,因为Java当中不存在Unit、Nothing这样的类型,所以Java里返回值为void的函数是无法成为表达式的,另外,throw这样的语句也是无法成为表达式的。而也正是因为Kotlin这样的类型系统,才让大部分的语句都摇身一变成为了表达式。因为Unit、Nothing在Kotlin编译器看来,也是所有类型当中的一种。 可以说,Unit和Nothing填补了原本Java当中的类型系统,让Kotlin的类型系统更加全面。也正因为如此,Kotlin才可以拥有真正的函数类型,比如: ```kotlin val f: (String) -> Unit = ::println ``` 可以看到,如果不存在Unit这个类型,我们是无法描述println这个函数的类型的。正因为println函数的返回值类型为Unit,我们才可以用“(String) -> Unit”来描述它。 换句话说就是:Kotlin的类型系统让大部分的语句都变成了表达式,同时也让无返回值的函数有了类型。 而所谓的表达式思维,其实就是要求我们开发者在编程的时候,时刻记住Kotlin大部分的语句都是可以作为表达式的,并且由于表达式都是有返回值的,这也就让我们可以用一种全新的思维来写代码。这在很多时候,都可以大大简化我们的代码逻辑。 那么现在,我们再回过头看之前的代码,就会觉得很顺眼了: ```kotlin val i = data ?: 0 val j = data ?: getDefault().also { println(it) } val k = data?: throw NullPointerException() val x = when (data) { is Int -> data else -> 0 } val y = try { "Kotlin".toInt() } catch (e: NumberFormatException) { 0 } ``` ### 小结 好,今天这节加餐,到这里就接近尾声了,我们来做个简单的总结。 - 所谓的表达式思维,就是要时刻记住:Kotlin大部分的语句都是表达式,它是可以产生返回值的。- 利用这种思维,往往可以大大简化代码逻辑。 - Any是所有非空类型的根类型,而“Any?”才是所有类型的根类型。 - Unit与Java的void类型,代表一个函数不需要返回值;而“Unit?”这个类型则没有太多实际的意义。 - 当Nothing作为函数返回值的时候,意味着这个函数永远不会返回结果,而且还会截断程序的后续流程。Kotlin编译器也会根据这一点,进行流程分析。 - 当Nothing作为函数参数的时候,就意味着这个函数永远无法被正常调用。这在泛型星投影的时候是有一定应用的。 - 另外,Nothing可以看作是“Nothing?”子类型,因此,Nothing可以看作是Kotlin所有类型的底类型。 - 正是因为Kotlin在类型系统当中,加入了Unit、Nothing这两个类型,才让大部分无法产生值的语句摇身一变,成为了表达式。这也是“Kotlin大部分的语句都是表达式”的根本原因。 ### 思考题 这节课,我们学习了表达式思维,请问,你觉得它和我们前面学到的“函数式编程”有联系吗?为什么?欢迎在留言区分享你的答案和思考,也欢迎你把今天的内容分享给更多的朋友。
嘿手大叔
2024年10月28日 16:10
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码