编者按:本文节选自华章科技出版的 《Kotlin 核心编程》一书中的部分章节。
与 Java 另一点不同在于,Kotlin 声明变量时,引入了 val 和 var 的概念。var 很容易理解,JavaScript 等其他语言也通过该关键字来声明变量,它对应的就是 Java 中的变量。那么 val 又代表什么呢?
如果说 var 代表了 varible(变量),那么 val 可看成 value(值)的缩写。但也有人觉得这样并不直观或准确,而是把 val 解释成 varible+final,即通过 val 声明的变量具有 Java 中的 final 关键字的效果,也就是引用不可变。
提示 我们可以在 IntelliJ IDEA 或 Android Studio 中查看 val 语法反编译后转化的 Java 代码,从中可以很清楚地发现它是用 final 实现这一特性的。
val 的含义:引用不可变
val 的含义虽然简单,但依然会有人迷惑。部分原因在于,不同语言跟 val 相关的语言特性存在差异,从而容易导致误解。
我们先用 val 声明一个指向数组的变量,然后尝试对其进行修改。
因为引用不可变,所以 x 不能指向另一个数组,但我们可以修改 x 指向数组的值。
如果你熟悉 Swift,自然还会联想到 let,于是我们再把上面的代码翻译成 Swift 的版本。
这下连引用数组的值都不能修改了,这是为什么呢?
其实根本原因在于两种语言对数组采取了不同的设计。在 Swift 中,数组可以看成一个 值类型,它与变量 x 的引用一样,存放在栈内存上,是不可变的。而 Kotlin 这种语言的设计思路,更多考虑数组这种大数据结构的拷贝成本,所以存储在堆内存中。
因此,val 声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。如果把数组换成一个 Book 类的对象,如下编写方式会变得更加直观:
首先,这里展示了 Kotlin 中的类不同于 Java 的构造方法,我们会在第 3 章中介绍关于它具体的语法。其次,我们发现 var 和 val 还可以用来声明一个类的属性,这也是 Kotlin 中一种非常有个性且有用的语法,你还会在后续的数据类中再次接触到它的应用。
优先使用 val 来避免副作用
在很多 Kotlin 的学习资料中,都会传递一个原则:优先使用 val 来声明变量。这相当正确,但更好的理解可以是:尽可能采用 val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性,我们会在第 10 章专门探讨这些概念。由于后续的内容我们会经常使用副作用来描述程序的设计,所以我们先大概了解一下什么是副作用。
简单来说,副作用就是修改了某处的某些东西,比方说:
修改了外部变量的值。
IO 操作,如写数据到磁盘。
UI 操作,如修改了一个按钮的可操作状态。
来看个实际的例子:我们先用 var 来声明一个变量 a,然后在 count 函数内部对其进行自增操作。
在以上代码中,我们会发现多次调用 count(1)得到的结果并不相同,显然这是受到了外部变量 a 的影响,这个就是典型的副作用。如果我们把 var 换成 val,然后再执行类似的操作,编译就会报错。
这就有效避免了之前的情况。当然,这并不意味着用 val 声明变量后就不能再对该变量进行赋值,事实上,Kotlin 也支持我们在一开始不定义 val 变量的取值,随后再进行赋值。然而,因为引用不可变,val 声明的变量只能被赋值一次,且在声明时不能省略变量类型,如下所示:
不难发现副作用的产生往往与 可变数据 及 共享状态 有关,有时候它会使得结果变得难以预测。比如,我们在采用多线程处理高并发的场景,“并发访问”就是一个明显的例子。然而,在 Kotlin 编程中,我们推荐优先使用 val 来声明一个本身不可变的变量,这在大部分情况下更具有优势:
这是一种防御性的编码思维模式,更加安全和可靠,因为变量的值永远不会在其他地方被修改(一些框架采用反射技术的情况除外);
不可变的变量意味着更加容易推理,越是复杂的业务逻辑,它的优势就越大。
回到在 Java 中进行多线程开发的例子,由于 Java 的变量默认都是可变的,状态共享使得开发工作很容易出错,不可变性则可以在很大程度上避免这一点。当然,我们说过,val 只能确保变量引用的不可变,那如何保证引用对象的不可变性?你会在第 6 章关于只读集合的介绍中发现一种思路。
var 的适用场景
一个可能被提及的问题是:既然 val 这么好,那么为什么 Kotlin 还要保留 var 呢?
事实上,从 Kotlin 诞生的那一刻就决定了必须拥抱 var,因为它兼容 Java。除此之外,在某些场景使用 var 确实会起到不错的效果。举个例子,假设我们现在有一个整数列表,然后遍历元素操作后获得计算结果,如下:
这是我们非常熟悉的做法,以上代码中的 res 是个局部的可变变量,它与外界没有任何交互,非常安全可控。我们再来尝试用 val 实现:
这就有点尴尬了,必须利用递归才能实现,原本非常简单的逻辑现在变得非常不直观。当然,熟悉 Kotlin 的朋友可能知道 List 有一个 fold 方法,可以实现一个更加精简的版本。
函数式 API 果然拥有极强的表达能力。
可见,在诸如以上的场合下,用 var 声明一个局部变量可以让程序的表达显得直接、易于理解。这种例子很多,即使是 Kotlin 的源码实现,尤其集合类遍历的实现方法,也大量使用了 var。之所以采用这种命令式风格,而不是更简洁的函数式实现,一个很大的原因是因为 var 的方案有更好的性能,占用内存更少。所以,尤其针对数据结构,可能在业务中需要存储大量的数据,所以显然采用 var 是其更加适合的实现方案。
图书简介:https://item.jd.com/12519581.html?dist=jd
相关阅读
评论