Kotlin(四)—— 面向表达式编程

表达式(expressions)和语句(statements)虽然是很基本的概念,但也经常被混淆和误解。语句很容易理解,我们在一开始学习命令式编程的时候,程序往往是由一个个语句组成的。比如以下这个例子:

fun main() {
  var a = 1
  while (a < 10) {
    println(a)
    a++
  }
}

可以看到,该程序依次进行了赋值、循环控制、打印等操作,这些都可以被称为语句。

表达式可以是一个值、常量、变量、操作符、函数,或它们之间的组合,编程语言对其进行解释和计算,以求产生另一个值。通俗地理解,表达式就是可以返回值的语句。 下面来写几个表达式的例子:

1 // 单纯的字面量表达式,值为1
-1 // 增加前缀草足服,值为-1
1 + 1 // 加法操作符,返回2
listOf(1, 2, 3) // 列表表达式
"Kotlin".length // 值为6
{ 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分支也必须被考虑,因为表达式具备类型信息,最终它的类型就是ifelse多个分支类型的相同类型或公共父类型;

可以看出,基于表达式的方案让程序变得更加安全。

对于有返回值的函数,也可以直接将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.IntegerInteger是为了对基本类型int的实例进行装箱操作,Void的设计则是为了对应void。由于void表示没有返回值,所以Void并不能具有实例,它继承自Object

Unitint一样是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()

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如何利用多个表达式组合表达的能力:

  • tryKotlin中也是一个表达式,try/catch/finally语法的返回值类型由trycatch部分决 定,finally不会产生影响;
  • Kotlin中,if-else很大程度上代替了传统三元运算符的做法,虽然增加了语法词数量,但是减少了概念,同时更利于阅读;
  • if-else的返回值即try部分的返回值,最终res的值由trycatch部分决定;

虽然Kotlin没有采用三元运算符,但是它有一个很像的语法?:。注意,这里的问号和冒号必须放在一起使用,它被叫作Elvis运算符,或者null合并运算符。由于Kotlin可以用?来表示一种类型的可空性,我们可以用?:来给一种可空类型的变量指定为空情况下的值:

val maybeInt: Int? = null
println(maybeInt ?: 1) // 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关键字开始,用花括号包含多个逻辑分支,每个分支由->连接,不再需要switchbreak,由上到下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switchdefaultswitch只能传入整型或短于整型的变量作为条件,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内置的一个抽象类,像IntLongFloatDouble等与数字相关的类都是它的子类,所以这里就可以使用类型匹配来判断传入的参数到底属于什么类型。

5 for循环和范围表达式

5.1 for循环

Java中,经常在for加上一个分号语句块来构建一个循环体,如:

for (int i = 0; i < 10; i++) {
  System.out.println(i);
}

Kotlinfor循环方面做了很大幅度的修改,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) // 13579

如果是倒序,可以用downTo方法来实现:

for (i in 10 downTo 1 step 2) print(i) // 108642

此外,还有一个until函数来实现一个半开区间:

for (i in 1 until 10) print(i) // 123456789
5.3 用in来检查成员关系

Kotlin中我们可以用in关键字来对检查一个元素是否是一个区间或集合中的成员:

"a" in listOf("b", "c") // false
"a" !in listOf("b", "c") // true

除了等和不等,in还可以结合范围表达式来表示更多的含义:

"kot" in "abc".."xyz" // true
// 等价于
"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")
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值