在这一章和下一章,我们将学习和练习 Kotlin 的核心基础。事实上,我们将探讨编程的主要原则。在本章中,我们将重点关注数据本身的创建和理解,在下一章中,我们将探索如何操纵和响应数据。
本章将集中讨论 Kotlin 中最简单的数据类型——变量。我们将在第 15 章 处理数据和生成随机数中重温更复杂和强大的数据类型。
当在我们继承的类(如Activity
和AppCompatActivity
)和我们自己编写的类(正如我们将在第 10 章 面向对象编程中开始做的那样)中工作时,我们将学习的核心 Kotlin 基础知识适用。
由于在编写自己的课程之前学习基础知识更符合逻辑,我们将学习基础知识,然后使用扩展的Activity
课程AppCompatActivity
,将这一新理论付诸实践。我们将再次使用Log
和Toast
来查看我们的编码结果。此外,我们将使用更多我们自己编写的函数(从按钮调用)以及Activity
类的覆盖函数来触发代码的执行。然而,我们将保存研究功能的全部细节,直到第 9 章, Kotlin 功能。
当我们进入第 10 章, 面向对象编程,并开始编写自己的类,以及了解别人编写的类是如何工作的时,我们在这里学到的一切在那时仍然适用。
到这一章结束时,您将可以轻松地编写在 Android 中创建和使用数据的 Kotlin 代码。本章将带您了解以下主题:
- 学习行话
- 了解更多关于代码注释的信息
- 什么是变量?
- 变量类型
- 声明变量的不同方式
- 初始化变量
- 运算符和表达式
- 表达自己演示应用
让我们从找出变量到底是什么开始。
在这本书里,我将用简单的英语解释一些技术概念。我不会要求你阅读之前没有用非技术语言解释过的 Kotlin 或 Android 概念的技术解释。
给刚接触 Kotlin 的 Java 程序员的一个注意事项:如果你做过一些 Java 编程,那么事情就要变得奇怪了!你甚至可以发誓我犯了一些错误;也许你甚至会认为我忘记在所有代码行的末尾添加分号了!我敦促你继续阅读,因为我认为你会发现 Kotlin 比 Java 有一些优势,因为它更简洁、更具表现力。学习 Java 仍然有它的位置,因为大多数安卓应用编程接口仍然是 Java,即使整个安卓社区立即放弃 Java(他们没有),在未来几年仍然会有遗留的 Java 代码。我不会继续指出 Java 和 Kotlin 之间的差异,因为有太多的差异,这样的分析是不必要的。如果你有兴趣,我推荐这篇文章:https://yalantis.com/blog/kotlin-vs-java-syntax/。最终,Kotlin 和 Java 编译成完全相同的 Dalvik 兼容的 Java 字节码。事实上,Java 和 Kotlin 是 100%可互操作的,甚至可以在一个项目中混合在一起。你甚至可以将 Java 代码粘贴到一个 Kotlin 项目中,它会立即转换成 Kotlin。
Kotlin 和安卓社区都是用技术术语说话的人;因此,要加入这些社区并从中学习,您需要了解他们使用的术语。
因此,这本书采取的方法是学习一个概念或使用完全简单的语言获得一个粗略的大纲,但同时引入行话或技术术语作为学习的一部分。
Kotlin 语法是我们将 Kotlin 的语言元素组合在一起以生成执行代码的方式。Kotlin 语法是我们使用的单词的组合,这些单词形成类似句子的结构,这就是我们的代码。
这些 Kotlin“单词”数量很多但是,如果把它们分成小块,它们肯定比任何人类语言都更容易学习。我们称这些词为关键词。
我相信,如果你能读简单的英语,那么你就能学习 Kotlin,因为学习 Kotlin 比学习读英语容易得多。那么,是什么把完成了初级 Kotlin 课程的人和专业程序员区分开了呢?
答案和区分语言学生和诗人大师完全一样。Kotlin 的专业知识不在于我们知道如何使用的 Kotlin 关键词的数量,而在于我们使用它们的方式。语言的掌握来自于练习、进一步的学习和更熟练地使用关键词。许多人认为编程既是一门科学,也是一门艺术,这是有一定道理的。
随着你越来越擅长编写 Kotlin 程序,你用来创建程序的解决方案将变得越来越长,越来越复杂。此外,正如我们将在后面的章节中看到的,Kotlin 旨在通过让我们将代码分成单独的类(通常跨多个文件)来管理复杂性。
代码注释是 Kotlin 文件的一部分,在程序执行本身没有任何作用;也就是说,编译器忽略了它们。它们的作用是帮助程序员记录、解释和澄清他们的代码,以便以后他们自己或其他可能需要使用或更改代码的程序员更容易理解。
我们已经看到一个单行评论:
// this is a comment explaining what is going on
前面的注释以两个正斜杠字符//
开始。注释在行尾结束。所以,那一行的任何内容都是仅供人使用的,而下一行的任何内容(除非是另一条注释)都需要语法正确的 Kotlin 代码:
// I can write anything I like here
but this line will cause an error
我们可以使用多个单行注释,如下所示:
// Below is an important note
// I am an important note
// We can have as many single line comments like this as we like
如果我们想暂时禁用一行代码,单行注释也很有用。我们可以把//
放在代码前面,它不会包含在程序中。回头参考这段代码,它告诉安卓加载我们的布局:
// setContentView(R.layout.activity_main)
在这种情况下,布局将不会被加载,并且应用在运行时会有一个空白屏幕,因为编译器会忽略整行代码。
当我们临时注释掉一个函数中的一行代码时,我们在第 5 章、用 CardView 和 ScrollView 看到了这一点。
Kotlin 中还有另一种类型的注释,称为多行注释。多行注释对于跨越多行的较长注释以及在代码文件顶部添加版权信息等内容非常有用。像单行注释一样,多行注释可以用来暂时禁用代码;在这种情况下,通常跨越多条线。
编译器会忽略前导/*
字符和结尾*/
字符之间的所有内容。看看下面的例子:
/*
You can tell I am good at this because my
code has so many helpful comments in it.
*/
多行注释的行数没有限制;最好使用的注释类型将取决于具体情况。在这本书里,我总是会明确地解释文本中的每一行代码,但是你经常会在代码本身中发现大量的注释,这些注释增加了进一步的解释、洞察力或上下文。因此,彻底阅读所有代码总是一个好主意:
/*
The winning lottery numbers for next Saturday are
9,7,12,34,29,22
But you still want to make Android apps?
*/
所有最好的程序员都会在他们的代码中随意添加注释!
我们可以把一个变量想象成一个命名的储物盒。我们选择一个名字,也许是variableA
。这些名字是程序员对用户安卓设备内存的访问。
变量是内存中的值,在必要时可以通过引用它们的名称来使用。
计算机内存有一个高度复杂的地址系统,幸运的是,我们不需要直接与之交互。Kotlin 变量允许我们为应用需要处理的所有数据设计自己方便的名称。操作系统将依次与物理(硬件)内存交互。
所以,我们可以把安卓设备的内存想象成一个等待我们添加变量的巨大仓库。当我们给变量命名时,它们被存储在仓库中,为我们需要它们做好准备。当我们使用变量的名称时,设备确切知道我们指的是什么。然后我们可以告诉它去做一些事情,比如下面这些:
- 给
variableA
赋值 - 将
variableA
添加到variableB
- 测试
variableB
的值,并根据结果采取行动
在一个典型的应用中,我们可能有一个名为unreadMessages
的变量;可能是为了保存用户拥有的未读消息的数量。我们可以在新消息到达时添加它,在用户阅读消息时从它那里拿走,并在应用布局的某个地方显示给用户,这样他们就知道他们有多少未读消息。
可能出现的情况包括:
- 用户获得三条新消息,因此在
unreadMessages
的值上增加三条。 - 用户登录应用,因此使用
Toast
显示一条消息以及存储在unreadMessages
中的值。 - 用户看到一些消息来自他们不喜欢的人,于是删除了两条消息。然后我们可以从
unreadMessages
中减去 2。
变量名是任意的,如果你没有使用 Kotlin 限制的任何字符或关键词,你可以随意调用你的变量。
然而,在实践中,最好采用命名约定,这样您的变量名就会一致。在本书中,我们将使用以小写字母开头的变量名的简单约定。当变量名称中有多个单词时,第二个单词将以大写字母开头。这叫骆驼肠衣。
以下是一些骆驼大小写变量名的例子:
unreadMessages
contactName
isFriend
在我们看一些使用一些变量的真实 Kotlin 代码之前,我们需要先看一下可以创建和使用的变量的类型。
不难想象,即使是一个简单的应用也会有相当多的变量。在前一节中,我们引入了unreadMessages
变量作为假设的例子。如果应用有联系人列表,需要记住每个联系人的名字怎么办?然后,我们可能需要每个联系人的变量。
而当一个应用需要知道一个联系人是否也是朋友,或者只是一个普通的联系人时呢?我们可能需要测试朋友状态的代码,然后将来自该联系人的消息添加到适当的文件夹中,以便用户知道它们是否是来自朋友的消息。
包括安卓应用在内的计算机程序的另一个常见要求是正确或错误的测试。计算机程序使用真和假来表示正确或错误的计算。
为了涵盖这些以及您可能想要存储或操作的许多其他类型的数据,Kotlin 使用不同类型的变量。
变量有很多种类型,我们甚至可以发明自己的类型。但是,现在,我们将看看最常用的 Kotlin 类型,这些类型将涵盖我们可能遇到的几乎所有情况。解释类型的最好方法是通过一些例子。
我们已经讨论了假设的unreadMessages
变量。这个变量当然是一个数字。
另一方面,假设的contactName
变量将保存组成联系人姓名的字符或字母。
保存常规数字的类型称为 Int (整数的缩写)类型,保存类似名字的数据的类型称为 String 。
以下是我们将在本书中使用的变量的类型列表:
Int
:Int
类型用于存储整数和整数。这种类型可以存储超过 20 亿的值,包括负值。Long
:顾名思义,Long
数据类型可以在需要更大的数字时使用。一个Long
变量最多可以存储 9,223,372,036,854,775,807 个数字。那是很多未读的信息。Long
变量有很多用途,但是如果一个更小的变量就可以了,我们应该使用它,因为我们的应用会使用更少的内存。Float
:此变量用于浮点数。也就是精度超过小数点的数字。由于数字的小数部分与整数部分一样占用内存空间,因此与非浮点数相比,一个数字在Float
变量中的可能范围缩小了。因此,除非我们的变量将使用额外的精度,Float
将不是我们选择的数据类型。Double
:当Float
变量中的精度不够时,我们有Double
。- 我们将在整本书中使用大量的布尔语。
Boolean
变量类型可以是true
或false
;没别的了。布尔人回答问题,例如:- 联系人是朋友吗?
- 有新消息吗?
- 布尔人的两个例子够了吗?
Char
:存储单个字母数字字符。它本身不会改变世界,但如果我们把它们放在一起,它可能会很有用。String
:字符串可以用来存储任意键盘字符。它类似于Char
变量,但几乎是任意长度。从联系人的名字到整本书,任何东西都可以存储在一个单独的String
中。我们将定期使用字符串,包括在本章中。Class
:这是最强大的数据类型,我们已经稍微讨论过了。我们将深入学习第 10 章、面向对象编程中的课程。Array
:这种类型有很多不同的风格,是处理和组织大量数据的关键。我们将探索第十五章【数据处理和生成随机数】中Array
的变化。
现在我们知道了变量是什么,并且有很多种类型可供选择,我们几乎可以看到一些实际的 Kotlin 代码了。
在我们可以使用刚才讨论的变量类型之前,我们必须声明它们,这样编译器就知道它们的存在,我们还必须初始化它们,这样它们就持有一个值。
对于 Kotlin 中的每个变量类型,如Int
、Float
和String
,有两个关键词可以用来声明它们:val
和var
。
val
类型用于存储由程序员在应用启动前或初始化期间决定的值,并且在执行期间不能再次更改。var
类型是指在整个执行过程中可以操作和更改的值。
因此,val
类型只有可读。用专业术语来说,它被称为不变。var
类型可读可写,这叫做可变。编写在执行过程中试图更改val
类型的值的代码将导致 AndroidStudio 显示错误,并且代码不会编译。var
也有规则,我们稍后会探讨。
有两种方法可以声明和初始化一个String
类型;首先,通过使用val
,如下:
val contactName: String = "Gordon Freeman"
在前一行代码中,声明了一个名为contactName
且类型为String
的新val
变量,该变量现在持有Gordon Freeman
值。
此外,Gordon Freeman
文本现在是contactName
在应用执行期间唯一可以保存的值。您可以尝试用下面一行代码来更改它:
contactName = "Apple Crumble" // Causes an error
如果您将前面的代码粘贴到安卓项目中的onCreate
函数中,您将会看到以下内容:
AndroidStudio 正在帮助我们执行我们的决定,让变量保持不变。当然,我们经常需要改变变量的值。当我们这样做的时候,我们会用var
来代替;看看下面两行代码:
var contactName: String = "Gordon Freeman"
contactName = "Alyx Vance" // No problem
在前面的代码中,我们使用var
来声明一个String
类型,这次我们成功地将contactName
持有的值更改为Alyx Vance
。
这里带走的重点是,如果变量在 app 执行过程中不需要改变,那么我们应该使用val
,因为编译器可以帮助保护我们不出错。
让我们声明并初始化一些不同类型的变量:
val battleOfHastings: Int = 1066
val pi: Float = 3.14f
var worldRecord100m: Float = 9.63f
var millisecondsSince1970: Long = 1544693462311
// True at 9:30am 13/12/2018
val beerIsTasty: Boolean = true
var isItRaining: Boolean = false
val appName: String = "Express Yourself"
var contactName: String = "Geralt"
// All the var variables can be reassigned
worldRecord100m = 9.58f
millisecondsSince1970 = 1544694713072
// True at 9:51am 13/12/2018
contactName = "Vesemir"
请注意,在前面的代码中,当变量不太可能改变时,我将变量声明为val
,当变量很可能改变时,我将变量声明为var
。开发应用时,您可以猜测是使用还是使用val
或var
,如果需要,您可以将var
变量更改为val
变量,或者反过来。此外,在前面的代码中,请注意String
类型是用语音标记之间的值初始化的,但Int
、Float
、Long
和Boolean
不是。
Kotlin 被设计得尽可能简洁。让开发人员用尽可能少的代码完成尽可能多的工作,是捷脑团队的目标之一。我们将在整个 Kotlin 语言中看到这样的例子。如果您以前用另一种语言编写过代码,尤其是 Java,您会注意到打字量显著减少。这方面的第一个例子是型推断。
Kotlin 经常可以从上下文中推断出你需要的类型,如果是这种情况,那么就不需要显式写类型;考虑以下示例:
var contactName: String = "Xian Mei"
在前面的代码中,一个名为contactName
的String
类型是使用“鲜美”声明和初始化的。想一想,一定是String
。幸运的是,这对于 Kotlin 编译器来说也是显而易见的。我们可以(也应该)使用类型推断来改进前面的代码行,比如下面这段代码:
var contactName = "Xian Mei"
省略了冒号和类型,但结果是相同的。
Java 程序员还会注意到,Kotlin 代码不需要在每行的末尾都有一个分号。但是,如果您喜欢分号,那么如果您在每一行的末尾添加一个分号,编译器就不会抱怨:
var contactName = "Xian Mei"; // OK but superfluous
然而,我们必须记住,虽然我们没有明确指定String
,但它仍然是一个String
类型——并且只是一个String
类型。如果我们试图做一些不适合String
类型的事情,那么我们会得到一个错误;例如,当我们尝试将其重新初始化为一个数值时,如下面的代码所示:
contactName = 3.14f // Error
前面的代码将在 AndroidStudio 被标记,编译将不起作用。以下是前一段代码中的所有声明和初始化,但这次使用了类型推断:
val battleOfHastings = 1066
val pi = 3.14f
var worldRecord100m = 9.63f
var millisecondsSince1970 = 1544693462311
// True at 9:30am 13/12/2018
val beerIsTasty = true
var isItRaining = false
val appName = "Express Yourself"
var contactName = "Geralt"
在接下来的两节中,我们将看到更多关于变量的类型推断,在后面的章节中,我们将对更复杂的类型(如类、数组和集合)使用类型推断。通过使我们的代码更短、更易管理,类型推断也将成为一个很好的省时工具。
这听起来可能很明显,但是值得一提的是,如果您稍后声明一个变量进行初始化,那么类型推断是不可能的,如下面的代码所示:
var widgetCount // Error
前一行代码将导致错误,应用将无法编译。
使用类型推断时,通常很明显变量是什么类型,但是,如果有任何疑问,您可以在 AndroidStudio 中选择一个变量,并同时按下Shift+Ctrl+P,以获得方便的屏幕提示:
省略偶尔的String
、Int
或冒号(:
)类型本身不会有太大的变化,所以让我们学习如何通过将运算符与我们的变量组合来制作表达式。
当然,在几乎任何程序中,我们都需要用这些变量的值“做事”。我们可以用操作符操纵和改变变量。当我们将运算符和变量组合成一个结果时,它被称为表达式。
以下部分列出了允许我们操作变量的最常见的 Kotlin 运算符。您不需要记住它们,因为我们将在第一次使用它们时查看每一行代码。
在上一节初始化变量时,我们已经看到了第一个操作符,但是我们将再次看到它有点冒险。
这是赋值运算符:
=
它使运算符左边的变量与右边的值相同;例如,在这一行代码中:
unreadMessages = newMessages
前一行代码执行后,unreadMessages
中存储的值将与newMessages
中存储的值相同。
这是加法运算符:
+
它会将操作员任一侧的值相加。它通常与赋值运算符一起使用。例如,它可以将两个具有数值的变量相加,如下一行代码所示:
unreadMessages = newMessages + unreadMessages
一旦执行了前面的代码,由newMessages
和unreadMessages
保存的值的总和将存储在unreadMessages
中。作为同样事情的另一个例子,看一下这一行代码:
accountBalance = yesterdaysBalance + todaysDeposits
请注意,在运算符的两端同时使用同一个变量是完全可以接受的(也是非常常见的)。
这是减法运算符:
-
它将从左边的值中减去操作员右边的值。这通常与赋值运算符结合使用,如本例所示:
unreadMessages = unreadMessages - 1
减法运算符的另一个示例如下:
accountBalance = accountBalance - withdrawals
前一行代码执行后,accountBalance
将保持其初始值减去withdrawals
中的值。
这是除法运算符:
/
它会将左边的数字除以右边的数字。同样,它是通常与赋值运算符一起使用;下面是一行示例代码:
fairShare = numSweets / numChildren
如果在前一行代码中,numSweets
持有九个,numChildren
持有三个,那么fairShare
现在持有三个的值。
这是乘法运算符:
*
它将变量和数字相乘,和许多其他运算符一样,通常与赋值运算符一起使用;对于的例子,看看这一行代码:
answer = 10 * 10
乘法运算符的另一个示例如下:
biggerAnswer = 10 * 10 * 10
前两行代码执行完毕后,answer
保存值 100,biggerAnswer
保存值 1000。
这是增量运算符:
++
增量运算符是一种将一个值加到另一个值上的快速方法。例如,看下一行代码,它使用加法运算符:
myVariable = myVariable + 1
前一行代码的结果与这一更紧凑的代码相同:
myVariable ++
这是递减运算符:
--
减量运算符(正如你可能已经猜到的那样)是从某物中减去 1 的快速方法。例如,看下一行代码,它使用减法运算符:
myVariable = myVariable -1
上一行代码与myVariable --.
相同
现在我们可以把这些新知识放入一个有效的应用中。
让我们尝试使用一些声明、赋值和运算符。当我们将这些元素捆绑成一些有意义的语法时,我们称之为表达式。让我们编写一个快速应用来尝试一些。然后我们将使用Toast
和Log
来检查我们的结果。
创建一个名为Express Yourself
的新项目,使用一个空活动项目模板,并将所有其他选项保留在它们通常的设置中。我们将在这个项目中编写的完整代码可以在下载包的Chapter07
文件夹中找到。
切换到编辑器中的主活动选项卡,我们将编写一些代码。在onCreate
函数中,就在右花括号(}
)之前,添加以下突出显示的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val name = "Nolan Bushnell"
val yearOfBirth = 1943
var currentYear = 2019
var age: Int
}
}
我们刚刚给onCreate
函数增加了四个变量。前两个是val
不可更改的变量。他们是持有人名的String
型和持有出生年份的Int
型。代码中没有明确提到类型;它们是推断出来的。
接下来的两个变量是var
变量。我们有一个代表当前年份的Int
类型,和一个未初始化的Int
类型来代表一个人的年龄。由于age
变量未初始化,无法推断其类型,所以必须指定。
在前面的代码之后,仍然在onCreate
里面,添加以下几行:
age = currentYear - yearOfBirth
Log.i("info", "$age")
运行该应用,并在 logcat 窗口中注意到以下输出:
info: 76
在Log.i…
代码的语音标记中使用$
符号向编译器表明,我们想要输出存储在age
变量中的值,而不是字面上的单词“age”。
实际值本身(76),表示存储在yearOfBirth
(1943)中的值从存储在currentYear
(2019)中的值中减去,结果用于初始化age
变量。如您所见,我们可以在语音标记中包含尽可能多的 $
符号,并将它们与文本甚至 Kotlin 表达式混合在一起。该功能被称为字符串模板。让我们尝试另一个字符串模板。
在onCreate
功能内的前一个代码后添加这两行代码:
currentYear++
Log.i("info", "$name
was born in $yearOfBirth and is $age years old.
Next year he will be ${currentYear - yearOfBirth} years old)")
关于代码首先要解释的是,虽然在本书中它被格式化为四行,但是当你将它输入 Android Studio 时,它必须被输入为两行。第一行currentYear++
,增加(加一)存储在currentYear
中的值。代码的其余部分都是一行。
运行应用,并在 logcat 窗口中观察以下输出:
Nolan Bushnell was born in 1943 and is 76 years old. Next year he will be 77 years old
代码工作是因为 Kotlin 字符串模板的。让我们分解这段相当长的代码。首先,我们调用Log.i
函数,就像我们以前多次做的那样。在第一个字符串中,我们传递"info"
,在第二个字符串中,我们传递一组变量名称,前面是与一些文字混合的$
符号。分解中最有趣的部分是倒数第二部分,因为我们使用一个表达式来构成字符串的一部分:
$name
打印诺兰·布什内尔Was born in
是字面意思- 【1943 年版画
- 接下来是文字文本
and is
$currentAge
印花 76- 接下来,跟随
years old
的文字 - 接下来是文字文本
Next year he will be
${currentYear - yearOfBirth}
是表达式,并且打印表达式(77)的结果- 打印最后的文字文本
years old
,结束输出
这表明我们可以使用以下形式在String
类型中包含任何有效的 Kotlin 表达式:
${expression}
我们将在下一章看到更复杂和强大的表达。
在本章中,我们已经学习了 Kotlin 中数据的基本构造块。我们已经探索了不同的类型,并概述了它们的不同用途。我们还学习了如何使用字符串模板从文字值、变量和表达式构建字符串。我们还看到了我们如何能够并且应该使用类型推断来尽可能地使我们的代码更加简洁。
我们没有看到太多布尔变量类型,但是我们将在下一章学习 Kotlin 决策和循环时纠正这个错误。