Kotlin 的其他技术
目录
一解构声明
二区间
三类型检查与转换
四 this 表达式
五相等性
六操作符重载
七空安全
八异常
九类型别名
一解构声明
解构声明能同时创建多个变量, 将对象中的数据解析成相对的变量举个例子:
- // 创建一个数据类 User
- data class User(var name: String, var age: Int)
- // 获得 User 的实例
- var user = User("Czh", 22)
- // 声明变量 name 和 age
- var (name, age) = user
- println("name:$name age:$age")
- // 输出结果为: name:Czh age:22
上面代码中用解构声明同时创建两个变量的时候, 会被编译成以下代码:
- // 指定变量 name 的值为 user 第一个参数的值
- var name = user.component1()
- // 指定变量 name 的值为 user 第二个参数的值
- var age = user.component2()
- println("name:$name age:$age")
- // 输出结果为: name:Czh age:22
解构声明和 Map
Map 可以保存一组 key-value 键值对, 通过解构声明可以把这些值解构出来如下所示:
- var map = mutableMapOf<String, Any>()
- map.put("name", "Czh")
- map.put("age", 22)
- for ((key, value) in map) {
- println("$key:$value")
- }
运行代码, 输出结果:
二区间
1.in
假如现在要判断 i 是否在 1-5 内, 可以这样写:
- if (i in 1..5) {
- println("i 在 1-5 内")
- }
上面代码中, 1..5 指的是 1-5,in 指的是在... 范围内, 如果 i 在范围 1-5 之内, 将会执行后面的代码块, 输出结果如果想判断 i 是否不在 1-5 内, 可以这样写:
- //!in 表示不在... 范围内
- if (i !in 1..5) {
- println("i 不在 1-5 内")
- }
上面两段代码等同于:
- if (i >= 1 && i <= 5) {
- println("i 在 1-5 内")
- }
- if (i <= 1 && i >= 5) {
- println("i 不在 1-5 内")
- }
- 2.downTo
如果想输出 1-5 , 可以这样写:
- for (i in 1..5) println(i)
- // 输出 12345
如果倒着来:
- for (i in 5..1) println(i)
- // 什么也不输出
这个时候可以用 downTo 函数倒序输出 5-1
- for (i in 5 downTo 1) println(i)
- 3.step
上面的代码顺序输出 12345 或倒序 54321, 按顺序 + 1 或者 - 1, 也就是步长为 1 如果要修改步长, 可以用 step 函数, 如下所示:
- for (i in 1..5 step 2) println(i)
- // 输出 135
- // 倒序
- for (i in 1 downTo 5 step 2) println(i)
- // 输出 531
- 4.until
上面的代码中, 使用的范围都是闭区间, 例如 1..5 的区间是[1,5], 如果要创建一个不包括其结束元素的区间, 即区间是[1,5), 可以使用 until 函数, 如下所示:
- for (i in 1 until 5) println(i)
- // 输出 1234
三类型检查与转换
1.is 操作符
在 Kotlin 中, 可以通过 is 操作符判断一个对象与指定的类型是否一致, 还可以使用 is 操作符的否定形式! is, 举个例子:
- var a: Any = "a"
- if (a is String) {
- println("a 是 String 类型")
- }
- if (a !is Int) {
- println("a 不是 Int 类型")
- }
运行代码, 输出结果为:
2. 智能转换
在 Kotlin 中不必使用显式类型转换操作, 因为编译器会跟踪不可变值的 is 检查以及显式转换, 并在需要时自动插入 (安全的) 转换举个例子:
- var a: Any = "a"
- if (a is String) {
- println("a 是 String 类型")
- println(a.length) // a 自动转换为 String 类型
- // 输出结果为: 1
- }
还可以反向检查, 如下所示:
- if (a !is String) return
- print(a.length) // a 自动转换为 String 类型
在 && 和 || 的右侧也可以智能转换:
- // `&&` 右侧的 a 自动转换为 String
- if (a is String && a.length > 0)
- // `||` 右侧的 a 自动转换为 String
- if (a is String || a.length > 0)
在 when 表达式和 while 循环里也能智能转换:
- when(a){
- is String -> a.length
- is Int -> a + 1
- }
需要注意的是, 当编译器不能保证变量在检查和使用之间不可改变时, 智能转换不能用智能转换能否适用根据以下规则:
val 局部变量总是可以, 局部委托属性除外;
val 属性如果属性是 private 或 internal, 或者该检查在声明属性的同一模块中执行智能转换不适用于 open 的属性或者具有自定义 getter 的属性;
var 局部变量如果变量在检查和使用之间没有修改没有在会修改它的 lambda 中捕获并且不是局部委托属性;
var 属性决不可能(因为该变量可以随时被其他代码修改)
3. 强制类型转换
在 Kotlin 中, 用操作符 as 进行强制类型转换, 如下所示:
- var any: Any = "abc"
- var str: String = any as String
但强制类型转换是不安全的, 如果类型不兼容, 会抛出一个异常, 如下所示:
- var int: Int = 123
- var str: String = int as String
- // 抛出 ClassCastException
4. 可空转换操作符
null 不能转换为 String, 因该类型不是可空的举个例子:
- var str = null
- var str2 = str as String
- // 抛出 TypeCastException
解决这个问题可以使用可空转换操作符 as?, 如下所示:
- var str = null
- var str2 = str as? String
- println(str2) // 输出结果为: null
使用安全转换操作符 as? 可以在转换失败时返回 null, 避免了抛出异常
四 this 表达式
为了表示当前的接收者我们使用 this 表达式当 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 中存在结构相等和引用相等两中相等判断
1. 结构相等
使用 equals()或 == 判断, 如下所示:
- var a = "1"
- var b = "1"
- if (a.equals(b)) {
- println("a 和 b 结构相等")
- // 输出结果为: a 和 b 结构相等
- }
- var a = 1
- var b = 1
- if (a == b) {
- println("a 和 b 结构相等")
- // 输出结果为: a 和 b 结构相等
- }
2. 引用相等
引用相等指两个引用指向同一对象, 用 === 判断, 如下所示:
- data class User(var name: String, var age: Int)
- var a = User("Czh", 22)
- var b = User("Czh", 22)
- var c = b
- var d = a
- if (c == d) {
- println("a 和 b 结构相等")
- } else {
- println("a 和 b 结构不相等")
- }
- if (c === d) {
- println("a 和 b 引用相等")
- } else {
- println("a 和 b 引用不相等")
- }
运行代码, 输出结果为:
六操作符重载
Kotlin 允许对自己的类型提供预定义的一组操作符的实现, 这些操作符具有固定的符号表示 (如 + 或 *)和固定的优先级为实现这样的操作符, 我们为相应的类型 (即二元操作符左侧的类型和一元操作符的参数类型) 提供了一个固定名字的成员函数或扩展函数 重载操作符的函数需要用 operator 修饰符标记
重载操作符
+ 是一个一元操作符, 下面来对一元操作符进行重载:
- // 用 operator 修饰符标记
- operator fun String.unaryPlus(): String {
- return this + this
- }
- // 调用
- var a = "a"
- println(+a) // 输出结果为: aa
当编译器处理例如表达式 +a 时, 它执行以下步骤:
确定 a 的类型, 令其为 T;
为接收者 T 查找一个带有 operator 修饰符的无参函数 unaryPlus(), 即成员函数或扩展函数;
如果函数不存在或不明确, 则导致编译错误;
如果函数存在且其返回类型为 R, 那就表达式 +a 具有类型 R;
除对一元操作符进行重载外, 还可以对其他操作符进行重载, 其重载方式和原理大致相同下面来一一列举:
1. 一元操作符
表达式 | 对应的函数 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
2. 二元操作符
表达式 | 对应的函数 |
---|---|
a+b | a.plus(b) |
a-b | a.minus(b) |
a*b | a.times(b) |
a/b | a.div(b) |
a%b | a.mod(b) |
a..b | a.rangeTo(b) |
3.in 操作符
表达式 | 对应的函数 |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
4. 索引访问操作符
表达式 | 对应的函数 |
---|---|
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) |
5. 调用操作符
表达式 | 对应的函数 |
---|---|
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) |
6. 广义赋值
表达式 | 对应的函数 |
---|---|
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)(已弃用) |
7. 相等与不等操作符
表达式 | 对应的函数 |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
8. 比较操作符
表达式 | 对应的函数 |
---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
七空安全
在 Java 中, NullPointerException 可能是最常见的异常之一, 而 Kotlin 的类型系统旨在消除来自代码空引用的危险
1. 可空类型与非空类型
在 Kotlin 中, 只有下列情况可能导致出现 NullPointerException:
显式调用 throw NullPointerException();
使用了下文描述的 !! 操作符;
有些数据在初始化时不一致;
外部 Java 代码引发的问题
在 Kotlin 中, 类型系统区分一个引用可以容纳 null (可空引用)还是不能容纳(非空引用) 例如, String 类型的常规变量不能容纳 null:
如果要允许为空, 我们可以声明一个变量为可空字符串, 在字符串类型后面加一个问号?, 写作 String?, 如下所示:
- var b: String? = "b"
- b = null
2. 安全调用操作符
接着上面的代码, 如果你调用 a 的方法或者访问它的属性, 不会出现 NullPointerException, 但如果调用 b 的方法或者访问它的属性, 编译器会报告一个错误, 如下所示:
这个时候可以使用安全调用操作符, 写作?., 在 b 后面加安全调用操作符, 表示如果 b 不为 null 则调用 b.length, 如下所示: b?.length
安全调用操作符还能链式调用, 例如一个员工 Bob 可能会 (或者不会) 分配给一个部门, 并且可能有另外一个员工是该部门的负责人, 那么获取 Bob 所在部门负责人 (如果有的话) 的名字, 我们写作:
- Bob?.department?.head?.name
- // 如果 Bob 分配给一个部门
- // 执行 Bob.department.head? 获取该部门的负责人
- // 如果该部门有一个负责人
- // 执行 Bob.department.head.name 获取该负责人的名字
如果该链式调用中任何一个属性为 null, 整个表达式都会返回 null 如果要只对非空值执行某个操作, 安全调用操作符可以与 let 一起使用, 如下所示:
- val listWithNulls: List<String?> = listOf("A", null, "B")
- for (item in listWithNulls) {
- item?.let { println(it) }
- }
运行代码, 输出结果为:
安全的类型转换
如果对象不是目标类型, 那么常规类型转换可能会导致 ClassCastException 另一个选择是使用安全的类型转换, 如果尝试转换不成功则返回 null, 如下所示:
val i: Int? = i as? Int
可空类型的集合
如果你有一个可空类型元素的集合, 并且想要过滤非空元素, 你可以使用 filterNotNull 来实现如下所示:
- val nullableList: List<Int?> = listOf(1, 2, null, 4)
- val intList: List<Int> = nullableList.filterNotNull()
3.Elvis 操作符
先看一段代码:
- val i: Int = if (b != null) b.length else -1
- val i = b?.length ?: -1
这两行代码表达的都是如果 b 不等于 null,i = b.length; 如果 b 等于 null,i = -1 第一行代码用的是 if 表达式, 而第二行代码使用了 Elvis 操作符, 写作?:Elvis 操作符表示如果?: 左侧表达式非空, 就使用左侧表达式, 否则使用右侧表达式 请注意, 因为 throw 和 return 在 Kotlin 中都是表达式, 所以它们也可以用在 Elvis 操作符右侧如下所示:
- fun foo(node: Node): String? {
- val parent = node.getParent() ?: return null
- val name = node.getName() ?: throw IllegalArgumentException("name expected")
- //
- }
4. !! 操作符
!! 操作符将任何值转换为非空类型, 若该值为空则抛出异常如下所示:
- var a = null
- a!!
- // 运行代码, 抛出 KotlinNullPointerException
八异常
Kotlin 中所有异常类都是 Throwable 类的子类每个异常都有消息堆栈回溯信息和可选的原因 使用 throw 表达式可以抛出异常举个例子:
throw NullPointerException("NPE")
使用 try 表达式可以捕获异常一个 try 表达式可以有多个 catch 代码段; finally 代码段可以省略举个例子:
- try {
- // 捕获异常
- } catch (e: NullPointerException) {
- // 异常处理
- } catch (e: ClassNotFoundException) {
- // 异常处理
- } finally {
- // 可选的 finally 代码段
- }
因为 Try 是一个表达式, 所以它可以有一个返回值举个例子:
- val a: Int? = try {
- parseInt(input)
- } catch (e: NumberFormatException) {
- null
- }
try 表达式的返回值是 try 块中的最后一个表达式或者是 catch 块中的最后一个表达式 finally 块中的内容不会影响表达式的结果
九类型别名
Kotlin 提供类型别名来代替过长的类型名称, 这些类型别名不会引入新类型, 且等效于相应的底层类型可以通过使用关键字 typealias 修改类型别名, 如下所示:
- // 使用关键字 typealias 修改类型别名 Length
- // 相当于 Length 就是一个 (String) -> Int 类型
- typealias Length = (String) -> Int
- // 调用
- fun getLength(l: Length) = l("Czh")
- // 编译器把 Length 扩展为 (String) -> Int 类型
- val l: Length = { it.length }
- println(getLength(l)) // 输出结果为: 3
使用类型别名能让那些看起来很长的类型在使用起来变得简洁, 如下所示:
- typealias MyType = (String, Int, Any, MutableList<String> ) -> Unit
- // 当我们使用的时候
- var myType:MyType
- // 而不需要写他原来的类型
- //var myType:(String, Int, Any, MutableList<String> ) -> Unit
总结
相对于 Java 来说, Kotlin 有很多新的技术和语法糖, 这也是为什么使用 Kotlin 来开发 Android 要优于 Java 运用好这些新的东西, 能大大加快开发速度
参考文献:
Kotlin 语言中文站 Kotlin 程序开发入门精要
来源: https://juejin.im/post/5a9cdc7df265da2393768e06