写点什么

Kotlin 核心编程:val 和 var 的使用规则

  • 2020-04-23
  • 本文字数:2878 字

    阅读完需:约 9 分钟

Kotlin核心编程:val 和 var 的使用规则

编者按:本文节选自华章科技出版的 《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 声明一个指向数组的变量,然后尝试对其进行修改。


>>> val x = intArrayOf(1, 2, 3)>>> x = intArrayOf(2, 3, 4)error: val cannot be reassigned>>> x[0] = 2>>> println(x[0])2
复制代码


因为引用不可变,所以 x 不能指向另一个数组,但我们可以修改 x 指向数组的值。


如果你熟悉 Swift,自然还会联想到 let,于是我们再把上面的代码翻译成 Swift 的版本。


let x = [1, 2, 3]x = [2, 3, 4]Swift:: Error: cannot assign to value: 'x' is a 'let' constantx[0] = 2Swift:: Error: cannot assign through subscript: 'x' is a 'let' constant
复制代码


这下连引用数组的值都不能修改了,这是为什么呢?


其实根本原因在于两种语言对数组采取了不同的设计。在 Swift 中,数组可以看成一个 值类型,它与变量 x 的引用一样,存放在栈内存上,是不可变的。而 Kotlin 这种语言的设计思路,更多考虑数组这种大数据结构的拷贝成本,所以存储在堆内存中。


因此,val 声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。如果把数组换成一个 Book 类的对象,如下编写方式会变得更加直观:


class Book(var name: String) {  // 用var声明的参数name引用可被改变    fun printName() {        println(this.name)    }}
fun main(args: Array<String>) { val book = Book("Thinking in Java") // 用val声明的book对象的引用不可变 book.name = "Diving into Kotlin" book.printName() // Diving into Kotlin}
复制代码


首先,这里展示了 Kotlin 中的类不同于 Java 的构造方法,我们会在第 3 章中介绍关于它具体的语法。其次,我们发现 var 和 val 还可以用来声明一个类的属性,这也是 Kotlin 中一种非常有个性且有用的语法,你还会在后续的数据类中再次接触到它的应用。

优先使用 val 来避免副作用

在很多 Kotlin 的学习资料中,都会传递一个原则:优先使用 val 来声明变量。这相当正确,但更好的理解可以是:尽可能采用 val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性,我们会在第 10 章专门探讨这些概念。由于后续的内容我们会经常使用副作用来描述程序的设计,所以我们先大概了解一下什么是副作用。


简单来说,副作用就是修改了某处的某些东西,比方说:


  • 修改了外部变量的值。

  • IO 操作,如写数据到磁盘。

  • UI 操作,如修改了一个按钮的可操作状态。


来看个实际的例子:我们先用 var 来声明一个变量 a,然后在 count 函数内部对其进行自增操作。


var a = 1fun count(x: Int) {    a = a + 1    println(x + a)}>>> count(1)3>>> count(1)4
复制代码


在以上代码中,我们会发现多次调用 count(1)得到的结果并不相同,显然这是受到了外部变量 a 的影响,这个就是典型的副作用。如果我们把 var 换成 val,然后再执行类似的操作,编译就会报错。


val a = 1>>> a = a + 1error: val cannot be ressigned
复制代码


这就有效避免了之前的情况。当然,这并不意味着用 val 声明变量后就不能再对该变量进行赋值,事实上,Kotlin 也支持我们在一开始不定义 val 变量的取值,随后再进行赋值。然而,因为引用不可变,val 声明的变量只能被赋值一次,且在声明时不能省略变量类型,如下所示:


fun main(args: Array<String>) {    val a: Int    a = 1    println(a) // 运行结果为 1}
复制代码


不难发现副作用的产生往往与 可变数据共享状态 有关,有时候它会使得结果变得难以预测。比如,我们在采用多线程处理高并发的场景,“并发访问”就是一个明显的例子。然而,在 Kotlin 编程中,我们推荐优先使用 val 来声明一个本身不可变的变量,这在大部分情况下更具有优势:


  • 这是一种防御性的编码思维模式,更加安全和可靠,因为变量的值永远不会在其他地方被修改(一些框架采用反射技术的情况除外);

  • 不可变的变量意味着更加容易推理,越是复杂的业务逻辑,它的优势就越大。


回到在 Java 中进行多线程开发的例子,由于 Java 的变量默认都是可变的,状态共享使得开发工作很容易出错,不可变性则可以在很大程度上避免这一点。当然,我们说过,val 只能确保变量引用的不可变,那如何保证引用对象的不可变性?你会在第 6 章关于只读集合的介绍中发现一种思路。

var 的适用场景

一个可能被提及的问题是:既然 val 这么好,那么为什么 Kotlin 还要保留 var 呢?


事实上,从 Kotlin 诞生的那一刻就决定了必须拥抱 var,因为它兼容 Java。除此之外,在某些场景使用 var 确实会起到不错的效果。举个例子,假设我们现在有一个整数列表,然后遍历元素操作后获得计算结果,如下:


fun cal(list: List<Int>): Int {    var res = 0    for (el in list) {        res *= el        res += el    }    return res}
复制代码


这是我们非常熟悉的做法,以上代码中的 res 是个局部的可变变量,它与外界没有任何交互,非常安全可控。我们再来尝试用 val 实现:


fun cal(list: List<Int>): Int {    fun recurse(listr: List<Int>, res: Int): Int {        if (listr.size > 0) {            val el = listr.first()            return recurse(listr.drop(1), res * el + el)        } else {            return res        }    }    return recurse(list, 0)}
复制代码


这就有点尴尬了,必须利用递归才能实现,原本非常简单的逻辑现在变得非常不直观。当然,熟悉 Kotlin 的朋友可能知道 List 有一个 fold 方法,可以实现一个更加精简的版本。


fun cal(list: List<Int>): Int {    return list.fold(0) { res, el -> res * el + el }}
复制代码


函数式 API 果然拥有极强的表达能力。


可见,在诸如以上的场合下,用 var 声明一个局部变量可以让程序的表达显得直接、易于理解。这种例子很多,即使是 Kotlin 的源码实现,尤其集合类遍历的实现方法,也大量使用了 var。之所以采用这种命令式风格,而不是更简洁的函数式实现,一个很大的原因是因为 var 的方案有更好的性能,占用内存更少。所以,尤其针对数据结构,可能在业务中需要存储大量的数据,所以显然采用 var 是其更加适合的实现方案。


图书简介https://item.jd.com/12519581.html?dist=jd



相关阅读


Scala复合但不复杂,简单却不容易


Kotlin核心编程:Kotlin,改良的 Java


2020-04-23 10:063998

评论

发布
暂无评论
发现更多内容

Qt | 信号和槽的一些总结

YOLO.

qt 7月月更

MySQL进阶--存储过程以及自定义函数

Java学术趴

7月月更

什么样的知识付费系统功能,更有利于平台与讲师发展?

CRMEB

算法题每日一练---第12天:算式900

知心宝贝

程序员 算法 前端 后端 7月月更

【函数式编程实战】(十) 优雅的处理代码中的时间类

小明Java问道之路

Lambda java8 Stream API 7月月更 签约计划第三季

机器学习如何做到疫情可视化——疫情数据分析与预测实战

是Dream呀

人工智能 机器学习 爬虫 数据可视化 疫情分析

Starfish Os打造的元宇宙生态,跟MetaBell的合作只是开始

EOSdreamer111

Plato Farm在Elephant Swap上铸造的ePLATO是什么?

威廉META

谈谈基于JS实现阻止别人调试通过控制台调试网站的问题

南极一块修炼千年的大冰块

7月月更

本地化、低时延、绿色低碳:阿里云正式启用福州数据中心

阿里云弹性计算

公有云 本地Region

Starfish Os打造的元宇宙生态,跟MetaBell的合作只是开始

威廉META

Redis设计规范

知识浅谈

redis' redis 精讲

C# 窗体应用使用对象绑定 DataGridView 数据绑定

IC00

C# 7月月更

定了!就在7月30日!

腾源会

开源

汽车智能应用生态的下一个趋势:车载小程序

Geek_99967b

车联网 物联网,

一文读懂Plato Farm的ePLATO,以及其高溢价缘由

股市老人

《我的Vivado实战—单周期CPU指令分析》

攻城狮杰森

cpu 计算机组成原理 7月月更 vivado 计算机科学与技术

Bootstrap警告和轮播插件详解【前端Bootstrap框架】

恒山其若陋兮

7月月更

Linux环境快速搭建elasticsearch6.5.4集群和Head插件

程序员欣宸

Java elasticsearch 7月月更

Linux操作系统下Docker的完整部署过程

Java永远的神

Docker 程序员 架构 程序人生 云原生

数据库故障容错之系统时钟故障

CnosDB

时序数据库 开源社区 CnosDB 工程师有话说 CnosDB Tech Talk

鲜衣怒马散尽千金,Vue3.0+Tornado6前后端分离集成Web3.0之Metamask钱包区块链虚拟货币三方支付功能

刘悦的技术博客

Python 区块链 Vue 加密货币 虚拟货币

Starfish Os X MetaBell战略合作,元宇宙商业生态更进一步

股市老人

借助Elephant Swap打造的ePLATO,背后的高溢价解析

EOSdreamer111

语音聊天app——如何规范开发流程?

开源直播系统源码

软件开发 直播系统源码 语音聊天系统

新闻速递 | MobTech袤博科技参与中国信通院“绿色SDK产业生态共建行动”

MobTech袤博科技

数据安全 sdk

API 网关 APISIX 在Google Cloud T2A 和 T2D 的性能测试

API7.ai 技术团队

网关 API Gateway 谷歌云 网关性能测试

巧用ngx_lua做流量分组

转转技术团队

nginx

Prometheus 运维工具 Promtool (四)TSDB 功能

耳东@Erdong

Prometheus 7月月更 签约计划第三季 Promtool

C# 之 方法参数传递机制

陈言必行

7月月更

数据中台建设(三):数据中台架构介绍

Lansonli

数据中台 7月月更

Kotlin核心编程:val 和 var 的使用规则_移动_水滴技术团队_InfoQ精选文章