Kotlin学习(十):其他Kotlin技术
数据解构
数据解构,就是将对象中的数据解析成隔壁那个相应的独立变量,也就是脱离原来的对象而存在。
var (name, age) = user
在这行代码中,将user 对象的 name 和 age 属性解构了出来,分别赋给了 name 和 age 变量。如果要这样实现,那么 user 对象需要是数据类的实例。
data class User(var name: String, var age: Int)
这行代码是一个 User 数据类,该数据类有2个参数,下main的代码要将这2个参数对应的属性值赋给相应的2个变量。
var user = User("Mike", 23)
var (name, age) = user
println("name = $name , age = $age") // 输出结果:name = Mike , age = 23
如果要想让一个函数返回多个值,并能解构这些值,也需要返回数据类对象。
fun printUser(): User {
return User("Jack", 20)
}
fun main(args: Array<String>) {
var (name, age) = printUser()
println("name = $name , age = $age") // 输出结果:name = Jack , age = 20
}
有很多对象,可以保存一组值,井且可以通过 for...in 语旬,将这些值解构出来。例如,Map 对象就是这样。下面的代码创建了一个 MutableMap 对象,井保存了两个 key-value 值对,然后使用 for...in 语句将其解构出来。
fun main(args: Array<String>) {
var map = mutableMapOf<Int, String>()
map.put(10, "Bill")
map.put(20, "Jack")
map.put(30, "Mike")
for ((key, value) in map) {
println(" key = [ $key ] , value = [ $value ]")
}
}
输出结果:
key = [ 10 ] , value = [ Bill ]
key = [ 20 ] , value = [ Jack ]
key = [ 30 ] , value = [ Mike ]
集合
尽管 Kotlin 可以使用 JDK 提供的集合,但 Kotlin 标准库仍然提供了自己的集合 API 。与 Java 不同的是,Kotlin 标准库将集合分为可修改的和不可修改的。在 Kotlin 中,不可修改的集合 API 包括 List、 Set、 Map 等;可修改集合的 API包括 MutableList、 MutableSet、 MutableMap 等,也就是前面需要加 Mutable 前缀。这些 API都是接口,而且它们都是 Collection 的子接口。
Kotlin 之所以这样设计,是为了尽可能避免漏洞 。因为集合变量在声明时就确定是否为只读或读写的,所以可以避免在操作过程中向集合中误写入数据。由于 Byte Code 并没有只读集合的指令,因此 Kotlin 通过语法糖来实现只读集合。也许许多读者还记得前面讲过的用来声明泛型的 out 关键字。如果泛型用 out 声明,那么该泛型只能用于读操作。因此,前面提到的不可修改的集合 API(List、Set、Map),在定义时都使用 out 声明泛型。
// List 接口
public interface List<out E> : Collection<E> {
...
}
// Set 接口
public interface Set<out E> : Collection<E> {
...
}
// Map 接口
public interface Map<K, out V> {
...
}
很显然,这3段代码都使用了 out 声明泛型,其中 Map 只使用 out 声明了 V , K 表示 Map 的 key 。
下面是一些集合常用 的方式。
val numbers:MutableList<Int> = mutableListOf<Int>(1,2,3,4) // 创建可读写的列表对象
val readOnlyViews:List<Int> = numbers // 将读写列表变成只读列表
println(numbers) // 输出结果: [1,2,3]
numbers.add(4) // 向 numbers 添加一个新元素
println(readOnlyViews) // 输出结果:[1,2,3,4]
readOnlyViews.clear() // 编译错误,没有 clear() 函数
val strings = hashSetOf("a","b",'c','d')
println(strings) // 输出结果:[a, b, c, d]
从这段代码可以看出,集合类并没有提供构造器创建集合对象,而是提供了一些函数来创建集合对象。下面是一些常用的创建集合对象的函数。
listOf:用于创建List对象。setOf:用于创建Set对象。mapOf:用于创建Map对象。mutableListOf:用于创建MutableList对象。mutableSetOf:用于创建MutableSet对象。mutableMapOf:用于创建MutableMap对象。
下面是上述部分函数的应用案例。
// 创建 List 对象
var items = listOf(1, 2, 3, 4, 5)
// 创建 MutableList 对象
var mutableList = mutableListOf(4, 5, 6)
// 创建 Map 对象
var map = mapOf<String, Int>(Pair ("Jack", 20), Pair("Mike", 30))
定义泛型时,如果使用 out ,那么 List<String> 会被认为是 List<Any>的子类型。 根据这个规则, List、 Set、 Map 都符合,而 MutableList、 MutableSet、 MutableMap由于未使用 out 定义泛型,因此并不符合这个规则。
对于可读写的集合,可以通过 toXxx 函数将其转换为只读的版本,其中 Xxx 是List、 Set 和 Map 。
值范围
值范围的应用
值范围表达式使用 rangeTo 函数实现,该函数的操作符形式是两个点 (..) ,另外还有两个相关操作符 in 和 !in 。任何可比较大小的数据类型都可以定义值范围,但对于整数基本类型,值范围的实现进行了特殊的优化。下面是使用示例。
var n = 20
if (n in 1..100) { // 相当于java代码 if(n >= 1 && n <= 10)
println("满足要求")
}
if (n !in 30..80) { // 相当于java代码 if(n <30 || n >80)
println("不满足要求")
}
整数的值范围还有一种额外的功能,就是可以对这些值范围进行遍历。编译器会负责将这些代码变换为 Java 中基于下标的 for 循环,不会产生不必要的性能损耗。
for (i in 1..10) { //相当于Java 中的 for (int i = l; i <= 10; i++)
println("$i*$i = ${i * i}")
}
输出结果:
1 * 1 = 1
2 * 2 = 4
3 * 3 = 9
4 * 4 = 16
5 * 5 = 25
6 * 6 = 36
7 * 7 = 49
8 * 8 = 64
9 * 9 = 81
10 * 10 = 100
如果要按倒序输出,该怎么办呢?需要使用标准库中的 downTo 函数。
for (i in 10 downTo 1) {
println("$i*$i = ${i * i}")
}
在前面的代码中, i 是顺序加 1 或减 1 的,也就是步长为 1 。如果要修改步长,可以使用 step 函数。
for (i in 1..10 step 2) { // 顺序修改步长
println("$i*$i = ${i * i}")
}
for (i in 10 downTo 1 step 3) { // 倒序修改步长
println("$i*$i = ${i * i}")
}
在前面的代码中,使用的范围都是闭区间。例如,1..10 表示 1 <= i <= 10 。如果要表示 1 <= i < 10 ,需要使用 unitil函数,代码如下:
for (i in 1 until 10 ){ // i in [1,10),不包含10
println("$i*$i = ${i * i}")
}
输出结果:
1 * 1 = 1
2 * 2 = 4
3 * 3 = 9
4 * 4 = 16
5 * 5 = 25
6 * 6 = 36
7 * 7 = 49
8 * 8 = 64
9 * 9 = 81
值范围工作原理
值范围实现了标准库中的一个共通接口 ClosedRange<T> 。 ClosedRange<T> 表示数学上的一个闭区间 (closed interval) ,由可比较大小的数据类型 (comparable type ) 构成。这个区间包括两个端点: start 和 endlnclusive ,这两个端点的值都包含在值范围内。主要的操作是 contains ,主要通过 in/!in 操作符的形式来调用。
整数类型的数列 (IntProgression、LongProgression、CharProgression) 代表算术上的一个整数数列。数列由 first 元素、 last 元素,以及一个非0的 increment 来定义。第一个元素就是 first,后续的所有元素等于前一个元素加上 increment 。除非数列为空,否则遍历数列时一定会到达 last 元素。
数列是 Iterable<N>的子类型,这里的 N 分别代表 Int 、 Long、 Char ,因此数列可以用在 for 循环内,还可以用于 map 函数、 filter 函数等。在 Progression 上的遍历等价于 Java/JavaScript中基于下标的 for 循环。
for (int i = first; i != last; i += increment) {
...
}
对于整数类型,范围操作符 (..) 创建一个实现了 ClosedRange<T> 和 *Progression 接口的对象。例如, IntRange 实现了 ClosedRange<Int> ,并继承 IntProgression 类,因此 IntProgression上定义的所有操作对于 IntRange 都有效。 downTo 和 step 函数的结果永远是一个 *Progression 。
要构造一个数列,可以使用对应的类的同伴对象中定义的 fromClosedRange 函数。
IntProgression.romClosedRange(rangeStart: Int, rangeEnd: Int, step: Int)
常用工具函数
- **rangeTo:**整数类型上定义的
rangeTo操作符,只是简单地调用*Range类的构造器。浮点数值(Double、·Float )没有定义自己的rangeTo操作符,而是使用标准库为共同的Comparable类型提供的操作符。 - downTo: 反向迭代数字
- reversed : 返回相反的数列
- step: 改变步长
类型检查与类型转换
is 与 !is 操作符
我们可以使用 is操作符,在运行时检查对象与给定的类型是否一致,或者使用与它相反的 !is 操作符。
var obj: Any = 456
if (obj is String) { // 判断 obj 是否为 String 类型
println("obj 是字符串")
}
if (obj is Int) { // 判断 obj 是否为 Int 类型
println("obj 是 Int 类型")
}
if (obj !is Int) {
println("obj 不是 Int 类型")
}
// 输出结果:obj 是 Int 类型
智能类型转换
很多情况下,在 Kotlin 中你不必使用显式的类型转换操作,因为编译器会对不可变的值追踪 is 检查,然后在需要的时候自动插入(安全的)类型转换。
fun demo(x: Any) {
if (x is String) {
print("x = $x , x.length = ${x.length}") // x 自动转换为字符串
}
}
fun main(args: Array<String>) {
var str="hello"
demo(str)
}
// 输出结果: x = hello , x.length = 5
如果一个相反的类型检查导致了 return ,此时编译器足够智能,可以判断出转换处理是安全的。
if (x !is String) return
print(x.length) // x 自动转换为字符串
或者在 && 和 || 的右侧:
// `||` 右侧的 x 自动转换为字符串
if (x !is String || x.length == 0) return
// `&&` 右侧的 x 自动转换为字符串
if (x is String && x.length > 0) {
print(x.length) // x 自动转换为字符串
}
这种智能类型转换对于 when 表达式和 while 循环同样有效。
var x:Any = "abc"
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}
请注意,当编译器不能保证变量在检测和使用之间不可改变时,智能转换不能用。 更具体地,智能转换能否适用根据以下规则:
- val 局部变量——总是可以,局部委托属性除外;
- val 属性——如果属性是 private 或 internal,或者该检测在声明属性的同一模块中执行。智能转换不适用于 open 的属性或者具有自定义 getter 的属性;
- var 局部变量——如果变量在检测和使用之间没有修改、没有在会修改它的 lambda 中捕获、并且不是局部委托属性;
- var 属性——决不可能(因为该变量可以随时被其他代码修改)。
强行类型转换
如果类型强制转换,而且类型不兼容,类型转换操作符通常会抛出一个异常。因此,我们称之为不安全的。在 Kotlin 中,不安全的类型转换使用中缀操作符 as 。
val x = "hello"
val y: Int = x as Int
// 编译出错:Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
注意,null 不能转换为 String 因该类型不是可空的, 即如果 x 为空,上面的代码会抛出一个异常。 为了让这样的代码用于可空值,请在类型转换的右侧使用可空类型:
val x: Any? = "hello"
val y: Int? = x as Int?
为了避免抛出异常,我们可以使用安全的类型转换操作符 “ as? ” ,当类型转换失败时,它会返回 null。
val x: Any? = "hello"
val y: Int? = x as? Int
println(y) // 输出结果:null
注意,尽管事实上 as? 的右边是一个非空类型的 Int,但是其转换的结果是可空的。
this 表达式
为了表示当前的 接收者 我们使用 this 表达式:
- 在类的成员中,this 指的是该类的当前对象。
- 在扩展函数或者带有接收者的函数字面值中, this 表示在点左侧传递的 接收者 参数。
如果 this 没有限定符,它指的是最内层的包含它的作用域。要引用其他作用域中的 this,请使用 标签限定符 。
为了访问更外层范围(如类、扩展函数或有标签的带接收者的函数字面值〉内的 this ,我们使用 this@label , 其中的 @label 是个标签,代表我们想要访问的 this 所属的范围。
class A { // 隐式标签 @A
inner class B { // 隐式标签 @B
fun Int.foo() { // 隐式标签 @foo
val a = this@A // A 的 this
val b = this@B // B 的 this
val c = this // foo() 的接收者,一个 Int
val c1 = this@foo // foo() 的接收者,一个 Int
val funLit = lambda@ fun String.() {
val d = this // funLit 的接收者
}
val funLit2 = { s: String ->
// foo() 的接收者,因为它包含的 lambda 表达式
// 没有任何接收者
val d1 = this
}
}
}
}
相等性
Kotlin 中有两种类型的相等性:
- 结构相等(用
equals()检测); - 引用相等(两个引用指向同一对象)。
引用相等由 ===(以及其否定形式 !==)操作判断。a === b 当且仅当 a 与 b 指向同一个对象时求值为 true。对于运行时表示为原生类型的值 (例如 Int),=== 相等检测等价于 == 检测。
结构相等由 ==(以及其否定形式 !=)操作判断。按照惯例,像 a == b 这样的表达式会翻译成:
a?.equals(b) ?: (b === null)
也就是说如果 a 不是 null 则调用 equals(Any?) 函数,否则(即 a 是 null)检测 b 是否与 null 引用相等。
操作符重载
Kotlin 允许我们对数据类型的一组预定义的操作符提供实现函数。这些操作符的表达符号是固定的(如 + 或 * ),优先顺序也是固定的。要实现这些操作符,我们需要对相应的数据类型实现一个固定名称的成员函数或扩展函数,这里所谓“相应的数据类型”,对于二元操作符,是指左侧操作数的类型,对于一元操作符,是指唯一一个操作数的类型。用于实现操作符重载的函数应该使用 operator 修饰符进行标记。
一元操作符重载
一元操作符与对应的函数关系如下所示:
| 表达式 | 对应的函数 |
|---|---|
| +a | a.unaryPlus() |
| -a | a.unaryMinus() |
| !a | a.not() |
| a++ | a.inc() |
| a-- | a.dec() |
这个表是说,当编译器处理例如表达式 +a 时,它执行以下步骤:
- 确定
a的类型,假设为T; - 为接收者
T查找一个带有operator修饰符的无参函数unaryPlus(),即成员函数或扩展函数; - 如果函数不存在或不明确,则导致编译错误;
- 如果函数存在且其返回类型为
R,那就表达式+a具有类型R;
注意 这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引入函数调用的开销。
以下是如何重载一元运算符的示例:
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y)
val point = Point(10, 20)
fun main() {
println(-point) // 输出“Point(x=-10, y=-20)”
}
表中的 a.inc 和 a.dec 应该改变它们的接收者,并且返回一个值 ( 可选 ) 。inc()/dec() 不应该改变接收者对象的值。这里所谓“改变它们的接收者”,我们指的是改变接收者变量,而不是改变接收者对象的值。
对于后缀形式操作符,如 a++,编译器解析时将执行以下步骤。
- 确定
a的类型,令其为T; - 查找一个适用于类型为
T的接收者的、带有operator修饰符的无参数函数inc(); - 检测函数的返回类型是
T的子类型。
计算表达式的步骤是:
- 把
a的初始值存储到临时存储a0中; - 把
a0.inc()结果赋值给a; - 把
a0作为表达式的结果返回。
对于 a--,步骤是完全类似的。
对于前缀形式 ++a 和 --a 以相同方式解析,其步骤是:
- 把
a.inc()结果赋值给a; - 把
a的新值作为表达式结果返回。
二元操作
算术运算符
| 表达式 | 对应的函数 |
|---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b)、 a.mod(b) (已弃用) |
a..b | a.rangeTo(b) |
对于此表中的操作,编译器只是解析成翻译为列中的表达式。
请注意,自 Kotlin 1.1 起支持 rem 运算符。Kotlin 1.0 使用 mod 运算符,它在 Kotlin 1.1 中被弃用。
下面是一个从给定值起始的 Counter 类的示例,它可以使用重载的 + 运算符来增加计数:
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}
“In”操作符
| 表达式 | 对应的函数 |
|---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
对于 in 和 !in,过程是相同的,但是参数的顺序是相反的。
索引访问操作符
| 表达式 | 对应的函数 |
|---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ……, i_n] | a.get(i_1, ……, i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ……, i_n] = b | a.set(i_1, ……, i_n, b) |
方括号转换为调用带有适当数量参数的 get 和 set。
调用操作符
| 表达式 | 对应的函数 |
|---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ……, i_n) | a.invoke(i_1, ……, i_n) |
圆括号转换为调用带有适当数量参数的 invoke。
广义赋值
| 表达式 | 对应的函数 |
|---|---|
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b), a.modAssign(b)(已弃用) |
对于赋值操作,例如 a += b,编译器执行以下步骤:
- 如果右列的函数可用
- 如果相应的二元函数(即
plusAssign()对应于plus())也可用,那么报告错误(模糊), - 确保其返回类型是
Unit,否则报告错误, - 生成
a.plusAssign(b)的代码;
- 如果相应的二元函数(即
- 否则试着生成
a = a + b的代码(这里包含类型检测:a + b的类型必须是a的子类型)。
注意:赋值在 Kotlin 中不是表达式。
相等与不等操作符
| 表达式 | 对应的函数 |
|---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
这些操作符只使用函数 equals(other: Any?): Boolean,可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Foo))。
注意:=== 和 !==(同一性检测)不可重载,因此不存在对他们的约定。
这个 == 操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null 值。 null == null 总是 true,对于非空的 x,x == null 总是 false 而不会调用 x.equals()。
比较操作符
| 表达式 | 对应的函数 |
|---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
所有的比较都转换为对 compareTo 的调用,这个函数需要返回 Int 值
空安全
可空类型与非空类型
Kotlin 的类型系统旨在消除来自代码空引用的危险,也称为《十亿美元的错误》。
许多编程语言(包括 Java)中最常见的陷阱之一,就是访问空引用的成员会导致空引用异常。在 Java 中,这等同于 NullPointerException 或简称 NPE。
Kotlin 的类型系统旨在从我们的代码中消除 NullPointerException。NPE 的唯一可能的原因可能是:
- 显式调用
throw NullPointerException(); - 使用了下文描述的
!!操作符; - 有些数据在初始化时不一致,例如当:
- 传递一个在构造函数中出现的未初始化的 this 并用于其他地方(“泄漏 this”);
- 超类的构造函数调用一个开放成员,该成员在派生中类的实现使用了未初始化的状态;
- Java 互操作:
- 企图访问平台类型的
null引用的成员; - 用于具有错误可空性的 Java 互操作的泛型类型,例如一段 Java 代码可能会向 Kotlin 的
MutableList<String>中加入null,这意味着应该使用MutableList<String?>来处理它; - 由外部 Java 代码引发的其他问题。
- 企图访问平台类型的
在 Kotlin 中,类型系统区分一个引用可以容纳 null (可空引用)还是不能容纳(非空引用)。 例如,String 类型的常规变量不能容纳 null:
var a: String = null // 编译错误,a 不可为null
var b: String = "abc" // 默认情况下,常规初始化意味着非空
b = null // 编译错误
如果要允许为空,我们可以声明一个变量为可空字符串,写作 String?:
var a: String = "xyz"
var b: String? = "abc" // 可以设置为空
b = null // ok
print(b)
现在,如果你调用 a 的方法或者访问它的属性,它保证不会导致 NPE,这样你就可以放心地使用:
val l = a.length
但是如果你想访问 b 的同一个属性,那么这是不安全的,并且编译器会报告一个错误:
val l = b.length // 错误:变量“b”可能为空
但是我们还是需要访问该属性,对吧?有几种方式可以做到。
在条件中检测 null
首先,你可以显式检测 b 是否为 null,并分别处理两种可能:
val l = if (b != null) b.length else -1
编译器会跟踪所执行检测的信息,并允许你在 if 内部调用 length。 同时,也支持更复杂(更智能)的条件:
val b: String? = "Kotlin"
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}
请注意,这只适用于 b 是不可变的情况(即在检测和使用之间没有修改过的局部变量 ,或者不可覆盖并且有幕后字段的 val 成员),因为否则可能会发生在检测之后 b 又变为 null 的情况。
安全的调用
你的第二个选择是安全调用操作符,写作 ?.:
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // 无需安全调用
如果 b 非空,就返回 b.length,否则返回 null,这个表达式的类型是 Int?。
安全调用在链式调用中很有用。例如,如果一个员工 Bob 可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的话)的名字,我们写作:
bob?.department?.head?.name
如果任意一个属性(环节)为空,这个链式调用就会返回 null。
如果要只对非空值执行某个操作,安全调用操作符可以与 let一起使用:
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
item?.let { println(it) } // 输出 Kotlin 并忽略 null
}
安全调用也可以出现在赋值的左侧。这样,如果调用链中的任何一个接收者为空都会跳过赋值,而右侧的表达式根本不会求值:
// 如果 `person` 或者 `person.department` 其中之一为空,都不会调用该函数:
person?.department?.head = managersPool.getManager()
Elvis 操作符
当我们有一个可空的引用 b 时,我们可以说“如果 b 非空,我使用它;否则使用某个非空的值”:
val l: Int = if (b != null) b.length else -1
除了完整的 if-表达式,这还可以通过 Elvis 操作符表达,写作 ?::
val l = b?.length ?: -1
如果 ?: 左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。 请注意,当且仅当左侧为空时,才会对右侧表达式求值。
请注意,因为 throw 和 return 在 Kotlin 中都是表达式,所以它们也可以用在 elvis 操作符右侧。这可能会非常方便,例如,检测函数参数:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ……
}
!! 操作符
第三种选择是为 NPE 爱好者准备的:非空断言运算符(!!)将任何值转换为非空类型,若该值为空则抛出异常。我们可以写 b!! ,这会返回一个非空的 b 值 (例如:在我们例子中的 String)或者如果 b 为空,就会抛出一个 NPE 异常:
val l = b!!.length
因此,如果你想要一个 NPE ,你可以得到它,但是你必须显式要求它,否则它不会不期而至。
安全的类型转换
如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null:
val aInt: Int? = a as? Int
可空类型的集合
如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull 来实现:
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
异常
异常类
Kotlin 中所有异常类都是 Throwable 类的子类。 每个异常都有消息、堆栈回溯信息以及可选的错误原因。
使用 throw-表达式来抛出异常。
throw Exception("Hi There!")
使用 try-表达式来捕获异常:
try {
// 一些代码
}catch (e: SomeException) {
// 处理程序
}finally {
// 可选的 finally 块
}
可以有零到多个 catch 块。finally 块可以省略。 但是 catch 与 finally 块至少应该存在一个。
Try 是一个表达式
try 是一个表达式,即它可以有一个返回值:
val a: Int? = try {
parseInt(input)
} catch (e: NumberFormatException) {
null
}
try-表达式的返回值是 try 块中的最后一个表达式或者是(所有)catch 块中的最后一个表达式。 finally 块中的内容不会影响表达式的结果。
受检的异常
Kotlin 没有受检的异常。这其中有很多原因,但我们会提供一个简单的例子。
以下是 JDK 中 StringBuilder 类实现的一个示例接口:
Appendable append(CharSequence csq) throws IOException;
这个签名是什么意思? 它是说,每次我追加一个字符串到一些东西(一个 StringBuilder、某种日志、一个控制台等)上时我就必须捕获那些 IOException。 为什么?因为它可能正在执行 IO 操作(Writer 也实现了 Appendable)…… 所以它导致这种代码随处可见的出现:
try {
log.append(message)
}
catch (IOException e) {
// 必须要安全
}
这样的结果就很不好。在小型程序中的试验证明,在方法定义中要求标明异常信息,可以提高开发者的生产性,同时提高代码质量,但在大型软件中的经验则指向 个不同的结论:生产性降低,而代码质量改善不大,或者根本没有改善。
Nothing 类型
在 Kotlin 中 throw 是表达式,所以你可以使用它(比如)作为 Elvis 表达式的一部分:
val s = person.name ?: throw IllegalArgumentException("Name required")
throw 表达式的类型是特殊类型 Nothing。 该类型没有值,而是用于标记永远不能达到的代码位置。 在你自己的代码中,你可以使用 Nothing 来标记一个永远不会返回的函数:
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
当你调用该函数时,编译器会知道在该调用后就不再继续执行了:
val s = person.name ?: fail("Name required")
println(s) // 在此已知“s”已初始化
可能会遇到这个类型的另一种情况是类型推断。这个类型的可空变体 Nothing? 有一个可能的值是 null。如果用 null 来初始化一个要推断类型的值,而又没有其他信息可用于确定更具体的类型时,编译器会推断出 Nothing? 类型:
val x = null // “x”具有类型 `Nothing?`
val l = listOf(null) // “l”具有类型 `List<Nothing?>
注解
注解声明
注解是将元数据附加到代码的方法。要声明注解,请将 annotation 修饰符放在类的前面:
annotation class Fancy
注解的附加属性可以通过用元注解标注注解类来指定:
@Target指定可以用该注解标注的元素的可能的类型(类、函数、属性、表达式等);@Retention指定该注解是否存储在编译后的 class 文件中,以及它在运行时能否通过反射可见 (默认都是 true);@Repeatable允许在单个元素上多次使用相同的该注解;@MustBeDocumented指定该注解是公有 API 的一部分,并且应该包含在生成的 API 文档中显示的类或方法的签名中。
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy
用法
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加 constructor 关键字 ,并将注解添加到其前面:
class Foo @Inject constructor(dependency: MyDependency) { …… }
你也可以标注属性访问器:
class Foo {
var x: MyDependency? = null
@Inject set
}
构造函数
注解可以有接受参数的构造函数。
annotation class Special(val why: String)
@Special("example") class Foo {}
允许的参数类型有:
- 对应于 Java 原生类型的类型(Int、 Long等);
- 字符串;
- 类(
Foo::class); - 枚举;
- 其他注解;
- 上面已列类型的数组。
注解参数不能有可空类型,因为 JVM 不支持将 null 作为注解属性的值存储。
如果注解用作另一个注解的参数,则其名称不以 @ 字符为前缀:
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""))
@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))
如果需要将一个类指定为注解的参数,请使用 Kotlin 类 (KClass)。Kotlin 编译器会自动将其转换为 Java 类,以便 Java 代码能够正常访问该注解与参数 。
import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)
@Ann(String::class, Int::class) class MyClass
Lambda 表达式
注解也可以用于 lambda 表达式。它们会被应用于生成 lambda 表达式体的 invoke() 方法上。这对于像 Quasar 这样的框架很有用, 该框架使用注解进行并发控制。
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
注解使用处目标
当对属性或主构造函数参数进行标注时,从相应的 Kotlin 元素生成的 Java 元素会有多个,因此在生成的 Java 字节码中该注解有多个可能位置 。如果要指定精确地指定应该如何生成该注解,请使用以下语法:
class Example(@field:Ann val foo, // 标注 Java 字段
@get:Ann val bar, // 标注 Java getter
@param:Ann val quux) // 标注 Java 构造函数参数
可以使用相同的语法来标注整个文件。 要做到这一点,把带有目标 file 的注解放在文件的顶层、package 指令之前或者在所有导入之前(如果文件在默认包中的话):
@file:JvmName("Foo")
package org.jetbrains.demo
如果你对同一目标有多个注解,那么可以这样来避免目标重复——在目标后面添加方括号并将所有注解放在方括号内:
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
支持的使用处目标的完整列表为:
file;property(具有此目标的注解对 Java 不可见);field;get(属性 getter);set(属性 setter);receiver(扩展函数或属性的接收者参数);param(构造函数参数);setparam(属性 setter 参数);delegate(为委托属性存储其委托实例的字段)。
要标注扩展函数的接收者参数,请使用以下语法:
fun @receiver:Fancy String.myExtension() { ... }
如果不指定使用处目标,则根据正在使用的注解的 @Target 注解来选择目标 。如果有多个适用的目标,则使用以下列表中的第一个适用目标:
param;property;field.
Java 注解
Java 注解与 Kotlin 100% 兼容:
import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*
class Tests {
// 将 @Rule 注解应用于属性 getter
@get:Rule val tempFolder = TemporaryFolder()
@Test fun simple() {
val f = tempFolder.newFile()
assertEquals(42, getTheAnswer())
}
}
因为 Java 编写的注解没有定义参数顺序,所以不能使用常规函数调用语法来传递参数。相反,你需要使用具名参数语法:
// Java
public @interface Ann {
int intValue();
String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
就像在 Java 中一样,一个特殊的情况是 value 参数;它的值无需显式名称指定:
// Java
public @interface AnnWithValue {
String value();
}
// Kotlin
@AnnWithValue("abc") class C
数组作为注解参数
如果 Java 中的 value 参数具有数组类型,它会成为 Kotlin 中的一个 vararg 参数:
// Java
public @interface AnnWithArrayValue {
String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C
对于具有数组类型的其他参数,你需要显式使用数组字面值语法(自 Kotlin 1.2 起)或者 arrayOf(……):
// Java
public @interface AnnWithArrayMethod {
String[] names();
}
// Kotlin 1.2+:
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C
// 旧版本 Kotlin:
@AnnWithArrayMethod(names = arrayOf("abc", "foo", "bar"))
class D
访问注解实例的属性
注解实例的值会作为属性暴露给 Kotlin 代码:
// Java
public @interface Ann {
int value();
}
// Kotlin
fun foo(ann: Ann) {
val i = ann.value
}
反射
反射是这样的一组语言和库功能,它允许在运行时自省你的程序的结构。 Kotlin 让语言中的函数和属性做为一等公民、并对其自省(即在运行时获悉一个名称或者一个属性或函数的类型)与简单地使用函数式或响应式风格紧密相关。
类引用
最基本的反射功能是获取 Kotlin 类的运行时引用。要获取对静态已知的 Kotlin 类的引用,可以使用 类字面值 语法:
val c = MyClass::class
该引用是 KClass 类型的值。
注意,Kotlin 类引用与 Java 类引用不同。要获得 Java 类引用, 请在 KClass 实例上使用 .java 属性。
枚举类成员
反射最常用的功能之一就是枚举类的成员 ,如类的属性、方法等。在 Kotlin 的引用类中,有多个 memberXxx 函数可以实现这个功能。其中 Xxx 是 Properties 、 Functions 等。下面的代码使用相应的函数获取了 Person 类中所有的成员,以及单独获取了所有的属性和所有的函数。
open class Person constructor(var name: String) { //申明主构造函数
var mName: String = "Bill" // 初始化成员属性
var age: Int = 0
var gender: Int = 0
init {
this.mName = name
println("name = [ $name ]")
}
// 申明次构造函数(通过 this 直接调用了主构造函数)
constructor(name: String, age: Int) : this(name) {
this.mName = name
this.age = age
}
// 申明次构造函数(通过 this 调用了次构造函数,间接的调用了主构造函数)
constructor(name: String, age: Int, gender: Int) : this(name, age) {
this.gender = gender
println("name = [${name}], age = [${age}], gender = [${gender}]")
}
/**
* kotlin 函数默认值
*
* @param name 姓名
* @param age 年龄
* @param gender 性别
*/
fun setPersonInfo(name: String = "bill", age: Int = 23, gender: Int) {
println("name = [${name}], age = [${age}], gender = [${gender}]")
this.mName = name
this.age = age
this.gender = gender
}
override fun toString(): String {
return "Person(name='$mName', age=$age, gender=$gender)"
}
}
// 获取 Person 类的类应用
val c = Person::class
fun main(args: Array<String>) {
// 获取 Person 类中所有的成员列表(属性和函数)
println("Person 类的所有成员数:${c.members.size}")
// 枚举 Person 类中的所有成员
println("=============枚举 Person 类中的所有成员=============")
for (member in c.members) {
// 输出每个成员的名字和类型
println(" 成员名:${member.name} , 返回类型:${member.returnType}")
}
// 获取 Person 中所有属性的个数
println("属性个数:${c.memberProperties.size} ")
// 枚举 Person 类中所有的函数
println("=============枚举 Person 类中所有的函数=============")
for (function in c.functions) {
// 输出函数名和返回类型
println(" 函数名: ${function.name} , 返回类型: ${function.returnType}")
}
// 枚举 Person 类中所有的属性
println("=============枚举 Person 类中所有的属性=============")
for (property in c.memberProperties) {
// 输出属性名和返回类型
println(" 属性名: ${property.name} , 返回类型: ${property.returnType}")
}
}
输出结果:
动态调用成员函数
反射的另外一个重要应用就是可以动态调用对象中的成员,如成员函数、成员属性等。所谓的动态调用,就是根据类成员的名字进行调用,可以动态指定成员的名字。
通过 :: 操作符,可以直接返回类的成员。例如, MyClass 类有 process 函数,使用 MyClass::process 可以获取该成员函数的对象,然后使用 invoke 函数调用 process 函数即可。
不过这也不算是动态指定 process 函数的名字,而是将函数名字硬编码在代码中。不过通过调用 Java 的反射机制,就可以实现动态指定函数名,并调用该函数的功能。
fun main(args: Array<String>) {
// 获取 setPersonInfo 函数对象
var p = Person::setPersonInfo
var person = Person("jack")
// 调用 invoke 函数执行 setPersonInfo 函数
p.invoke(person, "mike", 23, 1)
// 利用 java 反射机制指定 setPersonInfo 方法的名字
var method = Person::class.java.getMethod(
"setPersonInfo",
String::class.java,
Int::class.java,
Int::class.java
)
method.invoke(person, "Bill", 25, 0)
}
// 输出结果:
// name = [ jack ]
// name = [mike], age = [23], gender = [1]
// name = [Bill], age = [25], gender = [0]
动态调用成员属性
Kotlin 类的属性与函数一样,也可以使用反射动态调用。不过 Kotlin 编译器在处理 Kotlin 类属性时,会将其转换为 getter 和 setter 方法,而不是与属性同名的 Java 字段。例如 ,对于下面的 Person 类,在编译后, name 属性会变成两个方法: getName 和 setName 。
fun main(args: Array<String>) {
var person = Person("jack")
var name = person::name
println(name.get()) // 输出结果:jack
name.set("Tom")
println(name.get()) // 输出结果:Tom
// 利用 Java 反射获取 getName 方法
var getName = Person::class.java.getMethod("getName")
// 动态获取 name 属性的值
println("java 反射得到的 name = " + getName.invoke(person)) // 输出结果:java 反射得到的 name = Tom
}
作用域函数
Kotlin 标准库包含几个函数,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数。共有以下五种:let、run、with、apply 以及 also。
这些函数基本上做了同样的事情:在一个对象上执行一个代码块。不同的是这个对象在块中如何使用,以及整个表达式的结果是什么。
下面是作用域函数的典型用法:
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
如果不使用 let 来写这段代码,就必须引入一个新变量,并在每次使用它时重复其名称。
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
作用域函数没有引入任何新的技术,但是它们可以使你的代码更加简洁易读。
由于作用域函数的相似性质,为你的案例选择正确的函数可能有点棘手。选择主要取决于你的意图和项目中使用的一致性。下面我们将详细描述各种作用域函数及其约定用法之间的区别。
区别
由于作用域函数本质上都非常相似,因此了解它们之间的区别很重要。每个作用域函数之间有两个主要区别:
- 引用上下文对象的方式
- 返回值
上下文对象:this 还是 it
在作用域函数的 lambda 表达式里,上下文对象可以不使用其实际名称而是使用一个更简短的引用来访问。每个作用域函数都使用以下两种方式之一来访问上下文对象:作为 lambda 表达式的接收者(this)或者作为 lambda 表达式的参数(it)。两者都提供了同样的功能,因此我们将针对不同的场景描述两者的优缺点,并提供使用建议。
fun main() {
val str = "Hello"
// this
str.run {
println("The receiver string length: $length")
//println("The receiver string length: ${this.length}") // 和上句效果相同
}
// it
str.let {
println("The receiver string's length is ${it.length}")
}
}
this
run、with 以及 apply 通过关键字 this 引用上下文对象。因此,在它们的 lambda 表达式中可以像在普通的类函数中一样访问上下文对象。在大多数场景,当你访问接收者对象时你可以省略 this,来让你的代码更简短。相对地,如果省略了 this,就很难区分接收者对象的成员及外部对象或函数。因此,对于主要对对象成员进行操作(调用其函数或赋值其属性)的 lambda 表达式,建议将上下文对象作为接收者(this)。
val adam = Person("Adam").apply {
age = 20 // 和 this.age = 20 或者 adam.age = 20 一样
city = "London"
}
println(adam)
it
反过来,let 及 also 将上下文对象作为 lambda 表达式参数。如果没有指定参数名,对象可以用隐式默认名称 it 访问。it 比 this 简短,带有 it 的表达式通常更容易阅读。然而,当调用对象函数或属性时,不能像 this 这样隐式地访问对象。因此,当上下文对象在作用域中主要用作函数调用中的参数时,使用 it 作为上下文对象会更好。若在代码块中使用多个变量,则 it 也更好。
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
此外,当将上下文对象作为参数传递时,可以为上下文对象指定在作用域内的自定义名称。
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
返回值
根据返回结果,作用域函数可以分为以下两类:
apply及also返回上下文对象。let、run及with返回 lambda 表达式结果.
这两个选项使你可以根据在代码中的后续操作来选择适当的函数。
上下文对象
apply 及 also 的返回值是上下文对象本身。因此,它们可以作为辅助步骤包含在调用链中:你可以继续在同一个对象上进行链式函数调用。
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
它们还可以用在返回上下文对象的函数的 return 语句中。
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
Lambda 表达式结果
let、run 及 with 返回 lambda 表达式的结果。所以,在需要使用其结果给一个变量赋值,或者在需要对其结果进行链式操作等情况下,可以使用它们。
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
此外,还可以忽略返回值,仅使用作用域函数为变量创建一个临时作用域。
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
几个函数
为了帮助你为你的场景选择合适的作用域函数,我们会详细地描述它们并且提供一些使用建议。从技术角度来说,作用域函数在很多场景里是可以互换的,所以这些示例展示了定义通用使用风格的约定用法。
let
上下文对象作为 lambda 表达式的参数(it)来访问。返回值是 lambda 表达式的结果。
let 可用于在调用链的结果上调用一个或多个函数。例如,以下代码打印对集合的两个操作的结果:
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
使用 let,可以写成这样:
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// 如果需要可以调用更多函数
}
若代码块仅包含以 it 作为参数的单个函数,则可以使用方法引用(::)代替 lambda 表达式:
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
let 经常用于仅使用非空值执行代码块。如需对非空对象执行操作,可对其使用安全调用操作符 ?. 并调用 let 在 lambda 表达式中执行操作。
val str: String? = "Hello"
//processNonNullString(str) // 编译错误:str 可能为空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 编译通过:'it' 在 '?.let { }' 中必不为空
it.length
}
使用 let 的另一种情况是引入作用域受限的局部变量以提高代码的可读性。如需为上下文对象定义一个新变量,可提供其名称作为 lambda 表达式参数来替默认的 it。
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.toUpperCase()
println("First item after modifications: '$modifiedFirstItem'")
Target platform: JVMRunning on kotlin v. 1.6.0
with
一个非扩展函数:上下文对象作为参数传递,但是在 lambda 表达式内部,它可以作为接收者(this)使用。 返回值是 lambda 表达式结果。
我们建议使用 with 来调用上下文对象上的函数,而不使用 lambda 表达式结果。 在代码中,with 可以理解为“对于这个对象,执行以下操作。”
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
with 的另一个使用场景是引入一个辅助对象,其属性或函数将用于计算一个值。
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
run
上下文对象 作为接收者(this)来访问。 返回值 是 lambda 表达式结果。
run 和 with 做同样的事情,但是调用方式和 let 一样——作为上下文对象的扩展函数.
当 lambda 表达式同时包含对象初始化和返回值的计算时,run 很有用。
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// 同样的代码如果用 let() 函数来写:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
除了在接收者对象上调用 run 之外,还可以将其用作非扩展函数。 非扩展 run 可以使你在需要表达式的地方执行一个由多个语句组成的块。
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}
apply
上下文对象 作为接收者(this)来访问。 返回值 是上下文对象本身。
对于不返回值且主要在接收者(this)对象的成员上运行的代码块使用 apply。apply 的常见情况是对象配置。这样的调用可以理解为“将以下赋值操作应用于对象”。
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
将接收者作为返回值,你可以轻松地将 apply 包含到调用链中以进行更复杂的处理。
also
上下文对象作为 lambda 表达式的参数(it)来访问。 返回值是上下文对象本身。
also 对于执行一些将上下文对象作为参数的操作很有用。 对于需要引用对象而不是其属性与函数的操作,或者不想屏蔽来自外部作用域的 this 引用时,请使用 also。
当你在代码中看到 also 时,可以将其理解为“并且用该对象执行以下操作”。
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
函数选择
为了帮助你选择合适的作用域函数,我们提供了它们之间的主要区别表。
| 函数 | 对象引用 | 返回值 | 是否是扩展函数 |
|---|---|---|---|
let | it | Lambda 表达式结果 | 是 |
run | this | Lambda 表达式结果 | 是 |
run | - | Lambda 表达式结果 | 不是:调用无需上下文对象 |
with | this | Lambda 表达式结果 | 不是:把上下文对象当做参数 |
apply | this | 上下文对象 | 是 |
also | it | 上下文对象 | 是 |
以下是根据预期目的选择作用域函数的简短指南:
- 对一个非空(non-null)对象执行 lambda 表达式:
let - 将表达式作为变量引入为局部作用域中:
let - 对象配置:
apply - 对象配置并且计算结果:
run - 在需要表达式的地方运行语句:非扩展的
run - 附加效果:
also - 一个对象的一组函数调用:
with
不同函数的使用场景存在重叠,你可以根据项目或团队中使用的特定约定选择函数。
尽管作用域函数是使代码更简洁的一种方法,但请避免过度使用它们:这会降低代码的可读性并可能导致错误。避免嵌套作用域函数,同时链式调用它们时要小心:此时很容易对当前上下文对象及 this 或 it 的值感到困惑。
takeIf 与 takeUnless
除了作用域函数外,标准库还包含函数 takeIf 及 takeUnless。这俩函数使你可以将对象状态检查嵌入到调用链中。
当以提供的谓词在对象上进行调用时,若该对象与谓词匹配,则 takeIf 返回此对象。否则返回 null。因此,takeIf 是单个对象的过滤函数。反之,takeUnless如果不匹配谓词,则返回对象,如果匹配则返回 null。该对象作为 lambda 表达式参数(it)来访问。
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
当在 takeIf 及 takeUnless 之后链式调用其他函数,不要忘记执行空检查或安全调用(?.),因为他们的返回值是可为空的。
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 编译错误
println(caps)
takeIf 及 takeUnless 与作用域函数一起特别有用。 一个很好的例子是用 let 链接它们,以便在与给定谓词匹配的对象上运行代码块。 为此,请在对象上调用 takeIf,然后通过安全调用(?.)调用 let。对于与谓词不匹配的对象,takeIf 返回 null,并且不调用 let。
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")
没有标准库函数时,相同的函数看起来是这样的:
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")