Kotlin的其他技术
目录
一、解构声明
二、区间 三、类型检查与转换 四、this表达式 五、相等性 六、操作符重载 七、空安全 八、异常 九、类型别名一、解构声明
解构声明能同时创建多个变量,将对象中的数据解析成相对的变量。举个例子:
//创建一个数据类Userdata class User(var name: String, var age: Int)//获得User的实例var user = User("Czh", 22)//声明变量 name 和 agevar (name, age) = userprintln("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()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) returnprint(a.length) // a 自动转换为String类型复制代码
在 && 和 || 的右侧也可以智能转换:
// `&&` 右侧的 a 自动转换为Stringif (a is String && a.length > 0)// `||` 右侧的 a 自动转换为Stringif (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 = 123var str: String = int as String//抛出ClassCastException复制代码
4.可空转换操作符
null
不能转换为 String
,因该类型不是可空的。举个例子:
var str = nullvar str2 = str as String//抛出TypeCastException复制代码
解决这个问题可以使用可空转换操作符as?
,如下所示:
var str = nullvar str2 = str as? Stringprintln(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 = 1var b = 1if (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 = bvar d = aif (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= listOf("A", null, "B")for (item in listWithNulls) { item?.let { println(it) }}复制代码
运行代码,输出结果为:
- 安全的类型转换 如果对象不是目标类型,那么常规类型转换可能会导致
ClassCastException
。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回null,如下所示:
val i: Int? = i as? Int复制代码
- 可空类型的集合 如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用
filterNotNull
来实现。如下所示:
val nullableList: List= listOf(1, 2, null, 4)val intList: List = nullableList.filterNotNull()复制代码
3.Elvis 操作符
先看一段代码:
val i: Int = if (b != null) b.length else -1val 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 = nulla!!//运行代码,抛出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) -> Unit//当我们使用的时候var myType:MyType //而不需要写他原来的类型//var myType:(String, Int, Any, MutableList ) -> Unit复制代码
总结
相对于Java来说,Kotlin有很多新的技术和语法糖,这也是为什么使用Kotlin来开发Android要优于Java。运用好这些新的东西,能大大加快开发速度。
参考文献:
、《Kotlin程序开发入门精要》 推荐阅读:更多精彩文章请扫描下方二维码关注微信公众号"AndroidCzh":这里将长期为您分享原创文章、Android开发经验等! QQ交流群: 705929135