Kotlin 扩展课堂

Kotlin 中的小魔术

字符串内嵌表达式

1
2
3
4
5
6
7
8
9
10
fun main(){
// Kotlin 中字符串内嵌表达式的语法规则:
// "hello, ${obj.name}, nice to meet you! "
// 当表达式仅有一个变量时,可将大括号省略。
// "hello, $name, nice to meet you! "

val brand = "Samsung"
val price = "1299.99"
println("Cellphone(brand=$brand,price=$price)")
}

函数的参数默认值
可在定义函数时给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main(){
printParams(123)
// 可通过键值对的方式来传参,就不必在意参数定义的顺序了。
printParams2(str = "world")
}

fun printParams(num:Int,str:String="hello"){
println("num is $num,str is $str")
}

fun printParams2(num:Int = 100,str:String = "hello"){
println("num is $num,str is $str")
}
1
2
3
4
5
6
7
/**
* 可以给参数设定默认值。
* 正因为此功能,它可在很大程度上替代次构造函数的作用。
*/
class Student(val sno:String="",val grade:Int=0,name:String="",age:Int=0) : Person(name,age) {

}

标准函数和静态方法

标准函数 let、with、run 和 apply

Kotlin 的标准函数指的是 Standard.kt 文件中定义的函数,任何 Kotlin 代码都可以自由地调用所有的标准函数。

let 这个标准函数在上面已经学过了,它的主要作用是配合 ?. 操作符来进行辅助判空处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
fun main(){
val list = listOf("Apple","Banana","Pear")

val builder = StringBuilder()
builder.append("Start eating fruits.\n")
for (fruit in list){
builder.append(fruit).append("\n")
}
builder.append("Ate all fruits.")
val result = builder.toString()
println(result)

withFun(list)
runFun(list)
applyFun(list)
}

/**
* with 函数接收两个参数:
* 第一个参数可以是一个任意类型的对象,
* 第二个参数是一个 Lambda 表达式。
* with 函数会在表达式中提供第一个参数对象的上下文,并使用表达式中的最后一行代码作为返回值。
* 示例代码:
* val result = with(obg){
* // 这里是 obj 的上下文
* "value" // with 函数的返回值
* }
*/
fun withFun(list: List<String>){
// with 函数可以在连续调用同一个对象的多个方法时让代码变得更加精简。
// 传入 StringBuilder() 对象,接下来表达式的上下文就是这个对象,所以就不用再调用 builder.。
val result = with(StringBuilder()){
append("Start eating fruits.\n")
for (fruit in list){
append(fruit).append("\n")
}
append("Ate all fruits.")
// 表达式的最后一行会作为返回值
toString()
}
println(result)
}

/**
* run 函数与 with 函数类似
* 首先 run 函数不能直接调用,而是一定要调用某个对象的 run 函数才行,
* 其次 run 函数只接收一个 Lambda 参数,并且会在表达式中提供调用对象的上下文。

(关于 run 函数不能直接调用的修正:在官方kotlin文档中,有这样的阐述:“除了在接收者对象上调用 run 之外,还可以将其用作非扩展函数。 非扩展 run 可以使你在需要表达式的地方执行一个由多个语句组成的块。”。标准函数中还有一个定义在顶层的 run 函数,是可以直接调用的,)

勘误: 首先 run 函数通常不会直接调用的,而是要在某个对象的基础上调用。

* 示例代码:
* val result = obj.run{
* // 这里是 obj 的上下文
* "value" // run 函数的返回值
* }
*/
fun runFun(list: List<String>){
// 与 with 函数相比,变化非常小
val result = StringBuilder().run {
append("Start eating fruits.\n")
for (fruit in list){
append(fruit).append("\n")
}
append("Ate all fruits.")
// 表达式的最后一行会作为返回值
toString()
}
println(result)
}

/**
* apply 函数 与 run 函数类似,
* 都要在某个对象上调用,并且只接收一个 Lambda 参数,也会在表达式中提供调用对象的上下文,
* 区别在于,apply 函数无法指定返回值,而是会自动返回调用对象本身。
* 示例代码:
* val result = obj.apply(){
* // 这里是 obj 的上下文
* }
* // result == obj
*/
fun applyFun(list: List<String>){
val result = StringBuilder().apply {
append("Start eating fruits.\n")
for (fruit in list){
append(fruit).append("\n")
}
append("Ate all fruits.")
// 表达式的最后一行会作为返回值
toString()
}
// 这里的 result 实际上是一个 StringBuilder 对象
println(result.toString())
}

定义静态方法

静态方法在某些语言中又叫作类方法,指的是不需要创建实例就能调用的方法,比如 Java 中是在方法上声明 static 关键字。静态方法非常适合用于编写一些工具类的功能,因为工具类通常没有创建实例的必要。

Kotlin 中极度弱化了静态方法的概念,因为它提供了更好用的语法特性,那就是单例类,像工具类这种功能就非常推荐使用单例类的方法。单例类使用 object 关键字,但这会使内部所有方法都变成类似静态方法的调用方式,如果只是想限定某一个方法,可用 companion object 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestClass {

fun doAction1(){}

/**
* 这里 Kotlin 没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似静态方法调用的写法。
* 由于它不是真正的静态方法,Java 代码中也无法调用它。
*
* companion object 关键字实际上会在 TestClass 类的内部创建一个伴生类,
* 而 doAction2() 就是定义在这个伴生类中的实例方法。
* 并且 Kotlin 会保证 TestClass 类始终只会存在一个伴生类对象。
*/
companion object {
fun doAction2(){}
}
}

如果确实需要定义真正的静态方法,有两种实现方式:注解和顶层方法。

1
2
3
4
5
6
7
8
9
10
11
class TestClass {
fun doAction1(){}
/**
* @JvmStatic 注解,会使 Kotlin 编译器将这些方法编译成真正的静态方法。
* 这个注解只能加在单例类或者 companion object 中的方法上
*/
companion object {
@JvmStatic
fun doAction2(){}
}
}

顶层方法指的是没有定义在任何类中的方法,比如 main 方法。编译器会将所有的顶层方法全部编译成静态方法。可以创建一个 File 文件 Helper.kt,在这个文件中定义的方法如 doSomething() 都会是顶层方法。顶层方法的调用:

  • 如果是 Kotlin 中:所有的顶层方法都可在任何位置被直接调用,不用管包名路径,也不用创建实例,直接键入方法名即可。
  • 如果是 Java 中:Kotlin 编译器会自动创建一个对应 File 文件的 Java 类,方法是以静态方法的形式定义在这个类里面的,因此使用 HelperKt.doSomething() 即可。

延迟初始化和密封类

对变量延迟初始化

有时,像即使明确知道一些全局变量不会为空,但出于 Kotlin 编译器的要求,还是需要额外做许多的判空处理代码。

可以使用 lateinit 关键字,进行延迟初始化,这样就不用在一开始时将它赋值为 null 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  /**
* 可以使用 lateinit 关键字,进行延迟初始化,
* 这样就不用在一开始时将它赋值为 null 了,类型声明可以改成 MsgAdapter。
* 但使用要注意,要确保变量在使用前,已做了初始化。
*/
// private var adapter:MsgAdapter?=null
private lateinit var adapter:MsgAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 使用 ::adapter.isInitialized 可用于判断 adapter 变量是否已经初始化,避免重复进行初始化操作。
if (!:: adapter.isInitialized){
adapter = MsgAdapter(msgList)
}
}

override fun onClick(v: View?) {

// 有时,像即使明确知道一些全局变量不会为空,onClick() 会在 onCreate() 之后调用,在 onCreate()中对 adapter 做了初始化。
// 但出于 Kotlin 编译器的要求,还是需要额外做许多的判空处理代码。但做了延迟初始化之后,就不必了。
// adapter?.notifyItemInserted(msgList.size-1)
adapter.notifyItemInserted(msgList.size-1)
}

使用密封类优化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 用于表示某个操作的执行结果
*/
//interface Result
//
//class Success(val msg:String) : Result
//class Failure(val error:Exception) :Result
//
//fun getResultMsg(result: Result) = when(result) {
// is Success -> result.msg
// is Failure -> result.error
// // 这个 else 分支实际上是必须的,但又是没有意义的,它只是为了满足编译器的语法检查
// // 因为结果只有成功了失败,走不到这里.
// // 但还是有个潜在风险,比如新增了一个 Unknown 类并实现了 Result 接口,用于表示未知的执行结果,
// // 但是却忘记在上面添加条件分支,这就会走到 else 分支里。
// else -> IllegalArgumentException()
//}

/**
* sealed class 表示密封类
* 密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。
*/
sealed class Result

/**
* 密封类是一个可继承的类,因此在继承它的时候需要在后面添加括号
*/
class Success(val msg:String) : Result()
class Failure(val error:Exception) :Result()

/**
* 为什么没有 else 分支也能编译通过?
* 因为当 when 语句传入一个密封类变量作为条件时,
* Kotlin 编译器会自动检查该密封类有哪些子类,并强制要求将每一个子类所对应的条件全部处理。
*/
fun getResultMsg(result: Result) = when(result) {
is Success -> result.msg
is Failure -> result.error
// 改成密封类之后,就不需要 else 分支了。
}

##

扩展函数和运算符重载

大有用途的扩展函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
fun main(){
// 统计字符串中字母的数量
val str = "ABC123xyz!@#"
val count = lettersCount(str)
println(count)

// 使用扩展函数的方式实现,统计字符串中字母的数量。
// 看起来就像 String 类中自带了这个函数一样
val count2 = str.lettersCount2()
println(count2)
}

/**
* 统计字符串中字母的数量
*/
fun lettersCount(str:String):Int{
var count = 0
for (char in str){
if (char.isLetter()){
count++
}
}
return count
}

/**
* 扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
* 定义扩展函数的语法结构:
* fun ClassName.methodName(param1:Int,param2:Int):Int{
* return 0
* }
* 相比于定义普通函数,定义扩展函数只需在函数名前加 ClassName. 的语法结构,就表示将该函数添加到指定类当中了。
*
* 这里为了省事,一般来讲,比如要添加到 String 类中,可以新创建一个 String.kt 同名文件(虽然文件名没有固定要求,但这样便于后期查找),
* 而且扩展函数也可定义在任何一个现有类中,但最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。
*
* 在 Java 中的 String 类是一个 final 类,但在 Kotlin 中可以向其中扩展任何函数,
* 比如内部有 reverse() 用于反转字符串,capitalize() 用于对首字母进行大写等。
*/
fun String.lettersCount2():Int{
var count = 0
// 将此函数定义成了 String 类的扩展函数,则此函数中自动拥有了 String 实例的上下文。
// 因此此函数就不再需要接收一个字符串参数了,而是直接遍历 this 即可,现在现在 this 就代表着字符串本身。
for (char in this){
if (char.isLetter()){
count++
}
}
return count
}

有趣的运算符重载

Kotlin 允许将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关键字的用法。

Kotlin 的运算符重载允许让任意两个对象进行相加,或者是进行更多其他的运算操作,但在实际编程时也要考虑逻辑的合理性,比如让两个 Student 对象相加没什么意义,但是让两个 Money 对象相加就变得有意义了,因为钱是可以相加的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
fun main(){
val money1 = Money(5)
val money2 = Money(10)
// 对象和对象相加
val money3 = money1 + money2
println(money3.value)
// 对象直接和数字相加
val money4 = money3 + 20
println(money4.value)

// String 类中提供的包含关系
println("hello".contains("he"))
// 借助重载的语法糖表达式。效果相同,但更简洁。
println("he" in "hello")
}

class Money(val value:Int){

/**
* 运算符重载使用 operator 关键字在指定函数的前面加上就可以了。
* 指定函数指的是不同运算符对应的重载函数也不同,
* 比如加号运算符对应的是 plus(),减号运算符对应 minus(),它们都是固定不变的。
* 但接收的参数和函数返回值可以自行设定,这里就代表一个 Obj 对象可以与另一个 Obj 对象相加,最终返回一个新的 Obj 对象。
* operator fun plus(obj:Obj){
* // 处理相关逻辑
* }
*
* 对应的调用方式如下:
* val obj1 = Obj()
* val obj2 = Obj()
* val obj3 = obj1 + obj2 // 它会在编译时被转换成 obj1.plus(obj2) 的调用方式
*/
operator fun plus(money:Money):Money{
// 将当前 Money 对象的 value 和参数传入的 Money 对象的 value 相加,
// 然后将得到的和传给一个新的 Money 对象并将该对象返回。
val sum = value + money.value * 6
return Money(sum)
}

/**
* Kotlin 允许对同一个运算符进行多重重载
*/
operator fun plus(newValue:Int):Money{
val sum = value + newValue
return Money(sum)
}

}

一些常用的可重载运算符和关键字对应的语法糖表达式,以及它们会被转换成的实际调用函数

语法糖表达式 实际调用函数
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++ a.inc()
a– a.dec()
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a == b a.equals(b)
a > b
a < b
a >= b a.compareTo(b)
a <= b
a..b a.rangeTo(b)
a[b] a.get(b)
a[b] = c a.set(b,c)
a in b(判断 a 是否在 b 中) b.contains(a)(判断 b 是否包含 a)

备注

参考资料

第一行代码(第3版)
官方文档
官方中文翻译站

传送门GitHub