Kotlin实战02 — Kotlin基础

时间:2021-01-06 15:46:48

这章将讲述
- 声明函数、变量、类、枚举和属性
- Kotlin的控制结构
- 智能强转
- 抛和处理异常

1 基本元素: 函数和变量

Kotlin的两大元素:函数和变量。你将明白,你可以省略类型声明,还有鼓励你使用不变而不是可变的数据。

1.1 Hello, world!

让我们以一个经典的例子开始:打印“Hello, world!”,在Kotlin中,你只需要一个函数:

fun main(args: Array<String>) { 
println("Hello, world!")
}

在这段简短的代码中,我们可以观察到什么该语言的什么部分和特点呢?请看下面的列表:
- fun关键词用来声明一个函数
- 参数类型现在参数名字的后面。同样适用于变量声明
- 函数可以在文件的最上层中声明;你没必要把它放到一个类中
- 数列仅仅是类。不像Java,Kotlin没有特定的声明数组的语法。
- 用println,而不是System.out.println。Kotlin标准库提供了很多标准Java库函数的包装,这有更简洁的语法。println就是其中之一。
- 在一行的最后省略了分号,就像在其他的语言。

1.2 函数

如果函数有返回类型,在函数参数后面加冒号和返回类型:

fun max(a: Int, b: Int): Int { 
return if (a > b) a else b
}
println(max(1, 2)) //2

注意,在Kotlin中if是个有返回值的表达式。类似于Java中的三目运算符(a > b)? a : b

语句(statement)和表达式(expression)

在Kotlin中,if是个表达式,而不是一个语句。语句和表达式的区别在于,表达式是一个值,可以被用作另外表达式的一部分;而语句总是一个包含它的代码块内的顶层元素,没有自己的值。在Java中,所有的控制结构都是语句,但是在Kotlin中,大部分控制结构,除了循环(for , do和do/while),是表达式。联合控制结构和其他的表达式,可以让你简洁表达许多通常的模式。

另外一方面,在Java中赋值是表达式,但是在Kotlin中变成了语句。这有效避免了比较和赋值之间的混淆,这个混淆也是错误的一个来源。

表达式主体
进一步可以简化前面函数,因为函数体只含有单个语句,你可以用表达式来作为整个函数体,移除花括号和返回语句:

fun max(a: Int, b: Int): Int = if (a > b) a else b

如果用花括号来表达函数主体,我们叫这个函数为代码块体(block body),如果直接返回表达式,我们叫它为表达式体(expression body)。

INTELLIJ IDEA提示 IntelliJ IDEA提供了在两种不同函数风格“Convert to expression body”和 “Convert to block body”之间的转换

表达式体的函数在Kotlin代码中很常见,这个风格不止用在一行函数,也用在对单一和更加复杂的表达式求值,比如if,when和try。我们进一步省略返回类型:

fun max(a: Int, b: Int) = if (a > b) a else b

为什么函数没有返回类型的声明呢?Kotlin不是一个静态语言,要求每个表达式在编译阶段都有类型吗?事实上,每个变量和每个表达式都有类型,每个函数也有返回类型。但是对于表达式体的函数,编译器可以分析作为函数体的表达式,用它的类型作为返回类型,即使没有显示的写出来。分析的这个类型通常叫类型推导(type inference)。

注意,省略返回类型仅仅在表达试体的函数中允许。有代码块体的有返回值的函数,你必须指明返回类型和显示的返回语句。这是个有意的抉择。实际中的函数通常非常长,可能包含很多返回语句,有显示的返回类型和语句可以帮助你快速的知道什么被返回。

1.3 变量

在Java中,你用类型声明变量。但是在Kotlin中,你可以也可以不把类型放到变量名后面。省略类型的声明如下

val question = "The Ultimate Question of Life, the Universe, and Everything"
val answer = 42

或者你显示的指明

val answer: Int = 42

如果你要浮点型的常量,可以推导为Double

val yearsToCompute = 7.5e6//7.5X10^6 = 7500000.0

可变和不可变量
- val(来源于value)— 不变的引用。一旦声明为val的量初始化后,不能够重新赋值。对应于Java里面的final变量
- var(来源于variable)— 可变的引用。变量的值可以改变。对应于Java里面的正常的变量(非final)

通常,尽量声明所有的变量为val关键词。只有有需要的时候,才变为val。用不可变的引用、不可变的实例和函数,没有副作用,使得你的代码更像函数式的风格。val变量只能在代码块中初始化有且仅有一次。但是可以根据不同的情况,用不同的值来初始化,如果编译器能够保证仅有一个初始化语句执行:

val message: String
if (canPerformOperation()) {
message = "Success"
// ... perform the operation }
else {
message = "Failed"
}

注意,val引用自己是不可变的,但是,他指向的实例是可以改变的。比如,下面的代码是完全有效的:

val languages = arrayListOf("Java") //声明不可变的引用
languages.add("Kotlin")//改变引用指向的实例

尽管var关键词允许变量改变他的值,但是它的类型是确定的:

var answer = 42 
answer = "no answer"//编译错误:类型不匹配

如果你想在变量里面存储一个不匹配的类型的值,你必须转换或者协变这个值到正确的类型。

1.4 更容易的字符串格式化:字符串模板

这个部分开始的“Hello World”例子,我们进一步这个惯例,用Kotlin的方式,通过名字来问候。

fun main(args: Array<String>) { 
//打印“Hello,Kotlin”,如果输入参数为Bob,则打印“Hello,Bob”
val name = if (args.size > 0) args[0] else "Kotlin"
println("Hello, $name!")
}

这个例子引进了一个功能叫字符串模板(string templates)。和其他脚本语言一样,Kotlin允许在字符串字面量中,通过$字符放在变量名前面,引用本地变量。这个同Java中的字符串连接(“Hello, ” + name + “!”), 但是更加紧凑和有效率(注:都是创建StringBuilder,添加常量部分和变量值,Java虚拟机有优化)。

如果你引用一个不存在的本地变量,因为表达式会静态检查,这些代码会编译不成功。如果你想在字符串中包含 println(" x”)换码,打印出$x,而不是把x翻译为一个变量的引用。

不限于一个简单的变量名,你也可以用更加复杂的表达式,仅仅只要在表达式括上花括号:

fun main(args: Array<String>) { 
//用${}插入args数组的第一个元素
if (args.size > 0) { println("Hello, ${args[0]}!") }
}

你也可以双引号内陷双引号,只要他们是在同一个表达式:

fun main(args: Array<String>) { 
println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}

2 类和属性

让我们看看一个简单的JavaBean的Person类,现在只包含一个name属性

/* Java */ public class Person { 
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

在Java中,构造子的代码块内,常常包含一些重复内容:把参数赋值到响应的域。在Kotlin中,这个逻辑不需要如此多的样板代码。在第一章中5.6节,我们介绍了Java-to-Kotlin转换器:一个自动把Java代码转换到Kotlin代码的工具,代码的功能是相同的。

class Person(val name: String)

如果你熟悉现代的JVM语言,你可能见过类似的东西。这个类型的类(只包含数据,但没有代码),常常叫值实例(value object),许多语言提供了声明他们的简洁语法。注意到在转换过程中,public修饰符不见了。因为在Kotlin,public是默认的可见性,所以你能省略它

2.1 属性

你肯定知道,类的概念是封装数据和处理数据的代码到单一的实体。在Java中,数据存储到域中,通常是私有的。如果你想让类的客户端访问这个数据,你需要提供访问器方法(accessor meth-ods):一个getter、可能有一个setter。你在Person类的例子中已经看到。setter可能包含一些额外的逻辑,验证传递值,或者发送值变化的通知等等。

在Java中,域和访问器的组合,通常叫做属性(property), 很多框架较多使用这个概念。在Kotlin中,属性是语言支持的第一等功能,完全用来替代域和它的访问器方法。就像你用val和var关键词,定义一个变量,你可以同样的方式定义类的属性。声明为val的属性是只读的,而var属性是可变的,

class Person( 
val name: String, //只读属性:自动生成一个域和简单的getter
var isMarried: Boolean //可写属性:一个域,getter和setter
)

基本上,当你定一个属性,你就定义了相应的访问器。默认地,定义访问器也是简单的,域存储值、getter和setter来返回和更新值。如果你愿意,用不同的逻辑计算和更新属性值,来自定义访问器。上面的Person简洁的定义隐藏了实现的细节,就像原来的Java代码一样:一个类含有私有的域并且在构造子中初始化,可以用响应的getter获取到。这意味着,你可以在Java和Kotlin中使用这个类,不管这个类在哪里申明的。使用是一样的。下面是Java代码中如何使用:

/* Java */
Person person = new Person("Bob", true);
System.out.println(person.getName()); //Bob
System.out.println(person.isMarried()); //true

Kotlin的name属性在Java中的getter方法叫getName。getter和setter命名规则有个例外:如果属性名以is开始,getter没有附加的前缀,在setter名字中,is被set取代。所以,在Java中,你调用isMarried()。如下是Kotlin的结果

val person = Person("Bob", true)
println(person.name)// Bob
println(person.isMarried) //true

你不是调用getter,而是直接引用属性。逻辑是一样的,但是代码更加简洁。可变属性的setter一样,在java中你用person.setMarried(false)表达离婚,在Kotlin中person.isMarried = false。

提示 你可以在Java定义的类中使用Kotlin的属性语法。在Java类中的getter可以在Kotlin中val属性获取,getter/setter可以通过var属性获取。比如,如果在Java类定义了setName和setName的方法,那么可以通过叫name的属性获取。如果类定义了isMarried和setMarried方法,相应的Kotlin属性叫isMarried。

大多数情况下,属性有相应的支持属性,即存储属性值。但是如果值是随手计算的,比如从其他属性计算,你可以用自定义的getter表达。

2.2 自定义访问器

这个部分,你将看到怎么自定义实现一个属性访问器。假设你声明了一个长方形,它可以告诉是不是一个正方形。你没必要用单独的域存储这个信息,因为你需要动态检查高是否等于宽:

class Rectangle(val height: Int, val width: Int) { 
val isSquare: Boolean
get() { //Property getter declaration
return height == width
}
}

isSquere属性不需要一个域来存储它的值。它仅仅只有自定义实现的getter。这个属性被获取时每次计算。注意到,你不需要用花括号这个完整的语法,你可有写成get() = height == width。这样的属性的调用也是一样的:

val rectangle = Rectangle(41, 43)
println(rectangle.isSquare) //false

如果你想在Java中获取这个属性,你可以就像以前一样调用isSquare方法。

你可能问,是否定义一个没有参数的函数比自定义getter的属性好。这两个选项是相似的:在实现和性能是没有区别的,它们仅仅在可读性上有差别。一般讲,如果你描述一个类的特点(属性),你应该定义它为属性。

2.3 Kotlin源码布局:目录和包

Java把所有的类放进包里面。Kotlin也像Java,有包的概念。每个Kotlin文件在开头有package语句,文件中所有的声明(类、函数和属性)将放在这个包下。如果其他的文件在同一包下,里面所有的定义可以直接使用;如果这些定义在不同包里面,那么他们需要导入。就像在Java中,导入语句放置在文件的开头,使用import关键词。下面是个例子,展示包声明和导入语句:

package geometry.shapes //包声明

import java.util.Random //导入标准Java库类

class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() = height == width
}

fun createRandomRectangle(): Rectangle {
val random = Random()
return Rectangle(random.nextInt(), random.nextInt())
}

Kotlin不会区别导入类和函数,允许你用import关键词导入任何声明。你可以通过名字导入顶层函数

package geometry.example 

import geometry.shapes.createRandomRectangle //通过名字导入函数

fun main(args: Array<String>) {
println(createRandomRectangle().isSquare)//非常不可能打印"true"
}

通过包名后面加上.*,你可以导入特定包里面定义的所有声明。注意,星号导入(star import)不仅使得定义包里面的类可见,而且使得顶层函数和属性可见。在上面的例子中,import geometry.shapes.* 代替显示的导入,也使得使得代码正常编译。

在Java中,目录结构和包的层级是重复的。在Kotlin中你可以在同个文件中定义多个类。Kotlin也没限制磁盘上源文件的结构。你可以用目录结构来组织你的文件。比如,你可以在文件shapes.kt中定义geometry.shapes包的所有内容,然后把这个文件放在geometry目录下,没有必要创建shapes文件夹。
Kotlin实战02 — Kotlin基础

但是,在大多数情况下,跟随Java目录结构和根据包结构把源码组织成目录,是最佳实践。特别是Kotlin和Java混合的项目,坚持这样的结构特别重要。因为这样做可以让你逐步迁移代码,而没有引入意外的情况。但是请你不要犹豫把多个类合成到同一个文件,特别是当类很小的时候(在Kotlin中,这些经常存在)。

3 选项的表述和处理:枚举和“when”

在这一节中,我们将讲述when结构。它可以被想成Java中的switch替代品,但是更加强大和更常使用。同时,有一个在Kotlin中声明枚举的例子,然后讨论智能强转的概念。

3.1 声明枚举类

让我们加一些想象的明亮照片到这个严肃的书籍,看看下面颜色的枚举

enum class Color { 
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

与Java的仅仅有关键词enum相比,Kotlin声明使用更多的关键词,这是比较少见的。在Kotlin中,enum就是所谓的软关键词(soft keyword):当它放置在class关键词之前,它才有特有的意义。但是你可以在其他的地方,把它当成常规的名字使用。另外一方面,class还是一个关键词,你可以用clazz和aClass来声明变量。
就像在Java中,枚举不是值的列表:你可以在枚举类中声明属性和方法:

enum class Color( 
val r: Int, val g: Int, val b: Int //声明枚举常量的属性
) {
RED(255, 0, 0), //当每个变量创建的时候,指定属性值
ORANGE(255, 165, 0), //逗号是必须的
YELLOW(255, 255, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255),
INDIGO(75, 0, 130),
VIOLET(238, 130, 238);
fun rgb() = (r * 256 + g) * 256 + b//定义枚举的方法
}
println(Color.BLUE.rgb())//255

枚举常量就像你看见的正常的类一样,有相同的构造子和属性声明。当你定义一个枚举常量,你需要为它提供属性值。这个例子中展示了Kotlin语法唯一需要分号的地方:在枚举类中如果你定义任何方法,分号区分了枚举常量列表和方法声明。

3.2 用“when”来处理枚举类

你记得孩子如何利用助记短语来记忆彩虹的颜色吗?这就是一个:“Richard Of York Gave Battle In Vain!”假设你需要一个函数来给你为每个颜色一个助记(你不想把这些信息存储在枚举里面)。在Java中,你使用switch语句,在Kotlin中,响应的结构是when。

就像if一样,when也是一个返回值的表达式,所以你可以写一个函数,它的表达式体直接返回when表达式。如下:

fun getMnemonic(color: Color) = //直接返回一个“when”的表达式
when (color) { //如果颜色等于枚举常量,返回响应的字符串
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
}
println(getMnemonic(Color.BLUE)) // Battle

不像Java,你不需要为每个分支写break语句(缺少break是Java代码中引入错误的原因)。你可以在同个分支结合值,用逗号来分离:

fun getWarmth(color: Color) = when(color) { 
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
println(getWarmth(Color.ORANGE)) //warm

上面的例子用全名来使用枚举常量,指定Color枚举类名。你可以用导入常量来简化代码:

import ch02.colors.Color //导入声明在另外一个包的Color类
import ch02.colors.Color.*//用名字显示导入枚举常量

fun getWarmth(color: Color) = when(color) {
RED, ORANGE, YELLOW -> "warm" //用名字导入常量
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}

3.3 使用任意实例的“when”

Java中的switch,需要使用常量(枚举常量、字符串或者数字字面常量)作为分支条件,但是在Kotlin中,when允许任何的实例。下面我们写一个混合两者颜色的函数,如果它们可以在这个小的调色板中能够混合。

fun mix(c1: Color, c2: Color) = 
when (setOf(c1, c2)) {//when表达式的参数可以是任何实例,用来被分支条件检查
setOf(RED, YELLOW) -> ORANGE//枚举可以混合的颜色对
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")//执行这个,如果没有分支可以匹配
}
println(mix(BLUE, YELLOW))//GREEN

Kotlin标准库中含有一个setOf的函数,用来创建Set,包含参数指定的实例;一个set是一个集合,它的项的次序并不重要。所以,如果setOf(c1, c2)和setOf(RED, YELLOW)是相等的,那么意味着要不然c1是RED和c2是YELLOW,或者相反。

3.4 用没有参数的when

上面的例子有点效率低下,因为每次你调用这个函数,它都会创建几个Set实例,仅仅是用在检查两个颜色是否匹配另外两个颜色。正常情况下,通常不是个问题。但是如果这个函数经常被调用,那么为了避免GC,值得用另外一种方式来重写这个代码。你可以用不带参数的when表达式完成。代码虽然可读性差一点,但是这是为了达到更好性能付出的代价。

fun mixOptimized(c1: Color, c2: Color) = 
when {
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORANGE
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> GREEN
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
else -> throw Exception("Dirty color")
}
println(mixOptimized(BLUE, YELLOW)) //GREEN

如果when表达式没有参数,它的分支可以是任何的布尔值。mixOptimized函数和上面的mix做相同的事情。优点是不需要创建任何额外的实例,但是代价是更难阅读。

3.5 智能强转:结合类型检查和强转

作为整个部分的例子,写一个简单算术表达式的求值,比如(1+2)+4。其他的算术操作(减乘除)也可以用类似的方式实现,你可以当做一个练习。
第一, 你怎么编码这个表达式?你可以在树状结构中存储,每个节点是一个总数(Sum)或者一个数字(Num)。Num永远是叶子节点,而Sum节点有两个作为sum操作的参数的子节点。以下列表显示的是一个用来编码表达式的类的简单结构:Expr的接口,它的Num和Sum两个实现类。需要注意的是,Expr没有声明任何方法,仅仅是用作标记接口,提供相同类型的不同种类表达式。

interface Expr 
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

Sum存储了左边和右边Expr类型的参数的引用。在这个小例子中,它们可以使Num或者Sum。为了存储表达式(1+2)+4,你可以穿件一个实例:Sum(Sum(Num(1), Num(2)), Num(4))。如下图展示的树状结构
Kotlin实战02 — Kotlin基础
我们看看怎么计算一个表达式的值:

println (eval(Sum(Sum(Num(1), Num(2)), Num (4)))) //7

Expr接口有两个实现,所以对于一个表达式求最终的值,你可以有两种选择:
- 如果表达式是一个数字,返回相应的值
- 如果是一个和,对左边和右边表达式求值,返回它们的和

首先,我们用通常的Java方式来写这个函数,然后用kotlin的方式重构。在Java中,你可能用if语句序列来检查选项,所以让我们在Kotlin中用同样的方法:

fun eval(e: Expr): Int { 
if (e is Num) {
val n = e as Num //显式的强转到Num是冗余的
return n.value
} if (e is Sum) {
return eval(e.right) + eval(e.left) //变量e是智能强转
}
throw IllegalArgumentException("Unknown expression")
}
println(eval(Sum(Sum(Num(1), Num(2)), Num(4)))) //7

在Kotlin中,检查一个变量是不是某种类型用is检查。如果你在C#编程过,这个概念会很熟悉。is检查和Java中的instanceOf类似。但是在Java中,如果你已经检查了一个变量是否是某种类型,同时想取得这个类型的属性,你需要在instanceOf检查后面再加一个显式的类型转换。初始的变量需要不只使用一次,通常需要把类型转换后的变量存储到单独的变量中。在kotlin中,编辑器为你做了这些工作。如果你检查变量为某种类型,你没必要在后面再类型转换。你可以当做你想检查的类型来使用它。事实上,编译器,编译为我们类型转换了,我们叫它只能智能类型转换(smart cast)

在eval函数中,在你检查这个变量e是否是Num类型后,编译器解释它为Num变量。然后你可以不需要显式的类型转换就可以取得Num的value属性。同样的情况适用于Sum的右边和左边的属性: 在相应的情形下你只需要写e.right和e.left。在IDE中,智能转换的值用一个背景色来强调,所以你可以很容易知道这个值是预先检查了的,如下:
Kotlin实战02 — Kotlin基础
智能转换只有变量在is检查后没有被改变。当你对一个类的属性进行智能转换,属性必须是val,而且不能有自定义的存取器。否则不能确定每次获得这个属性将获得同样的值。一个显式转换到特定类型用as关键词来表达:

val n = e as Num

3.7 代码块作为if和when的分支

if和when都可以用代码块作为分支。在这个例子中,代码块中最后最后一个表达式作为结果。如果你想在例子函数中加日志,你可以在代码块中完成,并用最后一个值返回。

fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value //如果e是Num类型,这是代码块最后一个表达式,并被返回
}
is Sum -> {
val left = evalWithLogging(e.left)
val right = evalWithLogging(e.right)
println("sum: $left + $right")
left + right//如果表达式被返回当e是Sum类型
}
else -> throw IllegalArgumentException("Unknown expression")
}
println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
//num: 1
//num: 2
//sum: 1 + 2
//num: 4
//sum: 3 + 4
//7

“代码块中最后一个表达式是返回值”这个规则,在使用代码块而且期待返回一个结果的情况西安,所有情形下都成立。你将在本章结束的时候,同样的规则在try代码体和catch子句下同样适用。在第五章论述lambada表达式下它的应用。但是在2.2节中提到,这个规则对于常规的函数式不适用的。一个函数可以是没有代码块的表达式体,或者是一个有显示的return语句的代码块体

4 迭代事物:while和for循环

for循坏只存在一种形式,相对于Java的for-each循环。就像C#里面的写法:for in 。不存在Java中的通常for语法。

4.1 while循环

和Java对应的循环一样的语法

while (condition) { //当条件为真时,代码体执行
/*...*/
}
do {//无条件的执行一次,之后当条件为真时执行
/*...*/
} while (condition)

4.2 数字的迭代:范围和累进

由于不存在Java中通常的for语法,Kotlin用范围(ranges)这个概念。范围是两个值之间的间距,这两个值为开始和结束的值,用..操作子表示。

val oneToTen = 1..10

范围在Kotlin是自闭的(Closed)或者自包含(inclusive),这意味着第二个值总是范围的一部分。如果你迭代范围内的所有的值,这样的范围也叫累进(progression)

让我们用整数的范围玩Fizz-Buzz游戏。参与者轮流递增式数数,用fizz单词替代任何可以被三整除的数字,用buzz单词替代任何可以被五整除的数字。如果一个数字同时是三和五的乘数,我们叫“FizzBuzz”。

如下列表打印了从1到100之间的正确答案。注意这么用没有参数的when表达式检查可能的条件:

fun fizzBuzz(i: Int) = when { 
i % 15 == 0 -> "FizzBuzz " //i可以被15整除,返回FizzBuzz。就像在Java中,%是模操作
i % 3 == 0 -> "Fizz " //i可以被5整除,返回Buzz
i % 5 == 0 -> "Buzz " //i可以被3整除,返回Fizz
else -> "$i " //Else返回这个数字本身
}
for (i in 1..100) { //迭代整数范围1..100
print(fizzBuzz(i))
}
//1 2 Fizz 4 Buzz Fizz 7 ...

如果你觉得厌倦了这些规则,想要把规则搞的复制一些。让我们从100倒过来数,而且只包括偶数:

for (i in 100 downTo 1 step 2) { 
print(fizzBuzz(i))
}
//Buzz 98 Fizz 94 92 FizzBuzz 88 ...

当你以一个步长step迭代一个累进,这可以忽略一些数字。这个步长可以是负数,这样的话累进向后而不是向前。这这个例子中,100 downTo 1 是一个(步长为-1)向后的累进。然后步长改变它的绝对值为2,同时保持方向(事实上是,设置步长为-2).

就像前面提到的,..语法创建了一个包含终点(..右边的值)的一个范围。在许多情况下,迭代半自闭的范围,即不包含指定的终点,这样会更加方便。为了创建这样一个范围,用until函数实现。比如,for (x in 0 until size)循环等于for (x in 0..size-1),但是它表达的意思更加清楚。

4.3 map的迭代

我们提到过,追常见的情形是,for…in循环是迭代一个集合。这个是和Java是一样的,所以毋庸赘言。下面我们看看怎么迭代一个map。

举个例子,让我看看一个小程序,打印字符的二进制表示。仅仅为了展示的目的,你将存储二进制表示到一个map之中。下面的代码创建了一个map,用一些字母的二进制填进去,然后打印map里面的内容。

val binaryReps = TreeMap<Char, String>()//用TreeMap,所以键是排序的

for (c in 'A'..'F') { //用字符的范围迭代从A到F的字符
val binary = Integer.toBinaryString(c.toInt()) //ASCII编码转换到二进制
binaryReps[c] = binary//在map中用c键存储值
}
for ((letter, binary) in binaryReps) { //迭代一个map,把键值对赋值到两个变量
println("$letter = $binary")
}

..语法创建范围不仅仅对数字适用,也对字符适用。我们用它迭代所有的字符,从A到(包括)F。

上面显示了,for循环让你解构迭代集合的元素,在这个例子中,是map里面键值对的集合。解构的结果存储到两个不同值中:letter接受键,而binary接受值。另外一个有用的技巧是,用键获取和更新一个map里面的值。不是调用get和put,你用map[key]读值,而用map[key] = value设置。代码binaryReps[c] = binary相当于Java里面的binaryReps.put(c, binary)。输出如下:

A = 1000001 B = 1000010 C = 1000011 D = 1000100 E = 1000101 F = 1000110

你可有用同样的结构语法迭代一个集合,同时记录当前项的索引。你不必手动的创建一个独立的存储索引的变量。编码打印如你所料,如下:

val list = arrayListOf("10", "11", "1001") 
for ((index, element) in list.withIndex()) {
println("$index: $element")
}
//0: 10
//1: 11
//2: 1001

4.4 用in检查集合和范围的属性

用in操作子检查一个值是否在范围里面,或者想法,用!in检查是否一个值是否不在一个范围里面。下面看看怎么用in检查一个字符是否属于字符范围里面:

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z' 
fun isNotDigit(c: Char) = c !in '0'..'9'
println(isLetter('q')) //true
println(isNotDigit('x')) //true

背下地,没有任何诡计:检查字符编码是否在第一个和最后一个编码之间的某个地方。但是这个逻辑被隐藏在标准库里面范围类的实现里面。

c in 'a'..'z'//变换成a <= c && c <= z

in和!in操作子也可以在when表达式里面使用

fun recognize(c: Char) = when (c) { 
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't know…"
}
println(recognize('8')) //It's a digit!

范围也不限于字符。如果你有任何支持比较实例的类(实现java.lang.Comparable接口),你可以创建那种类型实例的范围。如果你有这样的范围,你不能枚举这个范围的所有的实例。想想这个:比如,你能枚举在“Java”和“Kotlin”之间的所有的字符串吗?是的,你不能。但是你依然可以用in操作子检查另外实例是否属于这个范围:

println("Kotlin" in "Java".."Scala") //和“Java” <= “Kotlin” && “Kotlin” <= “Scala”一样
//true

字符串在这里是按字母比较的,因为那是String类怎么实现Comparable接口的。同样的in检查对集合也适用:

println("Kotlin" in setOf("Java", "Scala")) //这个集没有“Kotlin”字符串
//false

5 Kotlin中的Exception

Kotlin中的异常处理与Java或者其他语言中的处理方式相似。一个函数可以以正常方式结束,或者当错误发生的时候抛出异常。函数调用者捕获这个异常并处理它;如果没有,异常重新在调用栈向上抛。

Kotlin中的异常处理语句的基本形式和Java是相似的。你可以以不足为奇的方式抛出一个异常:

if (percentage !in 0..100) { 
throw IllegalArgumentException( "A percentage value must be between 0 and 100: $percentage")
}

就像其他的类,你不需要用new关键词创建异常实例。不像Java,在Kotlin中,throw结构是一个表达式,可以用作为其他表达式的一部分:

val percentage = 
if (number in 0..100)
number
else
throw IllegalArgumentException( //“throw” 是一个表达式
"A percentage value must be between 0 and 100: $number")

5.1 try、catch和finally

就像Java之中,可以用try结构,和catch和finally子句处理异常。如下,读取指定文件的一行,尝试解析为个数字,然后返回一个数字,如果这行不是有效的数字,返回null。

fun readNumber(reader: BufferedReader): Int? { //不必要显式地指定需要这个函数抛出的异常
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) { //异常的类型在右边
return null
} finally { //finally就像在Java一样的
reader.close()
}
}
val reader = BufferedReader(StringReader("239"))
println(readNumber(reader))
//239

在段代码和Java最大的不同是不需要throws子句:如果你在Java中写这个函数,你必须显式地在函数声明后面写throws IOException。你必须这么做是因为IOException是受检查的异常checked exception。在Java中,一个异常必须显式的处理。你不得不声明函数可以抛的所有的受检查异常。如果你调用其他的函数,你需要处理它的受检查的异常,或者声明你的函数抛出这些异常。
就像其他现代JVM语言,Koltin不区别受检查和不受检查的异常。你需要指定一个函数抛出的异常,你可以也可以不处理这些异常。这个设计决定是基于Java中使用受检查异常的实践。经验表明,Java规则常常需要很多无意义的代码从新抛出或者忽略异常,这个规则并不能一致地避免发生的错误。

在上面的例子中,NumberFormatException是一个不受检查的异常。所以Java编译器不会强迫你捕获这个异常,你可以很容易的看见运行时的异常。这相当令人遗憾,因为不有效的输入数据是经常的事情,应该更优雅的处理。同时,BufferedReader.close方法也能抛出一个IOException异常,这是个需要处理的受检查的异常。如果关闭一个流失败了,大部分代码不能采取任何有意义的行动,所以需要从close方法捕获异常的代码基本是样板代码。

那么关于Java 7的try-with-resources怎么样呢?Kotlin没有对应的特别的语法;它被处理成一个库函数。

5.2 try作为一个表达式

为了显示Java和Kotlin直接一个重要区别,让我们稍微改变下这个例子。移除fianlly部分(因为你已经知道这个怎么工作),然后加一些代码打印从这个文件读取的数字。

fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine()) //成为try表达式的值
} catch (e: NumberFormatException) {
return
}
println(number)
}

val reader = BufferedReader(StringReader("not a number"))
readNumber(reader)//没有打印任何数字

Kotlin中try关键词,就像if和when,引进了一个表达式,你可以把它的值赋值给一个变量。不像if,你一直需要把语句保函在花括号中。就像其他语句,如果包涵多个表达式,try表达式的值是最后一个表达式的值。在这个例子中,在catch代码块中有return语句,所以这个函数在catch代码块后不会再进行。如果你想继续这个执行,catch语句也需要一个值,这个值是最后表达式的值:

fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine()) //没有异常发生时使用这个值
} catch (e: NumberFormatException) {
null //异常发生时使用null值
}
println(number)
}
val reader = BufferedReader(StringReader("not a number"))
readNumber(reader)//异常被抛出,所以函数打印null
//null

这时候如果你不耐烦了,你可以用类似Java中的写法,开始在Kotlin中写代码。当你读这本书的时候,你将继续学习怎么改变你习惯的思考方式,使用这个新语言的全部功能