表达式(expressions )和语句(statements )虽然是很基本的概念,但也经常被混淆和误解。语句很容易理解,我们在一开始学习命令式编程的时候,程序往往是由一个个语句组成的。比如以下这个例子:
fun main() {
var a = 1
while (a < 10) {
println(a)
a++
}
}
可以看到,该程序依次进行了赋值、循环控制、打印等操作,这些都可以被称为语句。
表达式可以是一个值、常量、变量、操作符、函数,或它们之间的组合,编程语言对其进行解释和计算,以求产生另一个值。通俗地理解,表达式就是可以返回值的语句。 下面来写几个表达式的例子:
1
-1
1 + 1
listOf(1, 2, 3)
"Kotlin".length
{ x: Int -> x + 1}
fun(x: Int) { println(x) }
if (x > 1) x else 1
1 表达式比语句更安全
先来看一段Java 代码:
void ifStatement(Boolean flag) {
String a = null;
if (flag) {
a = "dive into kotlin";
}
System.out.println(a.toUpperCase());
}
由于if 在这里不是一个表达式,所以只能够在外部对变量a 进行声明。这段代码存在潜在的问题:
a 必须在if 语句外部声明,它被初始化为null 。这里的if 语句的作用就是对a 进行赋值, 这是一个副作用。在这个例子中,我们忽略了else 分支,如果flag 的条件判断永远为true ,那么程序运行并不会出错;否则,将会出现java.lang.NullPointerException 的错误,即使程序依旧会编译通过。因此,这种通过语句创建副作用的方式很容易引发bug ;- 现在的逻辑虽然简单,然而如果变量
a 来自上下文其他更远的地方,那么这种危险会更加容易被忽视。典型的例子就是一段并发控制的程序,业务开发会变得非常不安全;
Kotlin 中的if 语句相比于Java 有一个额外的功能,它是可以有返回值的,返回值就是if 语句每 一个条件中最后一行代码的返回值。 因此,上述代码就可以简化成如下形式:
fun isExpression(flag: Boolean) {
val a = if (flag) "dive into kotlin" else ""
println(a.toUpperCase())
}
- 赋值语句与
if 表达式混合使用,就不存在变量a 没有初始值的情况; - 在
if 作为表达式时,else 分支也必须被考虑,因为表达式具备类型信息,最终它的类型就是if 、else 多个分支类型的相同类型或公共父类型;
可以看出,基于表达式的方案让程序变得更加安全。
对于有返回值的函数,也可以直接将if 语句返回:
fun largerNumber(num1: Int, num2: Int): Int {
return if (num1 > num2) {
num1
} else {
num2
}
}
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) {
num1
} else {
num2
}
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2
2 Unit 类型:让函数调用皆为表达式
之所以不能说Java 中的函数调用皆是表达式,是因为存在特例void 。在Java 中如果声明的函数没有返回值,那么它就需要用void 来修饰。如:
void foo () {
System.out.println("return nothing")
}
所以foo() 就不具有值和类型信息,它就不能算作一个表达式。在Kotlin 中,函数在所有的情况下都具有返回类型,所以它们引入了Unit 来替代Java 中的void 关键字。
在Java 在语言层设计一个Void 类(注意大小写),java.lang.Void 类似java.lang.Integer ,Integer 是为了对基本类型int 的实例进行装箱操作,Void 的设计则是为了对应void 。由于void 表示没有返回值,所以Void 并不能具有实例,它继承自Object 。
Unit 与int 一样是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为() 。
Kotlin 为什么要引入Unit 呢?一个很大的原因是函数式编程侧重于组合,尤其是很多高阶函数,在源码实现的时候都是采用泛型来实现的。然而void 在涉及泛型的情况下会存在问题。
先来看个例子,Java 这门语言并不天然支持函数是头等公民,现在来尝试模拟出一种函数类型:
interface Function<Arg, Return> {
Return apply(Arg arg);
}
Function<String, Integer> stringLength = new Function<String, Integer>() {
public Integer apply(String arg) {
return arg.length();
}
};
int result = stringLength.apply("hello");
看上去似乎没什么问题。如果此时希望重新实现一个print 方法。问题来了,Return 的类型用什么来表示呢?可能你会想到void ,但Java 中是不能这么做的,只能把Return 换成Void ,即Function<String,Void> ,由于Void 没有实例,则返回一个null 。这种做法严格意义上讲,相当丑陋。
所以,最好的解决办法就是引入一个单例类型Unit ,除了不代表任何意义的以外,它与其他常规类型并没有什么差别。
3 复合表达式
相比语句而言,表达式更倾向于自成一块,避免与上下文共享状态,互相依赖,因此可以说它具备更好的隔离性。隔离性意味着杜绝了副作用,因此我们用表达式描述逻辑可以更加安全。此外,表达式通常也具有更好的表达能力。
典型的一个例子就是表达式更容易进行组合。由于每个表达式都具有值,并且也可以将另一个表达式作为组成其自身的一部分,所以我们可以写出一个复合的表达式。举个例子:
val res: Int? = try {
if (result.success) {
jsonDecode(result.response)
} else null
} catch (e: JsonDecodeException) {
null
}
这个程序描述了获取一个HTTP 响应结果,然后进行json 解码,最终赋值给res 变量的过程。它向我们展示了Kotlin 如何利用多个表达式组合表达的能力:
try 在Kotlin 中也是一个表达式,try/catch/finally 语法的返回值类型由try 或catch 部分决 定,finally 不会产生影响;- 在
Kotlin 中,if-else 很大程度上代替了传统三元运算符的做法,虽然增加了语法词数量,但是减少了概念,同时更利于阅读; if-else 的返回值即try 部分的返回值,最终res 的值由try 或catch 部分决定;
虽然Kotlin 没有采用三元运算符,但是它有一个很像的语法?: 。注意,这里的问号和冒号必须放在一起使用,它被叫作Elvis 运算符,或者null 合并运算符。由于Kotlin 可以用? 来表示一种类型的可空性,我们可以用?: 来给一种可空类型的变量指定为空情况下的值:
val maybeInt: Int? = null
println(maybeInt ?: 1)
4 枚举类和when 表达式
4.1 枚举类
在Kotlin 中,枚举是通过一个枚举类来实现的:
enum class Day {
MON, TUE, WEN, THU, FRI, SAT, SUN
}
与Java 中的enum 语法大体相似,只是多了一个class 关键词,表示它是一个枚举类。不过Kotlin 中的枚举类没那么简单,由于它是一种类,我们可以猜测它自然应该可以拥有构造参数,以及定义额外的属性和方法:
enum class DayOfWeek(val day: Int) {
MON(1), TUE(2), WEN(3), THU(4), FRI(5), SAT(6), SUN(7);
fun getDayNumber(): Int {
return day
}
}
需要注意的是,当在枚举类中存在额外的方法或属性定义,则必须强制加上分号。
4.2 用when 来代替if-else
在了解如何声明一个枚举类后,用它设计一个业务逻辑。比如,Shaw给新一周的几天计划了不同的活动,安排如下:周六打篮球;周日钓鱼;星期五晚上约会;平日里如果天晴就去图书馆看书,不然就在寝室学习。代码如下所示:
fun schedule(day: Day, sunny: Boolean) = {
if (day == Day.SAT) {
basketball()
} else if (day == Day.SUN) {
fishing()
} else if (day == Day.FRI) {
appointment()
} else {
if (sunny) {
library()
} else {
study()
}
}
}
因为存在不少if-else 分支,代码显得不够优雅。更好的改进方法就是用when 表达式来优化:
fun schedule(day: Day, sunny: Boolean) = when (day) {
Day.SAT -> basketball()
Day.SUN -> fishing()
Day.FRI -> appointment()
else -> when {
sunny -> library()
else -> study()
}
}
根据上述这段代码来分析下when 表达式的具体语法:
- 一个完整的
when 表达式类似switch 语句,由when 关键字开始,用花括号包含多个逻辑分支,每个分支由-> 连接,不再需要switch 的break ,由上到下匹配,一直匹配完为止,否则执行else 分支的逻辑,类似switch 的default ;(switch 只能传入整型或短于整型的变量作为条件,JDK 1.7 之后增加了对字符串变量的支持,但如果判断逻辑使用的并非是上述几种类型的变量,就无法使用switch ) - 每个逻辑分支具有返回值,最终整个
when 表达式的返回类型就是所有分支相同的返回类型,或公共的父类型。 在上面的例子中,假设所有活动函数的返回值为Unit ,那么 编译器就会自动推导出when 表达式的类型,即Unit 。以下是一个非Unit 的例子:
fun foo(a: Int) = when (a) {
1 -> 1
2 -> 2
else -> 0
}
when 关键字的参数可以省略,该情况下,分支-> 的左侧部分需返回布尔值,否则编译会报错, 如上述的子when 表达式可改成:
when {
sunny -> library()
else -> study()
}
- 表达式可以组合,所以这是一个典型的
when 表达式组合的例子。在Java 中很少见过这么长的表达式,但是这在Kotlin 中很常见。
如果这样嵌套子when 表达式,层次依旧比较深。when 表达式是很灵活的,可以通过如下修改来解决这个问题:
fun schedule(day: Day, sunny: Boolean) = when {
day == Day.SAT -> basketball()
day == Day.SUN -> fishing()
day == Day.FRI -> appointment()
sunny -> library()
else -> study()
}
除了精确匹配之外,when 语句还允许进行类型匹配:
fun checkNumber(num: Number) {
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number is not support")
}
}
上述代码中,is 关键字就是类型匹配的核心,它相当于Java 中的instanceof 关键字。 由于checkNumber() 函数接收一个Number 类型的参数,这是Kotlin 内置的一个抽象类,像Int 、Long 、Float 、Double 等与数字相关的类都是它的子类,所以这里就可以使用类型匹配来判断传入的参数到底属于什么类型。
5 for 循环和范围表达式
5.1 for 循环
在Java 中,经常在for 加上一个分号语句块来构建一个循环体,如:
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
Kotlin 在for 循环方面做了很大幅度的修改,Java 中最常用的for-i 循环在Kotlin 中直接被舍弃 了,而Java 中另一种for-each 循环则被Kotlin 进行了大幅度的加强,变成了for-in 循环,上述的代码等价表达为:
for (i in 1..10) println(i)
如果把上述的例子带上花括号和变量i 的类型声明,也是支持的:
for (i: Int in 1..10) print(i)
5.2 范围表达式
1..10 这种语法是在Kotlin 中的范围表达式(range ),它表示创建了一个[1, 10] 的区间。在Kotlin 官网的文档介绍中:Range 表达式是通过rangeTo 函数实现的,通过.. 操作符与某种类型的对象组成,除了整型的基本类型之外,该类型需实现java.lang.Comparable 接口。 举个例子,由于String 类实现了Comparable 接口,字符串值之间可以比较大小,所以就可以创建一个字符串区间,如:
"abc".."xyz"
以下是String 的源码:
public class String : Comparable<String>, CharSequence {
@FastNative
public native int compareTo(String anotherString);
}
字符串的大小根据首字母在字母表中的排序进行比较,如果首字母相同,则从左往右获取下一个字母,以此类推。
另外,当对整数进行for 循环时,Kotlin 还提供了一个step 函数来跳过区间内的元素:
for (i in 1..10 step 2) print(i)
如果是倒序,可以用downTo 方法来实现:
for (i in 10 downTo 1 step 2) print(i)
此外,还有一个until 函数来实现一个半开区间:
for (i in 1 until 10) print(i)
5.3 用in 来检查成员关系
在Kotlin 中我们可以用in 关键字来对检查一个元素是否是一个区间或集合中的成员:
"a" in listOf("b", "c")
"a" !in listOf("b", "c")
除了等和不等,in 还可以结合范围表达式来表示更多的含义:
"kot" in "abc".."xyz"
"kot" >= "abc" && "kot" <= "xyz"
事实上,任何提供迭代器(iterator )的结构都可以用for 语句进行迭代:
for (c in array) print(c)
此外,还可以通过调用一个withIndex 方法,提供一个键值元组:
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
|