Kotlin语言系统化学习K02-Kotlin基础

Kotlin系统化学习-20170823
文章中有大部分内容是借鉴《Kotlin实战》中文版,特此注明一下。

Kotlin系统化系列学习文章(有待更新):
http://blog.csdn.net/ClAndEllen/article/details/77466628

前篇文章我们把Kotlin的背景,学习流程整理了,以及详细说明了Kotlin语言的一些优势,接下来进入正式的学习:

K02-Kotlin基础

本篇文章学习内容:

1.声明函数,变量,类,枚举以及属性。

2.Kotlin中的控制结构。

3.智能转换。

4.抛出和处理异常。

2.1 函数和变量

2.1.1 Hello,World!

fun main(args: Array<String>) {

    println("Hello,World!")

}//相当于Java中的main,只不过这个main方法不放在一个单独的类中,而是放于顶层

你能从这样简单的一小段代码中观察到哪些特性和语法?

1.关键字fun用来声明一个函数。没错,Kotlin编程有很多乐趣(fun)。
2.参数类型写在名称后面。变量的声明也是如此。
3.函数可以定义在文件的最外层(顶层),不需要把它放在类中。
4.使用println代替了System.out.println。Kotlin标准库给Java标准库函数提供了许多语法更简洁的包装,而println就是其中一个。
5.和许多其他现代语言一样,可以省略每行代码结尾的分号。

2.1.2 函数

你已经看到了怎样声明一个没有任何返回任何东西的函数。但是如果函数有一个有意义的结果,返回类型应该放在哪里呢?其实很简单就放置与参数列表之后,紧跟于参数列表之后,以“:”号隔开,示例代码如下:

fun max(a:Int,b:Int):Int{
    return if(a>=b) a else b
}//普通写法

一个函数的基本结构如下图2.1所示:

语句和表达式:

在Kotlin中,if是表达式,而不是语句。语句和表达式的区别在于,表达式有值,并且能作为另一个表达式的一部分使用;而语句总是包围着它的代码块中的顶层元素,并且没有自己的值。在Java中,所有的控制结构都是语句。而在Kotlin中,除了循环(for,do和do/while)以外大多数控制结构都是表达式。这种结合控制结构和其他表达式的能力让你可以简明扼要地表示许多常见的模式,稍后你会在本书中看到这些内容。
  另一方面,Java中的赋值操作是表达式,在Kotlin中反而变成了语句。这有助于避免比较和赋值之间的混淆,而这种混淆是常见的错误来源。

表达式函数体

在Kotlin中,有一种函数的写法,让函数的声明变得非常简单,因为它的函数体是单个表达式构成,这种写法去掉了return和“{}”花括号,比如上面的代码写成表达式函数的形式如下:

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

如果函数体写在花括号中,我们说这个函数有代码块体。如果它直接返回了一个表达式,它就有表达式体。上述代码还可以这么简化,把返回类型省略掉:

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

为什么有些函数可以不声明返回类型?作为一门静态类型语言,Kotlin不是要求每个表达式都应该在编译期具有类型吗?事实上,每个变量和表达式都有类型。每个函数都有返回类型。但是对表达式函数来说,编译器会分析作为函数体的表达式,并把它的类型作为函数的返回类型,即使没有显式地写出来。这种分析通常被称作类型推导。
  注意,只有表达式体函数的返回类型可以省略。对于有返回值的代码块体函数,必须显式地写出返回类型和return语句。这是刻意的选择。真实项目中的函数一般很长且可以包含多条return语句,显式地写出返回类型和return语句能帮助你快速地理解函数能返回的是什么。接下来我们看看声明变量的语法。

2.1.3 变量

在Java中声明变量的时候会以类型开始。在Kotlin中这样是行不通的,因为许多变量声明的类型都可以省略。所以在Kotlin中以关键字开始,然后是变量名称,最后可以加上类型(不加也可以):

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

   val answer = 42

这个例子省略了类型声明,但是如果需要也可以显式地指定变量的类型:

   val answer:Int = 42

和表达体函数一样,如果你不指定变量的类型,编译器会分析初始化器表达式的值,并把它的类型作为变量的类型。在前面这个例子中,变量的初始化器42的类型是Int,那么变量就是这个类型。你可以理解编译器很聪明,总是给变量推导出合适的类型。
  如果变量没有初始化器,需要显式地指定它的类型:如果不能提供可以可以赋给这个变量的值的信息,编译器就无法推断出它的类型。

可变量和不可变量

声明变量的关键字有两个:

val(来自value)---不可变引用。使用val声明的变量不能在初始化之后再次赋值。它对应的是Java的final的变量。
var(来自variable)---可变引用。这种变量的值可以被改变。这种声明对应的是普通(非final)的Java变量。

默认情况下,应该尽可能地使用val关键字来声明所有的Kotlin变量,仅在必要的时候换成var。使用不可变引用,不可变对象及无副作用的函数让你的代码更接近函数式编程风格。只要你对Java中的“final”理解够透彻,理解Kotlin中的val也就不在话下。我这里就不再啰嗦了。var虽然是可变的,但是前提必须是类型匹配的,比如以下代码:

 var a = 3 //这里已经确定a的类型为Int
     a = 5 
     a = "3" //报错,因为a的类型是不可变的 

2.1.4 更简单的字符串格式化:字符串模版

Java当中的字符串模版很死板,变量的输出必须由"+"号连接,却不能镶嵌于一个常规的String当中,比如输出Student类的age和name,Java是这么输出的:

  System.out.println("学生姓名:"+s.name+",学生年龄:"+s.age);

要是输出Student类的属性有很多,那么就有点繁琐了,Kotlin就避免这一麻烦,可以在字符串字面值中引用局部变量,只需要在变量名称前面加上$,比如下面的代码:

 var a = 5
 println("a的值是:$a")

当然如果要输出一段表达式或者是引用调用某个属性,函数等,那么需要给输出的区域添加上花括号,示例代码如下:

fun main(args: Array<String>) {

    var a1 = 5
    var a2 = 6

    val  s = Student("ellen",23)

    println("相加的的值是:${a1+a2}") // 输出表达式
    println("学生姓名:${s.name},学生年龄:${s.myName()}")//输出通过引用调用属性或者方法

}

class Student(name:String,age:Int){

    var name = ""
    var age = 0

    fun myName() = name

}

现在你知道了如何定义函数和变量,那么接下来,来看看类。这一次,你会借助Java到Kotlin的转换器来开始运用新的语言特性。

2.2 类和属性

面向对象对你来说可不是什么新鲜的概念,你也许非常熟悉类的抽象机制。Kotlin这方面的概念你也会觉得似曾相识,但是你会发现许多常见的任务使用更少的代码就可以完成。这一节会向你介绍声明类的基本语法,在后面在深入细节。
  首先,来看一个简单的JavaBean类Person,目前它只有一个属性,name。
Java代码是这样的:

private class Person{

    private final String name;

    public Person(String name){
        this.name = name;
    }

    public String getName(){
        return name;
    }
}

在Java中,构造方法的方法体常常包含完全重复的代码:它把参数赋值给有着相同名称的字段。在Kotlin中,这种逻辑不用这么多的样板代码就可以表达。后面介绍Java到Kotlin的转换器:一个把Java代码替换成功能相同的Kotlin代码的工具。把上述代码使用转换器转换的代码如下:

class Person(val name:String)

就是这么强悍,如果你试过其他的一些现代JVM语言,你也许见过类似的事情。这中类(只有数据没有其他代码)通常被叫作值对象,许多语言都提供简明语法来声明它们。
  注意从Java到Kotlin的转换过程中public修饰符消失了。在Kotlin中public是默认的可见性,所以你能省略它。

2.2.1 属性

你肯定知道,类的概念就是把数据和处理数据的代码封装成一个单一的实体。在Java中,数据存储在字段中,通常还是私有的。如果想让类的使用者访问到数据,得提供访问器方法:一个getter,可能还有一个setter。在Person类中你已经看到了访问器的例子。setter还可以包含额外的逻辑,包括验证传给它的值。发送关于变化的通知等。
  在Java中,字段和其访问器的组合常常被叫作属性,而许多框架严重依赖这个概念。在Kotlin中,属性是头等的语言特性,完全代替了字段和访问器方法。在类中声明一个属性和声明一个变量一样:使用val和var关键字。声明成val的属性是只读的,而var属性是可变的。

 class Person{
 
     val name:String,  //只读属性:生成一个字段和一个简单的getter
     
     var isMarried:Boolean //可见属性:一个字段,一个getter和一个setter
 
 }

基本上,当你声明属性的时候,你就声明了对应的访问器(只读属性只有一个getter,而可写属性既有getter,也有setter)。访问器的默认实现非常简单:创建一个存储值的字段,以及返回值的getter和更新值的setter。但是如果有需要,可以声明自定义的访问器,使用不同的逻辑来计算和更新属性的值。

Person person = new Person("ellen",false);
System.out.println(person.getName());
System.out.println(person.isMarried());

注意,不管Person是定义在Java还是Kotlin中,这段代码看起来是一样的。Kotlin的属性name把一个名称getName的getter方法暴露给Java。getter和setter的命名规则有一个例外:如果属性的名称以is开头,getter不会增加任何的前缀;而它的setter名称中的is会被替换成set。所以在Java中,你强调的将是isMarried()。
  将上述Java代码使用Person的代码换成Kotlin代码如下:

val person = Person("ellen",true) //调用构造方法不需要关键字“new”

//可以直接访问属性,但是调用的是getter
println(person.name)
println(person.isMarried())

现在,可以直接引用属性,不再需要调用getter。逻辑没有变化,但代码更简洁了。可变属性的setter也是这样:在Java中,使用person.setMarried(false)来表示离婚,而在Kotlin中,可以这样写person.isMarried = false 。

小贴士 对于那些在Java中定义的类,一样可以使用Kotlin的属性语法。Java类中的getter可以被当成val属性在Kotlin中访问,而一对getter/setter可以被var属性访问。例如,如果一个Java类定义了两个名称为getName和setName的方法,就把它们当作名称为name的属性访问。如果类定义了isMarried和setMarried方法,对应的Kotlin属性的就是isMarried。

大多数情况下,属性有一个对应的支持字段来保存属性的值。但是如果这个值可以即时计算---例如,根据其他属性计算---可以自定义的getter来表示。

2.2.2 自定义访问器

这一节将向你展示怎样写一个属性访问器的自定义实现。假设你声明这样一个矩形。它能判断自己是否是正方形。不需要一个单独的字段来存储这个信息(是否是正方形),因为可以随时通过检查矩形的长宽是否相等来判断:

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

属性isSquare不需要字段来保存它的值。它只有一个自定义实现的getter。它的值时每次访问属性的时候计算出来的。
  注意,不需要使用带花括号的完整语法,也可以这样写get( ) = height == width。对这个属性的调用依然不变。

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

如果在Java中访问这个属性,可以像前面提到的那样调用isSquare()的方法。
  你可能会问,声明一个没有参数的函数是否比声明带自定义getter的属性更好。两种方式几乎一样:实现和性能都没有差别,唯一的差异时可读性。通常来说,如果描述的是类的特征(属性),应该把它声明成属性。接下来我们来探索一下Kotlin的代码在磁盘中说怎样组织的。

2.2.3 Kotlin源码布局:目录和包

你知道Java把所有的类组织成包。Kotlin也有和Java相似的包的概念。每一个Kotlin文件都能以一条package语句开头,而文件中定义的所有声明(类,函数及属性)都会放在这个包中。如果其他文件中定义的声明也有相同的包,这个文件可以直接使用他们;如果包不相同,则需要导入它们。和Java一样,导入语句放在文件的最前面并使用关键字import。下面这个源码文件的例子展示了包声明和导入语句的语法。

package  geometry.shapes

import java.util.Random

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

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

}

Kotlin不区分导入的是类还是函数,而且,它允许使用import关键字导入任何种类的声明。可以直接导入顶层函数的名称。

package geometry.example

import geometry.shapes.createRandomRectangle

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

也可以在包名称后加上.来导入特定包中定义的所有声明。注意这种星号导入不仅让包中定义的类可见,也会让顶层函数和属性可见。在上述代码中用import geometry.shapes.的写法代替显式的导入也能让代码成功编译。
  在Java中,要把类放到和包结构相匹配的文件与目录结构中。例如,如果你有一个包含若干类的名为shapes的包,必须把每一个类都放在一个有着和类相同名字的单独文件中,然后把这些文件放在一个名字为shapes的目录中。图2.2展示了geometry包以及它的子包在Java中上是怎样组织的。假设createRandomRectangle函数位于另外一个单独的类RectangleUtil。

图2.2 在Java中,目录层级结构照搬了包层级结构

在Kotlin中,可以把多个类放在同一个文件中,文件的名字还可以随意选择。Kotlin也没有对磁盘上源文件的布局强加任何限制。比如,可以把包geometry.shapes所有内容都放在文件shapes.kt中,并把这个文件直接放在目录geometry中,而不需要在创建一个独立的shapes文件夹(如下图所示)

图2.3 包层级结构不需要遵循目录层级结构

example.kt --> 包geometry.example
shapes.kt --> 包geometry.example

不管怎样,大多数情况下,遵循Java的目录布局并根据包结构把源码文件放到目录中,依然是个不错的实践。在Kotlin和Java混用的项目中坚持这样的结构尤为重要,因为这样做可以让你逐步地迁移代码,而不会和一些错误不期而遇。但是你应该毫不犹豫地把多个类放进同一个文件中,特别是那些很小的类(在Kotlin中,类通常很小)。接下来,来学习Kotlin控制结构。

2.3 表示和处理选择:枚举和“when”

2.3.1 声明枚举类

Kotlin中的枚举类声明说和Java是差不多的,下面我们就来看看Kotlin中的枚举,下面来看看使用Kotlin来进行色彩枚举类的声明:

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

这是极少数Kotlin声明比Java使用了更多关键字的例子:Kotlin用了enum class两个关键字,而Java只有enum一个关键字。Kotlin中,enum是一个所谓的软关键字:只有当它出现在class前面才有特殊的意义,在其他地方可以把它当作普通的名称使用。与此不同的是,class仍然是一个关键字,要继续使用名称clazz和aClass来声明变量。
  和Java一样,枚举并不是值的列表:可以给枚举声明属性和方法。下面的代码清单展示了这种方式。

fun main(args: Array<String>) {

    println(Color.BLUE.rgb()) //输出:255

}

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
}

枚举常量用的声明构造方法和属性的语法与之前你看到的常规类一样。当你声明每个枚举常量的时候,必须提供该变量的属性值。注意这个例子向你展示Kotlin语法中唯一必须使用分号的地方:如果要在枚举类中定义任何方法,就要使用分号把枚举常量列表和方法定义分开。现在我们来看看一些在代码中处理枚举常量的超酷的方式。

2.3.2 使用“when”处理枚举类

和if相似,when是一个有返回值的表达式,因此可以写一个直接返回when表达式的表达式体函数。

fun main(args: Array<String>) {

    println(getColorString(Color.INDIGO)) //输出:INDIGO

}

fun getColorString(color:Color) = when(color){//Java中的switch在Kotlin中使用when表达式实现

      Color.RED->"RED"
      Color.ORANGE->"ORANGE"
      Color.YELLOW->"YELLOW"
      Color.GREEN->"GREEN"
      Color.BLUE->"BLUE"
      Color.INDIGO->"INDIGO"
      Color.VIOLET->"VIOLET"


}//非合并when分支

fun getColorString1(color:Color) = when(color){

    Color.RED, Color.ORANGE->"RED OR ORANGE"//合并分支
    Color.YELLOW, Color.GREEN, Color.BLUE->"YELLOW GREEN BLUE"//合并分支
    Color.INDIGO->"INDIGO"
    Color.VIOLET->"VIOLET"


}

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

}

如果你还觉得上面代码when表达式不够简洁,那么你可以通过导入枚举常量的方式来简化when表达式:

import geometry.Color
import geometry.Color.*

fun main(args: Array<String>) {
    println(getColorString(BLUE))
}

fun getColorString(color:Color) = when(color){

    RED->"RED"
    ORANGE->"ORANGE"
    YELLOW->"YELLOW"
    GREEN->"GREEN"
    BLUE->"BLUE"
    INDIGO->"INDIGO"
    VIOLET->"VIOLET"
}

2.3.3 在“when”结构中使用任意对象

Kotlin中的when结构比Java中的Switch强大很多。Switch要求必须使用常量(枚举常量,字符串或者数字字面值)作为分支条件,和它不一样,when允许使用任意对象。我们写一个函数来混合两种颜色,如果它们在我们这个小小的调色板是能够混合的。你只有很少的选项,可以简单地把所有组合列举出来。

fun mix(c1:Color,c2:Color){

    when(setOf(c1,c2)){//setof()方法返回一个set集合
        setOf(RED,YELLOW)->ORANGE
        setOf(YELLOW,BLUE)->GREEN
        setOf(BLUE,VIOLET)->INDIGO
        else->throw Exception("Dirty color")//如果没匹配其它分支,就会执行此处
    }

}


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

}

when表达式把它的实参依次和所有分支匹配,直到某个分支满足条件。这里setOf(c1,c2)被用来检查是否和分支条件相等:先和setOf(RED,YELLOW)比较,然后是其他颜色的set,一个接一个。如果没有其他的分支满足条件,else分支会执行。
  能使用任何表达式做when的分支条件,很多情况下会让你写的代码既简洁又漂亮。这个例子中,分支条件是等式检查,接下来你会看到条件还可以是任意的布尔表达式。

2.3.4 使用不带参数的“when”

你可能注意到上述代码的效率多少有些低。每次调用这个函数的时候,它都会创建一些Set实例,仅仅用来检查两种给定的颜色是否和另外两种颜色匹配。一般这不是什么大问题,但是如果这个函数调用很频繁,它就会非常值得用另外一种方式重写,来避免创建额外的垃圾对象。代码可读性会变差,但这是为了达到更好性能而必须付出的代价。

fun mix(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")
   
    }

}


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

}

如果没有给when表达式提供参数,分支条件就是任意的布尔表达式。mixOptimized函数和前面的mix函数做的事情一样。这种写法的有点就是不会创建额外的对象,但代价是它更难理解。

2.3.5 智能转换:合并类型检查和转换

你会写一个函数来作为这一小节的例子,这个函数是对象(1+2)+4这样简单的算术表达式求值。这个表达式只包含一种运算:对两个数字求和。其他的算术运算(减法,乘法,除法)都可以用相似的方式实现,可以把这些作为练习。
  首先,你会用怎样的形式编码这种表达式?把它们存储在一个树状结构中,结构中每个节点要么是一次求和(Sum)要么是一个数字(Num)。Num永远都是叶子节点,而Sum节点有两个子节点:它们是求和运算的两个参数。下面的代码展示了一种简单的类结构来表示这种表达式编码方式:一个叫作Expr的接口和它的两个实现类Num和Sum。注意Expr接口没有声明任何方法,它只是一个标记接口,用来给不同种类的表达式提供一个公共的类型。声明类的时候,使用一个冒号(:)后面跟上接口名称,来标记这个类实现了这个接口。

interface Expr
class Num(var value:Int):Expr //简单的值对象类,只有一个属性value,实现了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum运算的实参可以是任何Expr:Num或者另外一个Sum

Sum存储了Expr类型的实参left和right的引用;在这个小例子中,它们要么是Num要么是Sum。为了存储前面提到的表达式(1+2)+4,你会创建这样一个对象Sum(Sum(Num(1),Num(2)),Num(4))。下图展示了它的树状图表示法。

这里写图片描述

现在我们来看看怎样计算表达式的值。例子中表达式的运算结果应该是7。

Expr接口有两种实现,所以为了计算出表达式的结果值,得尝试两种选项:
  1.如果表达式是一个数字,直接返回它的值。
  2.如果是一次求和,得先计算左右两个表达式得值。再返回它们的和。

首先我们来看看这个函数用普通的Java方式怎么写,然后我们把它重构成Kotlin风格写法。在Java中,很可能会用一连串if语句来检查这些选项,所以我们先用Kotlin按照这种方式实现:

interface Expr
class Num(var value:Int):Expr //简单的值对象类,只有一个属性value,实现了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum运算的实参可以是任何Expr:Num或者另外一个Sum

fun eval(e:Expr):Int{

    if(e is Num){
        val n = e as Num
        return n.value
    }
    if(e is Sum){
        return eval(e.left)+ eval(e.right)
    }
    throw IllegalArgumentException("Unknown expression")

}

在Kotlin中,你要使用is检查来判断一个变量是否是某种类型。如果你曾经使用过C#写过代码,这种表示法应该不会陌生。is检查和Java中的instanceof相似。但是Java中,如果你已经检查过一个变量是某种类型并且把它当作这种类型来访问其成员时,在instanceof检查之后还需要显式地加上类型转换。如果最初的变量会使用超过一次,常常选择把类型转换的结果存储在另外一个单独的变量里。在Kotlin中,编译器帮你完成这些工作。如果你检查过一个变量是某种类型,后面就不需要再转换它,可以就把它当作你检查过的类型使用。事实上编译器为你执行了类型转换,我们把这种行为称为智能转换
  在eval函数中,在你检查过变量e是否为Num类型之后,编译器就把它当成Num类型的变量解释。于是你不需要进行显式转换就可以像这样访问Num的属性value:e.value。Sum的属性left和right也是这样:在对应的上下文中,只需要写e.left和e.right。在IDE中,这种智能转换过的值会用不同的背景颜色着重表示,这样更容易发现这个值的类型是事先检查过的,如下图所示:

这里写图片描述

智能转换只在变量经过is检查之后不再发生变化的情况下有效,当你对一个类的属性进行智能转换的时候,就像这个例子中的一样,这个属性必须是一个val属性,而且不能有自定义的访问器。否则,每次对属性的访问是否都能返回同样的值将无从验证。使用as关键字来表示特定类型的显式转换。

接下来看看怎样把eval函数重构成更符合Kotlin语言习惯的风格。

2.3.6 重构:用“when”代替“if”

Kotlin和Java中的if有什么不同,你已经看到了。本章开始的时候,你见过if表达式用在适用Java三元运算符的上下文:if(a>b)a else b和 a > b ? a : b效果一样。Kotlin没有三元运算符,因为if表达式有返回值,这一点和Java不同。这意味着你可以用表达式体语法重写eval函数,去掉return语句和花括号,使用if表达式作为函数体。

interface Expr
class Num(var value:Int):Expr //简单的值对象类,只有一个属性value,实现了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum运算的实参可以是任何Expr:Num或者另外一个Sum

fun eval(e:Expr) : Int =
        if(e is Num){
            e.value
        } else if(e is Sum){
            eval(e.left) + eval(e.right)
        }else{
            throw IllegalArgumentException("Unknown expression")
        }

如果if分支中只有一个表达式,花括号是可以省略的。如果if分支是一个代码块,代码块中的最后一个表达式会被作为结果返回。
  让我们进一步打磨代码,使用when来重写它。

interface Expr
class Num(var value:Int):Expr //简单的值对象类,只有一个属性value,实现了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum运算的实参可以是任何Expr:Num或者另外一个Sum

fun eval(e:Expr) : Int =
       when(e){
           is Num ->
                   e.value
           is Sum ->
                   eval(e.left) + eval(e.right)
           else ->
                   throw  IllegalArgumentException("Unknown expression") 
               
       }

when表达式并不仅限于检查值是否相等,那是之前你看到的。而这里使用了另外一种when分支的形式,允许你检查when实参值的类型。和上述if的例子一样,类型检查应用了一次智能转换,所以不需要额外的转换就可以访问Num和Sum的成员。
  比较最后两个Kotlin版本的eval函数,想一想你应该怎样在自己的代码中也使用when代替连串的if表达式。当分支逻辑太过复杂时,可以使用代码块作为分支体。我们来看看这种用法。

2.3.7 代码块作为“if”和“when”的分支

if和when都可以使用代码块作为分支体。这种情况下,代码块中的最后一个表达式就是结果。如果在例子函数中加入日志,可以在代码块中实现它并像之前一样返回最后的值。

interface Expr
class Num(var value:Int):Expr //简单的值对象类,只有一个属性value,实现了Expr接口
class Sum(val left:Expr,val right:Expr):Expr//Sum运算的实参可以是任何Expr:Num或者另外一个Sum

fun main(args: Array<String>) {
    println(evalWithLogging(Sum(Sum(Num(1),Num(2)),Num(4))))
}

fun evalWithLogging(e:Expr) : Int =
        when(e){
            is Num ->{
                println("num:${e.value}")
                e.value
            }
            is Sum ->{
                val left = evalWithLogging(e.left)
                val right  = evalWithLogging(e.right)
                println("Sum:$left + $right")
                left + right
            }
            else -> throw  IllegalArgumentException("Unknown expression")
        }

输出结果:

num:1
num:2
Sum:1 + 2
num:4
Sum:3 + 4
7

规则————“代码中最后的表达式就是结果”,在所有使用代码块并期望得到一个结果的地方成立。你会在文章末尾看到,同样的规则对try主体和catch子句也有效,而文章K05还会讨论该规则在lambda表达式中的应用。但是在2.2中我们提到。这个规则对常规函数不成立。一个函数要么具有不是代码块的表达式函数体,要么具有包含显式return语句的代码块函数体。
  现在熟悉了Kotlin从众多选项中做出正确选择的方式,是时候看看怎样迭代事物了。

2.4 迭代事物:“while”循环和“for”语句

在本章讨论的所有特性中,Kotlin的迭代应该是和Java最接近的。when循环和Java完全一样,本节开头会一笔带过。for循环仅以唯一一种形式存在,和Java的for-each循环一致。其写法for <item> in
<elements>和C#一样。和Java一样,循环最常见的应用就是迭代集合。我们也会探索它是怎样覆盖其他使用循环的场景的。

2.4.1 “while”循环

Kotlin有while循环和do-while循环,它们的语法和Java相应的循环没有什么区别:

while(condition){
    /*...*/
}//先判断,后执行

do{
   /*...*/
} while(condition)//先执行,后判断

Kotlin并没有给这些简单的循环带来任何新东西,所以不必停留。我们继续讨论for循环的各种用法。

2.4.2 迭代数字:区间和数列

正如我们刚刚提到的那样,在Kotlin中没有常规的Java for循环。在这种循环中,先初始化变量,在循环的每一步更新它的值,并在值满足某个限制条件时退出循环。为了替代这种最常见的循环用法,Kotlin使用了区间的概念。
  区间本质上就是两个值之间的间隔,这两个值通常是数字:一个起始值,一个结束值。使用..运算符来表示区间:

var oneToTen = 1..10

注意Kotlin的区间是包含的或者闭合的,意味着第二个值始终是区间的一部分。
  你能用整数区间做的最基本的事情就是循环迭代其中所有的值。如果你能迭代区间中所有的值,这样的区间被称作数列。
  让我们用整数迭代来玩Fizz-Buzz游戏。这是一种用来打发长途驾驶旅程的不错方式,还能帮你回忆起被遗忘的除法技巧。游戏玩家轮流递增计数,遇到能被3整数的数字就用单词fizz代替,遇到能被5整除的数字则用单词buzz代替。如果一个数字3和5的公倍数,你得说出“FizzBuzz”。
  下面的代码打印出了游戏中1到100之间所有的数字的正确答案。注意你是怎样用不带参数的when表达式来检查可能的条件的。

fun main(args: Array<String>) {

    for(i in 1..100){
        println(fizzBuzz(i))
    }

}

fun fizzBuzz(i:Int) = when{
    i % 15 == 0 ->"FizzBuzz"
    i % 3  == 0 ->"Fizz"
    i % 5  == 0 ->"Buzz"
    else -> "$i"
}

假设一个小时的驾驶之后,你已经厌倦了这些规则,想把游戏变得复杂一点,那我们可以从100开始倒着技术并且只计偶数。
  现在你在迭代一个带步长的数列,它允许跳过一些数字。步长也可以是负数,这种情况下数列是递减而不是递增的。在这个例子中,100 downTo 1是递减的数列(步长为-1)。然后step把步长的绝对值变成了2,但方向保持不变(事实上,步长被设置成了-2)。
  如前所述,..语法始终创建的是包含结束值(..右边的值)的区间。许多情况下,迭代不包括指定结束值的半闭合区间更方便。使用until函数可以创建这样的区间。例如,循环for(x in 0 until size)虽然等同于for(x in 0..size-1),但是更清晰地表达了意图。在之后的文章中,你会学习更多关于这些例子中downTo,step和until的语法。
  可以看到使用区间和数列是怎样帮助你应付FizzBuzz游戏的进阶规则的。现在让我们看看其他使用for循环的例子。

2.4.3 迭代map

我们提到了使用for...in循环的最常见的场景迭代集合。这和Java中的用法一样,所以我们不会讲太多关于它的内容。让我们来看看你可以怎样迭代map。
  作为例子,我们看看这个打印字符二进制表示的小程序。你会把这些二进制表示保存在一个map中(仅说明之用)。下面的代码创建了一个map,把某些字母的二进制填充进去,最后打印map的内容。

fun main(args: Array<String>) {

    val binaryReps = TreeMap<Char,String>() //使用TreeMap让键排序

    for(c in 'A'..'F'){ //使用字符区间迭代从A到F之间的字符
        val binary = Integer.toBinaryString(c.toInt())
        binaryReps[c] = binary //根据c把值存储到map中
    }

    for((letter,binary) in binaryReps){//迭代map,把键和值赋给两个变量
        println("$letter = $binary")
    }

}

..语法不仅可以创建数字区间,还可以创建字符区间。这里使用它迭代从A开始到F的所有字符,包括F。
  展示了for循环允许展开迭代中的集合的元素(在这个例子中,展开的是map的键值对集合)。把展开的结果存储到了两个独立的变量中:letter是键,binary是值。稍后,你将学到更多的展开语法。
  上述代码中使用了一个使用的小技巧,根据键来访问和更新map的简明语法。可以使用map[key]
读取值,并使用map[key] = value设置它们,而不需要调用get和put。下面这段代码

binaryReps[c] = binary//等价于Java版本代码“binaryReps.put(c,binary);”

可以用这样的展开语法在迭代集合的同时跟踪当前项的下标。不需要创建一个单独的变量来存储下标并手动增加它:

val list = arrayListOf("10","11","1001")
for((index,element) in list.withIndex()){ //迭代集合时使用下标
    println("$index:$element")
}

之后的文章我们会探索关于withIndex的内容。
  我能已经看过了如何使用关键字in来迭代区间或者集合,还可以用in来检查区间或者集合是否包含了某个值。

2.4.4 使用“in”检查集合和区间的成员

使用in运算来检查一个值是否在区间中,或者它的逆运算,!in,来检查这个值是否不在区间中。下面展示了如何使用in来检查一个字符是否属于一个字符区间。

//使用in来检查一个字符是否属于英文字母
fun isLetter(c:Char) = c in 'a'..'z'||c in 'A'..'Z' //方式1
fun isNotDigit(c:Char) = c  !in '0'..'9'  //方式2

这种检查字符是否是英文字母的技巧看起来很简单。在底层,没有什么特殊处理:你依然会检查字符的编码是否位于第一个字母编码和最后一个字母编码之间的某个位置。但是这个逻辑被简洁地隐藏到了标准库中的区间类实现中:

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

in运算符和!in运算符也适用于when表达式。

fun recognize(c : Char) = when(c){
    in '0'..'9' -> "c是数字字符"
    in 'a'..'z','A'..'Z' -> "c是英文字符"
    else -> "不知道c是啥字符,反正不是数字字符和英文字符"
}

区间也不仅限于字符。假如有一个支持实例比较操作的任意类(实现了java.lang.Comparable接口),就能创建这种类型的对象的区间。如果这样的区间,并不能列举出这个区间种的所有的对象。想想这种情况:例如,是否可以列举出“Java”和“Kotlin”之间所有的字符串?答案是不能。但是仍然可以使用in运算符检查一个其他的对象是否属于这个区间:

println("Kotlin" in "Java".."Scala") //结果和"Java"<="Kotlin" && "Kotlin"<="Scala"一样

注意,这里字符串是按照字母排序进行比较的,因为String就是这样实现Comparable接口的。
  in检查也同样适用于集合:

println("Kotlin" in setOf("Java","Scala")) //因为Set集合不包含“Kotlin”,输出false

在之后的文章种我们将会对我们自己的数据类型的区间和数列使用in检查学习。下面来看看Kotlin种的异常。

2.5 Kot中的异常

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( 
           "A percentage value must be between 0 and 100:$number")//throw是一个表达式

在这个例子中,如果条件满足,程序的行为是正确的,而percentage变量会用number初始化。否则,异常将会抛出,而变量也不会初始化。在之后的文章中,我们将会讨论讨论关于throw作为其他表达式的一部分的技术细节。

2.5.1 “try” “catch” 和 “finally”

和Java一样,使用带有catch和finally子句的try结构来处理异常。你会在下面这个代码中看到这个结构,这个例子从给定的文件中读取一行,尝试把它解析成一个数字,返回这个数字;或者当这一行不是一个有效数字时返回null。

fun readNumber(reader:BufferedReader):Int?{
    try{
        val line = read.readLine()
        return Integer.parseInt(line)
    }catch(e : NumberFormatException){
        return null
    }finally{
        reader.close()
    }
}

和Java最大的区别就是throws子句没有出现在代码中:如果用Java来写这个函数,你会显式地在函数声明后写上throws IOException。你需要这样做的原因是IOException是一个受检异常。在Java中,这种异常必须显式地处理。必须声明你的函数能抛出的所有受检异常。如果你调用另外一个函数,需要处理这个函数的受检异常,或者声明你的函数也能抛出这些异常。
  和其他许多现代的JVM语言一样,Kotlin并不区分受检异常和未受检异常。不用指定函数抛出的异常,而且可以处理也可以不处理异常。这种设计是基于Java中使用异常的实践做出的决定。经验显示这些Java规则常常导致许多毫无意义的重新抛出或者忽略异常的代码,而且这些规则不能总是保护你免受可能发送的错误。
  在上述代码中,NumberFormatException就不是受检异常。因此,Java编译器并不会强迫你捕获它,在运行时很容易看到这个异常发生。与此同时,BufferedReader.close可能抛出需要处理的受检异常IOExceptio。如果流关闭失败,大多数程序都不会采取什么有意义的行动,所以捕获来自close()的异常所需要的代码就是冗余的样板代码。

2.5.2 “try”作为表达式

为了了解JavaK和otlin之间另外一个显著的差异,我们修改一下这个例子。让我们去掉finally部分(因为你已经看过它是怎样工作的),并添加一些代码,用来打印从文件中读取的数字。

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

    println(number) 
}

Kotlin中的try关键字就像if和when一样,引入了一个表达式,可以把它的值赋给一个变量。不同于if,你总是需要用花括号把语句主体括起来。和其他语句一样,如果其主体包含多个表达式,那么整个try表达式的值就是最后一个表达式的值。
  这个例子将return语句放在catch代码块中,因此此函数的执行在catch代码块之后不会继续。如果你想继续执行,catch子句也需要一个值,它将是子句中最后一个表达式的值。下面展示了这是怎么回事。

fun readerNumber(reader:BufferedReader){
    val number = try{
                     Integer.parseInt(reader.readLine)//没有任何异常发生时使用这个值
                 }catch(e : NumberFormatException){
                     null //发生异常情况下使用null
                 }

    println(number) 
}

如果一个try代码块执行一切正常,代码块中最后一个表达式就是结果。如果捕获到了一个异常,相应catch代码块中最后一个表达式就是结果。在上述代码中,如果捕获了NumberForMatException,结果值就是null。

2.6 小结

  • (1)fun关键字用来声明函数。val关键字和var关键字分别用来声明只读变量和可变变量。
  • (2)字符串模板帮助你避免烦琐的字符串连接。在变量名称前面加上$前缀或者用${}包围一个表达式,来把值注入到字符串中。
  • (3)值对象类在Kotlin中以简洁的方式表示。
  • (4)熟悉的if现在是带返回值的表达式。
  • (5)when表达式类似于Java中的switch但功能更强大。
  • (6)在检查过变量具有某种类型之后不必显示地转换它的类型:编译器使用智能转换自动帮你完成
  • (7)for,while和do-while循环于Java相似,但是for循环现在更加方便,特别是当你需要迭代Map的时候,又或者迭代集合需要下标的时候。
  • (8)简明的语法1..5会创建一个区间。区间和数列允许Kotlin在for循环中使用统一的语法和同一套抽象机制,并且还可以使用in运算符和!in运算符来检查值是否属于某个区间。
  • (9)Kotlin中的异常处理和Java非常相似,除了Kotlin不要求你声明函数可以抛出的异常。

第二章的内容到这里就结束了,谢谢大家的支持,我也学习,你们也学习了,如果大家喜欢这本《Kotlin实战》中文版的内容,那就购买一本吧,笔者觉得是一本非常不错的Kotlin语言学习书籍,你可以看看笔者总结的第二章,喜欢就购买一本吧!才70元而已,知识无价。

推荐阅读更多精彩内容