跳到主要内容

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 不是 1false 也不是 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 -b

if (条件) { 肯定块 } 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 否定块的值
}

上述程序的功能是计算 ab 中较大数除以较小数的结果,并且如果除数为 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 (条件) { 块 } 做以下两件事情:

  1. 检查条件是否成立。
  2. 如果条件成立,就执行块中的代码,然后回到第一步,否则,离开循环。

和分支结构一样,() 内的条件只能是逻辑值。

循环一旦开始,只要条件成立就会一直执行下去。不过,有时你可能会希望跳过一次循环,或者提前打断循环,这也是可以做到的:

while (true) {
    if (a < 10) {
        a = a - 1
        continue
    }

    a = a - 2

    if (a < 5) {
        break
    }
}

continuebreak 表达式用于控制循环。continue 跳过当前循环的剩余部分(立即开始下一次循环),而 break 终止循环,不论条件是否成立。

你是职业选手吗?

你或许已经猜到,continuebreak 以及后面要介绍的 returnthrow 也同样是表达式。尽管这些表达式的值没有什么实际的含义,但它们可以放在一些需要表达式的地方,使得下面这样的代码可以正常编译:

var b = if (a > 0) 5 else throw IllegalArgumentException()

这一节太长了,而且介绍的概念也很多,你可能需要点时间来理解。当你准备好了后可以继续,我们将在下一节介绍关于函数和对象的相关内容。