KT-1 搭上 Kotlin 号出发(上)
在你的第一个插件里,我们只是简单复制粘贴了一些代码,这对于快速理解插件的开发过程是很有用的,但若不能掌握 Kotlin 语言本身,只通过复制粘贴来拼凑源代码的话,也不可能做出什么更有价值的东西了。所以,在接下来的几个小节里,我们会讲解 Kotlin 语言的一些基本知识。这不会涵盖 Kotlin 的方方面面,甚至连冰山一角也算不上,但这些知识将足以支持我们完成第二个插件的编写。
我们从一个数字开始
1
这是数字 1,小巧可爱。1 就是 1,它能有什么坏心思呢?只要看到代码 1,你就知道它是数字 1,更确切说,是整数(Integer) 1。像这样只看字面就知道含义的东西,叫做字面量(Literal)。
除了整数,还有一些其它的字面量:
0.3 // 小数(Float)
"ciallo, world" // 字符串(String)
true // 逻辑值(Boolean)
null // 空值(Null)
……以及其它一些不太常用的字面量。
小数,也称浮点数,和数学中的小数是同样的概念。字符串,其实就是文字,只是软件工程师们喜欢用与众不同的词汇。
在 Kotlin 中,字符串使用双引号包裹。需要注意,双引号用来标记字符串,但它们本身并不是字符串的一部分。字符串字面量 "ciallo, world" 所包含的文字只有 ciallo, world(13 个字符)。
逻辑值(音译作布尔值)用来表示对错,true 表示对(是),false 表示错(否)。
你是职业选手吗?
在 Kotlin 中,true 不是 1,false 也不是 0,这和 C/C++ 中的逻辑值不同。除逻辑值以外的类型不能用在需要逻辑值的地方(如 if 的条件等)。
至于空值 null,它是整个 Java 世界中最可怕的东西。诗云『十有九人堪白眼,百无一用空指针』,人道是『但愿程序无崩溃,宁可架上 null 生尘』,又号作『此 null 只应天上有,人间能得几回闻』,故称『天长地久有时尽,此 null 绵绵无绝期』。
为了不至于让可爱的读者被 NullPointerException 困扰终生,大部分的 Java 教材都会建议谨慎地使用 null,或者干脆更强硬地说『不要使用 null!』 ,这对于 Java 来说都是正确的。不过,对于 Kotlin 而言,null 是编写高效代码的重要工具,这将在后面提到。
连词成句
字面量可以使用各种运算符(Operator) 进行连接:
"ciallo, " + "world" // "ciallo, world"
0.1 + 0.2 // 0.30000000000000004
42 + 14 > 100 // false
"高草丛" == "高*丛" // false
!false // true
这些由运算符连接的『算式』就被称作表达式(Expression),表达式运算的『结果』被称为表达式的值(Value),这些都是简单的数学概念,不过是换了个名字而已。下面列出一些常用的运算符:
+(对数字)、-、*、/:数字四则运算,运算顺序和数学中相同,不过由于键盘上没有×和÷,因此使用*和/代替。+(对字符串):字符串拼接,将两个字符串的内容拼在一起。>、<、>=、<=:数字比较运算,含义和数学中相同。==:判断左右的内容是否相等,以此决定表达式自身的值。相等为true,不相等为false。
相等运算符 == 使用两个等号 = 而不是一个。尽管 Kotlin 有防范误用单个等号的机制,但你仍然应该特别提防此问题。即使是非常有经验的工程师也会在这种地方犯错。
你是职业选手吗?
在 Kotlin 中,对象的相等性比较可以直接使用 == 来完成。Kotlin 中的 == 相当于 Java 中的 equals,而 Java 中的 ==(引用比较)在 Kotlin 中是 ===(3 个 =)。
!=:判断左右的内容是否不等,和==的含义刚好相反。!:否定其后跟随的逻辑值,把true变为false,反之亦然。
至于 0.1 + 0.2 的值是 0.30000000000000004 的原因已经超出了本书的讨论范畴,如果你作为严谨的数学家感到万万不能容忍,可以去查找一下关于舍入误差的一些资料。
有趣的事情是,这些运算符所连接的是值而不是字面量,注意到这里的细微差别了吗?表达式可以算出来一个值,也就是说,你可以用运算符连接两个表达式:
1 + 2 < 3 + 4 // true
5 * (1 + 4) // 25
这里我们使用了括号来让各个值正确结合(1 + 4 而不是 5 * 1),括号的用法与数学中相同,不过 Kotlin 中的中括号 [] 和大括号 {} 有其它的含义,因此只能使用小括号。Kotlin 会正确地把括号进行配对,即使是这 ((((((((((())))))))))) 么多括号也没问题。
给它起个名字
当你有了一些值之后,你就会想将它们保存起来,以便稍后使用。在 Kotlin 中这么做很简单:
var res = 1
我们通过 var 定义(Define) 了一个变量,名为 res,用来指代 1 这个数值。为什么说是『指代』呢?因为 res 实质上并不拥有 1,它只是临时借用了它的使用权。你可以让 res 改为指向其它的数值,也就是『借用』其它的数,但你不能改变 1 本身,因为 1 就是 1。
你是职业选手吗?
与 Java 不同,在 Kotlin 中,即使是基本类型也是包装在对象中的。尽管编译器可能会做足够的优化,但理论上 Kotlin 中的所有值(null 除外)皆为引用。
稍后你可以使用这个变量:
res + 42 // 43
变量参与计算时,使用的就是其先前指代的值。如果这段代码和上面的代码连在一起,那么这个表达式的值是 43。
如果想要改变 res 指代的值呢?只需要使用 = 对它重新进行设定,称作赋值(Assign):
var res = 1
res = 8 * 11 // 前面没有 `var`
变量也可以出现在赋值的右侧,Kotlin 先用变量当前的值计算出结果,再更新变量的值:
var res = 1
res = res + 12 // res 变为 13
虽说变量是可变的,但是下面这样的代码却不能运行:
var a = 1
a = "ciallo, world" // 不行!
对变量赋值时,变量的类型不能改变。a 在定义时是整数,那它在被销毁前就只能是个整数,就是这样。也正是因此,Kotlin 被称作强类型语言。
食食物者为俊杰
比计算器更智能一点的程序,都需要根据不同的条件做不同的事情。因此,工程师们发明了这样的条件分支(Conditional Branch) 表达式:
var b = 0
if (a != 0) {
b = 10 / a
} else {
b = -1
}
你是职业选手吗?
刚才的用词是不是错了?条件分支结构不是语句吗?在 Kotlin 中,if 在有些情况下可以作为表达式来使用,例如:
var a = if (b >= 0) b else -bif (条件) { 肯定块 } else { 否定块 } 这个结构,表达的就是『如果 XXX 成立,那么做……否则做……』。() 中的条件必须是逻辑值(即值为逻辑类型的表达式),而两对 {} 和 {} 中的内容,分别在条件成立和条件不成立的时候执行。上面的程序在 a 不为 0 的时候让 b 的值等于 10 / a,否则就让 b 的值等于 -1。
如果某个块只有一个表达式,那么可以将对应的 {} 省略掉。此外,如果否定块没有任何事情要做,那么就可以将第二对 {} 和 else 省略掉,所以上面的代码可以简化为:
var b = -1
if (a != 0) b = 10 / a
无论是肯定块还是否定块,其内部都可以嵌套条件分支,以及绝大多数其它结构。这是因为在 Kotlin 中,大括号 {} 所框出的区域,即所谓的块(Block),有极强的独立性。你可以在其中定义与外部同名的变量,添加其它条件分支或者循环(稍后要提到)表达式,甚至定义类与接口 —— 这都可以做!
『好吧,但是同样的事情我在 Java 里也能做啊,为什么要用 Kotlin 呢?』有读者一定会问这样的问题,那么我们就来展示一些 Kotlin 的魔法:
var a = if (b >= 0) b else -b
这段代码的实际作用就是求得 b 的绝对值并将其赋给 a,但我们居然可以直接将 if 结构的结果赋值给变量!好吧,也许在 Kotlin 设计师看来这没什么神奇的,因为 if 结构只要满足以下两个条件,就可以当作表达式使用,既可以用于计算,也可以用于赋值:
if必须是完整的(不能缺少else)- 肯定块与否定块的值类型必须相同
所谓某个块的值(嗯,没错,{} 本身也是一个表达式),指的是这个块中最后一个表达式的值。如果那个块只有一个表达式(无论是否省略 {}),那么块的值就是那个表达式的值。例如:
{
var x = 1
x + 2
}
这个块的值是 3,因为最后一个表达式是 x + 2。
了解了这些,你就可以写出这样的代码:
var res = if (a >= b) {
if (b == 0) -1 else a / b // 这个 if 表达式的值将作为外层 if 肯定块的值
} else {
if (a == 0) -1 else b / a // 这个 if 表达式的值将作为外层 if 否定块的值
}
上述程序的功能是计算 a 和 b 中较大数除以较小数的结果,并且如果除数为 0 则结果为 -1,然后将它赋给 res。
顺便一提,Kotlin 的格式很灵活,你可以在代码的几乎任何位置加入或删除换行或空格,只要不拆开单词或者将两个单词连成一个,都不会影响程序的执行。上面的代码也完全可以写成一行:
var res = if (a >= b) if (b == 0) -1 else a / b else if (a == 0) -1 else b / a
或者拆成很多行:
var res =
if
(a >= b)
{
if
(b == 0)
-1
else
a / b
}
else
{
if
(a == 0)
-1
else
b / a
}
只是两种写法都不如原始版本容易阅读和理解。
无论多少次
计算机非常强大,因为它们思维敏捷,而且完全不知疲倦。—— 改编自《父与子的编程之旅》
程序经常需要重复做一些事情,比如 Minecraft 每秒更新 20 次世界数据(理论上),防病毒程序反复地扫描系统寻找病毒,等等。在 Kotlin 中,要重复做一些事情,可以使用条件循环(Conditional Loop) 结构:
var a = 100
while (a != 0) {
a = a - 1
}
while (条件) { 块 } 做以下两件事情:
- 检查条件是否成立。
- 如果条件成立,就执行块中的代码,然后回到第一步,否则,离开循环。
和分支结构一样,() 内的条件只能是逻辑值。
循环一旦开始,只要条件成立就会一直执行下去。不过,有时你可能会希望跳过一次循环,或者提前打断循环,这也是可以做到的:
while (true) {
if (a < 10) {
a = a - 1
continue
}
a = a - 2
if (a < 5) {
break
}
}
continue 和 break 表达式用于控制循环。continue 跳过当前循环的剩余部分(立即开始下一次循环),而 break 终止循环,不论条件是否成立。
你是职业选手吗?
你或许已经猜到,continue、break 以及后面要介绍的 return 和 throw 也同样是表达式。尽管这些表达式的值没有什么实际的含义,但它们可以放在一些需要表达式的地方,使得下面这样的代码可以正常编译:
var b = if (a > 0) 5 else throw IllegalArgumentException()这一节太长了,而且介绍的概念也很多,你可能需要点时间来理解。当你准备好了后可以继续,我们将在下一节介绍关于函数和对象的相关内容。