## **[异常](https://www.kancloud.cn/alex_wsc/android_kotlin/1318345)处理**
[TOC]
### **异常概述**
什么是异常?说到异常处理,我们想到了try、catch、finally 这样的3个关键字,是的,Kotlin里面也是这样的3个关键字,意义和Java也是一样的。我们对可能出现异常的语句使用try语句块包裹,如果try里面的语句真的出现了异常,代码将会跳转到catch语句块里面。无论代码执行顺序如何,最终都会走到finally语句块中。
Kotlin 中所有异常类都是 Throwable 类的子孙类。 每个异常都有消息、堆栈回溯信息和可选的原因。
示例
```
fun divide(a: Int, b: Int): Int {
var result: Int = a / b //定义一个result变量用于存放a/b的值
return result //将结果返回
}
fun main(args: Array<String>) {
var result = divide(5, 0) //调用divide()方法
println(result)
}
```
运行结果
```
Exception in thread "main" java.lang.ArithmeticException: / by zero
at FileKt.divide (File.kt:2)
at FileKt.main (File.kt:7)
```
从运行结果可知,程序在运行过程中抛出一个算术异常(ArithmeticException),这个异常是因为程序的第6行代码中,调用divide()方法时第2个参数传入了0,当程序执行第3行代码时出现了被0除的错误。出现这个异常之后,程序会立即结束,不会再执行下面的其他代码。
ArithmeticException异常类只是Kotlin异常类中的一种,在Kotlin中还提供了其他异常类,如ClassNotFoundException、ArrayIndexOutOfBoundsException、IllegalArgumentException等,这些类都继承自java.lang.Throwable类。
要抛出异常可以使用 throw -表达式来抛出异常。
```
throw MyException("Hi There!")
```
要捕获异常,可以使用try表达式
```
try {
// 一些代码
}
catch (e: SomeException) {
// 处理程序
}
finally {
// 可选的 finally 块
}
```
>[info]注意:
①、try表达式可以有0到多个 catch 块。 finally 块可以省略。 但是 catch 和 finally 块至少应该存在一个。
②、在Kotlin中, throw和try都是表达式,这意味着它们可以被赋值给一个变量。这个在处理一些边界问题的时候确实非常有用
### try…catch和finally
Kotlin中提供了一种对异常进行处理的方式——**异常捕获**。异常捕获通常使用try…catch语句来实现,异常捕获的语法格式如下:
```
try {
// 程序代码
}catch (e: SomeException) { // e: 后面的这个类可以是Exception 类或者其子类
// 对捕获的Exception 进行处理
}
```
上述语法格式中,try代码块中编写的是可能发生异常的Kotlin语句,catch代码块中编写的是对捕获的异常进行处理的代码。当try代码块中的程序发生了异常,系统会将这个异常的信息封装成一个异常对象,并将这个对象传递给catch代码块。catch代码块中传递的Exception类型的参数是指定这个程序能够接收的异常类型,这个参数的类型必须是Exception类或者其子类。
* **示例**
```
fun divide(a: Int, b: Int): Int {
var result: Int = a / b //定义一个result变量用于存放a/b的值
return result //将结果返回
}
fun main(args: Array<String>) {
try {
var result: Int = divide(5, 0)
println(result)
} catch (e: Exception) {
println("捕获的异常信息为:" + e.message)
}
println("程序继续执行")
}
```
运行结果
```
捕获的异常信息为:/ by zero
程序继续执行
```
在上述代码中,对可能发生异常的代码用try…catch语句进行了处理,在try代码块中发生被0除异常之后,程序会接着执行catch中的代码,通过调用Exception对象中的message来返回异常信息“by/zero”,catch代码块对异常处理完毕之后,程序会继续向下执行,而不会出现异常终止。
>[info]需要注意的是,在try代码块中,发生异常的语句后面的代码是不会被执行的,如本案例中的第8行代码的输出语句就没有执行。
在程序中,由于功能的需求,有些语句在无论程序是否发生异常的情况下都要执行,这时就可以在try…catch语句后添加一个finally代码块。接下来我们来对上面的代码进行修改,看一下finally代码块的用法,具体代码如下所示。
```
fun divide(a: Int, b: Int): Int {
var result: Int = a / b //定义一个result变量用于存放a/b的值
return result //将结果返回
}
fun main(args: Array<String>) {
try {
var result: Int = divide(5, 0) //调用divide()方法
println(result)
} catch (e: Exception) {
println("捕获的异常信息为:" + e.message)
return //用于结束当前语句
} finally {
println("进入finally代码块")
}
println("程序继续执行")
}
```
运行结果
```
捕获的异常信息为:/ by zero
进入finally代码块
```
在上述代码中,catch代码块中增加了一个return语句,用于结束当前方法,此时第15行的代码`println("程序继续执行")`就不会执行了,而finally中的代码仍会执行,并不会被return语句所影响。
不论程序发生异常还是用return语句结束当前方法,finally中的语句都会执行,由于finally语句的这种特殊性,因此在程序设计时,经常会在try…catch后使用finally代码块来完成必须做的事情,如释放系统资源。
>[info]需要注意的是,当catch代码块中执行了System.exit(0)语句时,finally中的代码块就不会执行。System.exit(0)表示退出当前的程序,退出当前程序后,任何代码都不能再执行了。
#### 与Java中的try…catch…finally对比
与Java不同的是,**在try…catch…finally中,try是一个表达式,即它可以返回一个值,try表达式的返回值是try代码块中的最后一个表达式的结果或者是所有catch代码块中的最后一个表达式的结果,finally代码块中的内容不会影响表达式的结果**。接下来我们通过一个案例来演示try…catch…finally代码块中的结果,代码如下所示
```
fun main(args: Array<String>) {
var age: Int? = try {
10 //正常运行,返回10
} catch (e: NumberFormatException) {
12
null
} finally {
13 //finally代码块不会影响表达式结果
}
var score: Int? = try {
Integer.parseInt("s" + 1)
} catch (e: NumberFormatException) {
60
null //捕获异常,返回null
} finally {
70 //finally代码块不会影响表达式结果
}
println("年龄age=${age}")
println("分数score=${score}")
}
```
运行结果
```
年龄age=10
分数score=null
```
上述代码中,分别定义了2个变量age与score,age表示年龄,score表示分数,这2个变量的值都是用try…catch…finally代码块包括起来的,根据程序的运行结果可知,当给变量age赋值时,程序正常运行,则age的值是10,即try表达式的结果。当给变量score赋值时,程序出现了异常(s不知从何而来,Integer是Java中的类,字符串默认值是null,即为空未分配对象,看Integer源码就知道,Integer.parseInt方法参数为null时,`throw new NumberFormatException("null")`),此时score的值是null,即catch代码块中最后一个表达式的结果。其中finally代码块中的内容不影响try和catch代码块中表达式的结果。
总结:
在try…catch…finally语句中,catch代码块可以有多个也可以没有,finally代码块可以省略,但是catch和finally代码块至少应有一个是存在的。
#### **Try 是⼀个表达式,可以有一个返回值**
【Kotlin相比于Java,可以使用变量try表达式返回值】。try表达式要么有try语句块的最后一行决定,要么由catch语句块的最后一行决定;finally代码段的内容不会影响try表达式的结果值。参考如下代码:
![](https://box.kancloud.cn/d641eb1949151aad0704404c1f337dcc_984x627.png)
针对以上代码,如果try语句块没有异常,返回的就是字符串转换转换的结果,如果出现异常,就会走到catch语句块,返回的就是0。
运行结果:
![](https://box.kancloud.cn/e5d4ef9fa7248b21126d2a96df34c5d6_979x123.png)
### **throw表达式**
前面的代码中,由于调用的是自己写的divide()方法,因此很清楚该方法可能会发生异常。试想一下,如果去调用一个别人写的方法时,是否能知道别人写的方法是否会有异常呢?这是很难做出判断的。针对这种情况,在Kotlin中,允许在可能发生异常的代码中通过throw关键字对外声明该段代码可能会发生异常,这样在使用这段代码时,就明确地知道该段代码有异常,并且必须对异常进行处理。
throw关键字抛出异常的语法格式如下:
```
throw ExceptionType("异常信息")
```
上述语法格式是通过throw表达式来抛出一个异常,这个表达式需要放在容易出现异常的代码中,也可以作为Elvis表达式(三元表达式)的一部分,throw后面需要声明发生异常的类型,通常将这种做法称为抛出一个异常。接下来我们来修改前面的代码,在调用divide()方法的代码上方抛出一个异常,修改后的代码如下所示
```
fun divide(a: Int, b: Int): Int {
if (b==0)throw ArithmeticException("发生异常")
var result: Int = a / b
return result
}
fun main(args: Array<String>) {
var result: Int = divide(5, 0) //调用divide()方法
println(result)
}
```
报错如下
```
Exception in thread "main" java.lang.ArithmeticException: 发生异常
at FileKt.divide (File.kt:2)
at FileKt.main (File.kt:7)
```
上述代码中,第7行代码调用divide()方法时传递的第2个参数是0,程序在运行时不会发生被0除的异常,由于在divide()方法中抛出了一个算术异常ArithmeticException,因此在调用divide()方法时就必须对抛出的异常进行处理,否则就会发生编译错误。接下来我们对代码进行修改,在调用divide()方法时对其进行try…catch处理,修改后的代码如下所示。
```
fun divide(a: Int, b: Int): Int {
if (b==0)throw ArithmeticException("发生异常")
var result: Int = a / b
return result
}
fun main(args: Array<String>) {
try {
var result: Int = divide (5, 0) //调用divide()方法
println(result)
} catch (e: ArithmeticException) {
println(e.message)
}
}
```
运行结果
```
发生异常
```
上述代码中,在main()函数中通过try…catch语句已经捕获了divide()方法中抛出的异常,由于该程序的divide()方法中传递的变量b的值为0,因此运行结果为发生异常。如果将变量b的值设置为1,则运行结果为5。
#### 用于Elvis 表达式的⼀部分
在 Kotlin 中 throw 是表达式,所以你可以使⽤它(⽐如)作为 Elvis 表达式的⼀部分:
```
val s = person.name ?: throw IllegalArgumentException("Name required")
```
#### Nothing类型
参考后面的章节——[kotlin.Nothing类型](https://www.kancloud.cn/alex_wsc/android_kotlin/1053641)和官方文档[Nothing 类型](http://www.kotlincn.net/docs/reference/exceptions.html#nothing-%E7%B1%BB%E5%9E%8B)
在 Kotlin 中 throw 是表达式,它的类型是特殊类型 Nothing。 该类型没有值。跟C、Java中的 void 意思一样,⽽是⽤于标记永远不能达到的代码位置。
在Kotlin中,有些函数的“返回值类型”的概念没有任何意义,此时Kotlin使用一种特殊的返回值类型Nothing来表示,Nothing是一个空类型(uninhabited type),也就是说在程序运行时不会出任何一个Nothing类型的对象。在程序中,可以使用Nothing类型来标记一个永远不会有返回数据的函数,
我们在代码中,用 Nothing 来标记无返回的函数:
~~~
fun main(args: Array<String>) {
fail("Error occurred")
}
fun fail(message:String):Nothing{
throw IllegalStateException(message)
}
~~~
当调用含有Nothing类型的函数时,可能会遇到这个类型的一种情况就是类型推断,这个类型的可空类型为Nothing?,该类型有一个可能的值是null,如果用null来初始化一个变量的值,并且又不能确定该变量的具体类型时,编译器会推断这个变量的类型为Nothing?类型,具体示例代码如下:
```
val x = null // 变量“x”的类型为“Nothing? ”
val l = listOf(null) // 变量“l”的类型为“List<Nothing?>”
```
上述代码中,变量x的初始值为null,并且该变量的具体类型不确定,因此编译器会推断该变量的类型为“Nothing?”。变量l的初始值为listOf(null),可以看到List集合的初始值为null,此时不能确定该集合的具体类型,因此编译器会推断该集合的类型为“List<Nothing?>”。
* 另外,如果把一个throw表达式的值赋值给一个变量,需要显式声明类型为 Nothing ,
```
val ex:Nothing = throw Exception("YYYYYYYY")
```
>[info]【注意】另外,因为ex变量是Nothing类型,没有任何值,所以无法当做参数传给函数
可能会遇到这个类型的另⼀种情况是类型推断。这个类型的可空变体 Nothing? 有⼀个可能的值是 null 。如果⽤ null 来初始化⼀个要推断类型的值,⽽⼜没有其他信息可⽤于确定更具体的类型时,编译器会推断出 Nothing? 类型:
```
val x = null // “x”具有类型 `Nothing?`
val l = listOf(null) // “l”具有类型 `List<Nothing?>
```
### **没有受检异常**
**Kotlin没有受检异常(Checked Exceptions)**。
Java里面有两种异常类型,一种是受检异常(checked exception),一种是非受检异常(uncheckedexception)。
之所以编写Java代码的时候,IDE会提示进行try catch操作,因为编译时编译器会检查受检异常。
受检异常(Checked Exceptions)显得比较麻烦,一直以来争议比较大,可能会导致java API变得复杂,程序员编写代码的时候需要进行大量的try catch操作。所以,【Kotlin相比于Java,没有了受检异常,IDE不会提示进行try catch操作】。
写代码的时候,IDE调用某一个方法,这个方法即使可能抛出异常,IDE也不会提示你进行`try…catch`操作,直接运行该程序时会抛出异常信息。参考如下代码:
![](https://box.kancloud.cn/b6267b3d90998baa9b7de986f725b276_984x297.png)
针对以上代码,如果是java代码,【Integer.parseInt(line)】,这样的代码是会提示我们进行trycatch操作的,但是Kotlin不会提示。如果直接运行会抛出转换异常,参考截图:
![](https://box.kancloud.cn/f6b962f8694f98d2c0e3e48726494460_972x121.png)
我们可以自己给它加上try catch操作,参考如下代码:
![](https://box.kancloud.cn/cf18dc1b6a25d0297d18a3dcdac5887b_986x766.png)
* **JDK举例**
以下是 JDK 中 StringBuilder 类实现的⼀个⽰例接⼝:
```
Appendable append(CharSequence csq) throws IOException;
```
这个签名是什么意思? 它是说,每次我追加⼀个字符串到⼀些东西(⼀个 StringBuilder 、某种⽇志、⼀个控制台等)上时我就必须捕获那些IOException 。 为什么?因为它可能正在执⾏ IO 操作( Writer 也实现了 Appendable )…… 所以它导致这种代码随处可⻅的出现:
```
try {
log.append(message)
}
catch (IOException e) {
// 必须要安全
}
```
**结论**:通过一些小程序测试得出的结论是异常规范会同时提高开发者的生产力与代码质量,但是大型软件项目的经验表明一个不同的结论——生产力降低、代码质量很少或没有提高。
### 自定义异常
在Kotlin标准库中封装了大量的异常类,虽然这些异常类可以描述编程时出现的大部分异常情况,但是在程序开发中有时可能需要描述程序中特有的异常情况,例如在设计一个变量name的值时,不允许该变量的值为null,为了解决这个问题,Kotlin与Java一样,也允许用户自定义异常,但**自定义的异常类必须继承自Throwable类或其子类**,自定义异常类的主构函数可以传递一个String类型的message参数,也可以不传递参数,自定义异常类的语法格式如下:
```
//主构函数带参数的异常类
class异常类名称(override val message: String?) : Throwable() {}
//主构函数不带参数的异常类
class异常类名称() : Throwable() {}
```
在上述语法格式中,主构造函数带message参数的异常类,在抛出异常信息时会打印message信息,主构造函数不带message参数的异常类,在抛出异常信息时不会打印异常的具体信息。
示例
```
class MyException(override val message: String?) : Throwable() {
}
fun main(args: Array<String>) {
var name: String? = null
name?.length ?: throw MyException("变量name的值为null")
println("name.length=${name.length}")
}
```
报错如下
```
Exception in thread "main" MyException: 变量name的值为null
at FileKt.main (File.kt:5)
```
从运行结果可以看出,程序在运行时发生了异常,这是因为在程序中使用throw关键字抛出异常对象时,需要使用`try…catch`语句对抛出的异常进行处理。为了解决该程序运行时发生异常的问题,可以修改该程序,在抛出异常的地方使用`try…catch`语句对异常进行处理,修改后的代码如下所示。
```
class MyException(override val message: String?) : Throwable() {
}
fun main(args: Array<String>) {
var name: String? = null
try {
name?.length ?: throw MyException("变量name的值为null")
println("name.length=${name.length}")
} catch (e: MyException) {
println(e.message)
}
}
```
运行结果
```
变量name的值为null
```
在上述代码中,使用了一个try…catch语句用于捕获变量name的值为null时抛出的异常,在调用“name?.length”代码时,由于变量name的值不能为null,程序会抛出一个自定义异常MyException,该异常被捕获后最终被catch代码块处理,并打印异常信息。
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android