第 2 章 变量、常量和类型

第 2 章 变量、常量和类型

本章,你将学习程序的基本组成元素:变量、常量以及Kotlin基本数据类型。变量常量在应用程序中可用来储值和传递数据。类型则用来描述常量或变量中保存的是什么样的数据。

2.1 数据类型

变量和常量都有其指定的数据类型。类型描述常量或变量中保存的数据,告诉编译器该如何处理类型检查(type checking)。这种检查是Kotlin语言的一个特色,能避免变量和常量赋值出现类型不匹配错误。

理论需要结合实践,下面我们就在第1章创建的Sandbox项目中添加一个文件。首先启动IntelliJ。既然IntelliJ默认会打开最近使用的项目,Sandbox项目应该会自动打开。如果没有,请在欢迎界面左边的最近使用的文件列表中找到并打开它,或者使用File → Open Recent → Sandbox菜单项打开。

在项目工具窗口中右击src文件夹,添加一个新的项目文件。(没看到src文件夹的话,可能需要展开Sandbox项目文件夹。)选择New → Kotlin File/Class菜单项,输入文件名TypeIntro后确定,新文件随即会在编辑器中打开。

第1章已说过,main函数是应用程序的入口点。使用IntelliJ编写这个函数有捷径:在TypeIntro.kt文件中,输入单词main,然后按Tab键。IntelliJ会自动为你添加该函数的基本结构,如代码清单2-1所示。

代码清单2-1 添加一个main函数(TypeIntro.kt)

fun main(args: Array<String>) {

}

2.2 声明变量

假设你正在开发一个冒险游戏。游戏中,玩家可以探索一个互动世界。自然,你需要一个变量来保存玩家的经验值。

在TypeIntro.kt文件中,创建一个名为experiencePoints的变量并赋值。

代码清单2-2 声明一个名为experiencePoints的变量(TypeIntro.kt)

fun main(args: Array<String>) {
    var experiencePoints: Int = 5
    println(experiencePoints)
}

在上述代码中,你将一个Int类型的值赋给了experiencePoints变量。新增代码都起什么作用?我们来一探究竟。

首先,使用var关键字定义一个变量。var后面跟着一个变量名,表示你想定义一个新变量。

其次,使用: Int指定变量的类型。: Int表明,experiencePoints变量要存储的是整数。

最后,使用赋值运算符=)把右边的值(Int类型的实例值5)赋值给左边的experiencePoints变量。

图2-1是experiencePoints变量定义的图解。

图2-1 变量定义图解

定义变量并赋值后,接下来使用println函数将变量值输出到控制台。

单击main函数旁的运行按钮,选择Run ‘TypeIntroKt’ 运行程序。可以看到,输出到控制台的experiencePoints变量值是5。

现在,尝试给experiencePoints变量赋值"thirty-two"。(删除线表明代码需删除。)

代码清单2-3 给 experiencePoints变量赋值"thirty-two"(TypeIntro.kt)

fun main(args: Array<String>) {
    var experiencePoints: Int = 5
    var experiencePoints: Int = "thirty-two"
    println(experiencePoints)
}

单击运行按钮再次运行main函数。这次,Kotlin编译器报错了:

Error:(2, 33) Kotlin: Type mismatch: inferred type is String but Int was expected

你可能已注意到,输入"thirty-two"时,它下面会出现红色波浪线。这是IntelliJ在警示代码有错。如图2-2所示,把光标悬停其上可以看到问题的具体描述。

图2-2 类型不匹配

Kotlin使用的是静态类型系统(static type system)。这表明,编译器会按类型标识代码定义,以确保编码有效。IntelliJ也会在代码输入时就开始检测,一旦发现变量类型和实例值类型不匹配,就立即指出。IntelliJ的这个特色功能叫静态类型检查,能在代码编译前发现代码问题。

如代码清单2-4所示,为改正类型错误,需将experiencePoints变量值从"thirty-two"改回整数值5。

代码清单2-4 改正类型错误(TypeIntro.kt)

fun main(args: Array<String>) {
    var experiencePoints: Int = "thirty-two"
    var experiencePoints: Int = 5
    println(experiencePoints)
}

即使已有初始值,也能给变量重新赋值。例如,在游戏中,如果玩家获得了更多经验值,就可以给experiencePoints变量赋新值。如代码清单2-5所示,给experiencePoints变量值加5。

代码清单2-5 experiencePoints变量值加5(TypeIntro.kt)

fun main(args: Array<String>) {
    var experiencePoints: Int = 5
    experiencePoints + = 5
    println(experiencePoints)
}

如上述代码所示,experiencePoints变量一开始赋值5,然后使用+=运算符在原基础上再加5。再次运行代码,可以看到,控制台输出了数值10。

2.3 Kotlin的内置数据类型

前面你已接触过StringInt类型的变量。Kotlin还有其他一些数据类型,可以接纳诸如true/false值、元素集合、键值对集合元素这样的数据。表2-1列出了Kotlin支持的一些常用数据类型。

表2-1 常用内置类型

类型

描述

示例

String

字符串

"Estragon"
"happy meal"

char

单字符

'X'
Unicode character U+0041

Boolean

true/false值

true
false

Int

整数

"Estragon".length
5

Double

小数

3.14
2.718

List

元素集合

3, 1, 2, 4, 3
"root beer", "club soda", "coke"

Set

无重复元素的集合

"Larry", "Moe", "Curly"
"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"

Map

键值对集合

"small" to 5.99, "medium" to 7.99, "large" to 10.99

对这些数据类型不都熟悉的话,也不用担心,后面会陆续学到。第7章会介绍String类型,第8章会介绍数值类型,第10章和第11章会介绍合起来可称为集合类型ListSetMap

2.4 只读变量

前面你已看到,变量可以重新赋值。但通常我们也需要使用不能改变值的变量。例如,在冒险游戏中,玩家名一旦设定就不能再改。

Kotlin提供了一种语法来声明只读变量,也就是说,变量一旦赋值就不能更改。

要声明可修改变量,使用var关键字。要声明只读变量,使用val关键字。

日常口头交流时,可修改变量统称var,只读变量统称val。既然变量(variable)和只读变量(read-only variable)区分不明显,后续我们就沿用varval这种叫法。varval都被视为变量,所以我们继续使用“变量”来表示两组不同用处的变量。

如代码清单2-6所示,为游戏玩家名添加只读变量定义,紧随经验值后打印出来。

代码清单2-6 添加名为playerName的只读变量(TypeIntro.kt)

fun main(args: Array<String>) {
    val playerName: String = "Estragon"
    var experiencePoints: Int = 5
    experiencePoints + = 5
    println(experiencePoints)
    println(playerName)
}

单击main函数旁边的运行按钮并选择Run ‘TypeIntroKt’ 运行程序。可以看到控制台打印出了经验值和玩家名。

10
Estragon

如代码清单2-7所示,使用=赋值运算符,给playerName变量重新赋值,然后再次运行程序。

代码清单2-7 尝试修改playerName只读变量的值(TypeIntro.kt)

fun main(args: Array<String>) {
    val playerName: String = "Estragon"
    playerName = "Madrigal"
    var experiencePoints: Int = 5
    experiencePoints + = 5
    println(experiencePoints)
    println(playerName)
}

可以看到,编译器报错如下:

Error:(3, 5) Kotlin: Val cannot be reassigned

你试图修改只读变量的值,编译器当然要报错。记住,只读变量一旦赋值就不能再改。

如代码清单2-8所示,删除只读变量再赋值语句以修正问题。

代码清单2-8 修正只读变量再赋值问题(TypeIntro.kt)

fun main(args: Array<String>) {
    val playerName: String = "Estragon"
    playerName = "Madrigal"
    var experiencePoints: Int = 5
    experiencePoints + = 5
    println(experiencePoints)
    println(playerName)
}

既然只读变量能防止不应该更改的变量被意外改动,任何时候,只要有需要,都推荐区分使用可变变量和只读变量。

通过静态代码分析,IntelliJ能判定可变变量和只读变量是否使用恰当。如果一个不可变变量用了var关键字,IntelliJ会建议你改用val关键字。一般来说,我们推荐采纳IntelliJ的建议,除非你就是要给变量重新赋值。如果想看看IntelliJ是如何分析建议的,可把playerName定义为可变变量。

代码清单2-9 把playerName定义为可变变量(TypeIntro.kt)

fun main(args: Array<String>) {
    val playerName: String = "Estragon"
    var playerName: String = "Estragon"
    var experiencePoints: Int = 5
    experiencePoints + = 5
    println(experiencePoints)
    println(playerName)
}

playerName变量值不应再赋值,所以不需要也不应该使用var关键字定义。可以看到,IntelliJ已高亮了var关键字,如图2-3所示。将光标悬停其上,会看到IntelliJ给出的修改建议。

图2-3 变量不应被修改

和预期一样,IntelliJ建议以val关键字定义playerName变量。要采纳其建议,单击var关键字,然后按Option-Return(Alt-Enter)组合键。如图2-4所示,选择弹出菜单中的Make variable immutable选项。

图2-4 定义不可变变量

如图2-5所示,IntelliJ自动把var关键字改成了val

图2-5 不可变的playerName变量

前面说过,任何时候,只要有必要,都建议使用val关键字。这样,IntelliJ就能在你不小心重新赋值的时候及时提醒。建议多多留意IntelliJ的代码改进建议,虽然不一定会用到,但多看看总没坏处。

2.5 类型推断

注意观察,你会发现playerNameexperiencePoints变量的类型定义是灰色的。在IntelliJ中,某个元素呈灰色即表明该元素是多余的。如图2-6所示,将光标悬停在String类型定义上,IntelliJ会给出解释。

图2-6 冗余类型信息

IntelliJ所说的类型定义多余到底是什么意思呢?原来,Kotlin有个语言特性叫类型推断,对于已声明并赋值的变量,它允许你省略类型定义。既然String类型的playerName变量和Int类型的experiencePoints变量已赋值,Kotlin编译器就能据此推断出它们的数据类型。

类似于IntelliJ能把可变变量(var)改为只读变量(val),IntelliJ也能自动帮你删除冗余的类型定义。单击playerName变量旁边的String类型定义(: String),然后按Option-Return (Alt-Enter)组合键。如图2-7所示,选择弹出菜单的Remove explicit type specification选项。

图2-7 删除显式类型定义

可以看到,String定义不见了。执行同样的操作,也删掉experiencePoints变量的: Int的定义。

声明变量时,依赖类型推断也好,明确指定变量类型也好,编译器都会帮你记录类型定义。除非因为有歧义而必须手动管理,否则本书都会依靠类型推断来处理类型定义。这样做有助于编写简洁、易维护的代码。

注意,IntelliJ会应要求显示任何变量的类型,包括那些使用类型推断的变量。如果你对一个变量的类型有疑问,可以单击变量名,并按Control-Shift-P组合键。IntelliJ会显示出变量的类型(见图2-8)。

图2-8 查看类型信息

2.6 编译时常量

前面说过,可变变量可以重新赋值,而只读变量一旦赋值就无法更改。凡事无绝对,这是我们善意的谎言。事实上,你会在第12章看到,只读变量也有返回不同值的特例。真有数据要保证绝对只读的话,考虑使用编译时常量吧。

编译时常量只能在函数(指包括main在内的所有函数)之外定义。这是因为,编译时常量必须在编译时(程序编译时)赋值,而main和其他函数都是在运行时(程序运行时)才调用,函数内的变量也是在那时赋值。编译时常量要在这些变量赋值前就已存在。

因为使用复杂的数据类型可能会危害编译时的安全保障,所以编译时常量只能是一些常见的基本数据类型。第13章会介绍数据类型构建的相关知识。以下是编译时常量支持的基本数据类型:

  • String
  • Int
  • Double
  • Float
  • Long
  • Short
  • Byte
  • Char
  • Boolean

在TypeIntro.kt文件中,如代码清单2-10所示,在main函数之上,使用const修饰符定义一个编译时常量。

代码清单2-10 定义一个编译时常量(TypeIntro.kt)

const val MAX_EXPERIENCE: Int = 5000

fun main(args: Array<String>) {
    ...
}

const修饰符一起使用的val告诉编译器,MAX_EXPERIENCE常量值绝对不会改变。这也就是说,无论如何,整数值5000要绝对保证不变。据此,编译器就知道该如何灵活处理代码优化。

对常量名MAX_EXPERIENCE的书写格式感到好奇吗?编译器虽然没强制要求,但为突显编译时常量定义,我们倾向于使用这种字母全部大写、以下划线代替空格的格式。对于其他各种变量命名,你应该注意到了,我们依然遵循首字母小写的驼峰格式,以保证代码整洁易读。

2.7 查看Kotlin字节码

从第1章我们知道,Kotlin是Java之外的一种可选的开发语言,支持在执行Java字节码的JVM上运行。如果能查看Kotlin编译器生成的Java字节码,会对开发大有帮助。譬如,为分析Kotlin语言的某些功能在JVM上如何运作,本书中有好几个地方会让你查看Java字节码。

懂Java的话,查看Kotlin代码的Java翻译版本将有助于深入理解Kotlin语言。不懂也没关系,可以看看Java翻译版代码和Kotlin代码有没有相似之处,就当作伪代码帮助学习Kotlin好了。什么编程语言都没学过?恭喜你选了Kotlin!稍后你会看到,相比Java,同样的逻辑用Kotlin表达起来更简洁。

举个例子,你可能会好奇,定义变量时使用Kotlin类型推断,会对生成的JVM字节码有何影响。这很好办,使用Kotlin字节码工具吧。

在TypeIntro.kt文件中,连按Shift键两次,打开Search Everywhere对话框。如图2-9所示,在搜索框中输入show kotlin,然后选择结果列表中的Show Kotlin Bytecode选项。

图2-9 查看Kotlin字节码

如图2-10所示,Kotlin字节码工具窗口会出现。(也可以经由Tools → Kotlin → Show Kotlin Bytecode菜单项打开字节码工具窗口。)

图2-10 Kotlin字节码工具窗口

不懂字节码这门“外语”?不用怕!在字节码工具窗口,单击左上角的Decompile按钮,就可以把字节码转译为你可能相对熟悉的Java语言。

如图2-11所示,TypeIntro.decompiled.java文件,即Kotlin字节码的Java版本,会在一个新标签页中打开。

图2-11 字节码的Java翻译版

(图中的红色波浪线表明,Kotlin与Java交互过程中出现了偶发小问题,代码本身没有问题。)

注意观察experiencePoints变量和playerName变量的定义:

String playerName = "Estragon";
int experiencePoints = 5;

在Kotlin源码中,这两个变量的类型定义已省略,但在字节码中,还是看到了它们的显式类型定义。由此可见,字节码讲述了背后的故事,即Kotlin支持类型推断,所以不像Java那样,需要显式的变量类型定义。

关于字节码的学习到此为止,后面的章节还会深入研究Java字节码。现在,可以关闭TypeIntro.decompiled.java文件(使用标签页中的X)和字节码工具窗口(使用右上角的 {%})了。

本章,你已学会使用可更改变量和只读变量存储基本数据,知道何时该用哪种变量,这主要取决于变量值是否能修改;了解了如何使用编译时常量定义不可变常量值;最后学会了使用Kotlin功能强大的类型推断,在声明变量时简化代码输入。后续学习时,你会反复运用这些知识和工具。

下一章,我们将学习条件语句,看看如何利用它们编写出复杂的代码。

2.8 深入学习:Kotlin中的Java基本数据类型

Java有两类数据类型:引用类型与基本类型。引用类型有对应的源代码定义。基本类型不需要源码定义,由特殊关键字代表。

Java引用类型名总是由大写字母开头,表明该类型有对应的源代码定义。下面是使用引用类型定义的experiencePoints变量:

Integer experiencePoints = 5;

作为对照,Java基本数据类型名以小写字母开头:

int experiencePoints = 5;

所有基本数据类型都有相对应的引用类型(但不是所有引用类型都有相对应的基本类型)。那该如何判断它们各自的使用场景呢?

需要选用引用类型的一大原因是,某些Java语言特色功能只有引用类型才支持。例如,第17章要学习的泛型(generic)就不支持基本数据类型。此外,引用类型比基本类型更便于支持Java某些面向对象的特色功能。(第12章将介绍Kotlin面向对象编程及其面向对象的特色功能。)

当然,基本数据类型也有其优点,而且性能表现比引用类型好。

和Java不同,Kotlin只提供引用类型这一种数据类型。

var experiencePoints: Int = 5

Kotlin这样设计基于几大理由。首先,只有一种数据类型可选,你就不容易因选项多而选错,进而陷入编码困境。例如,定义了一个基本数据类型实例后,写着写着,猛然发现要用到只有引用类型才支持的泛型功能,怎么办?Kotlin通过只提供一种类型规避了此问题。

也许熟悉Java的你会说:“但是基本数据类型的性能要好于引用类型啊!”没错,但我们再来看看前面字节码中experiencePoints变量的如下定义:

int experiencePoints = 5;

看到没有,基本数据类型又用回来了。为什么会这样?Kotlin不是只有引用数据类型吗?原来,只要有可能,出于更高性能的需要,Kotlin编译器会在Java字节码中改用基本数据类型。

为了让你愉快地使用引用类型,Kotlin在后台改用了性能更佳的基本数据类型。假如你熟悉Java的八大基本数据类型,也能在Kotlin中分别找到它们的对应引用数据类型。

2.9 挑战练习:定义hasSteed变量

在冒险游戏中,玩家可能会拥有巨龙或人身牛头怪坐骑。定义一个名为hasSteed的变量做记录。给变量赋初值,以表明玩家现在还没坐骑。

2.10 挑战练习:独角兽之角

想象这样一幕冒险游戏场景。

英雄Estragon走进一家叫作“独角兽之角”的小酒馆。酒馆老板问道:“需要牵马入厩吗?”

“谢谢,”Estragon回答说,“我还没马呢,不过我有50个金币,给我来杯喝的吧。”

“好极了!”酒馆老板说,“蜂蜜酒、葡萄酒,还有LaCroix气泡水,您要哪种?”

请在hasSteed变量之下,添加小酒馆场景需要的变量:酒馆名、酒馆老板、玩家金币数。定义时,尽可能使用类型推断。

对了,还有酒馆的酒水单,请思考该选用哪种数据类型来定义它。如有必要,参考表2-1。

2.11 挑战练习:魔镜

不要松懈,还有个难题。英雄Estragon已准备好了,你呢?

英雄Estragon发现了一面魔镜,可以照出玩家名字的映像。使用String数据类型,模拟照镜子,把“Estragon”变为“nogartsE”。

解决这个难题需要查看String文档页:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html。小提示:某个类型有哪些动作事件,通常看看名字就能猜到了。

目录

  • 版权声明
  • 献词
  • 致谢
  • 前言
  • 第 1 章 Kotlin应用开发初体验
  • 第 2 章 变量、常量和类型
  • 第 3 章 条件语句
  • 第 4 章 函数
  • 第 5 章 匿名函数与函数类型
  • 第 6 章 null安全与异常
  • 第 7 章 字符串
  • 第 8 章 数
  • 第 9 章 标准库函数
  • 第 10 章 List与Set
  • 第 11 章 Map
  • 第 12 章 定义类
  • 第 13 章 初始化
  • 第 14 章 继承
  • 第 15 章 对象
  • 第 16 章 接口与抽象类
  • 第 17 章 泛型
  • 第 18 章 扩展
  • 第 19 章 函数式编程基础
  • 第 20 章 Kotlin与Java互操作
  • 第 21 章 用Kotlin开发首个Android应用
  • 第 22 章 Kotlin协程简介
  • 第 23 章 编后语
  • 附录 A 补充挑战练习
  • 术语表