文章目录
一、标准函数和静态方法
1.1、标准函数 with、run 和 apply
Kotlin 的标准函数指的是 Standart.kt 文件中定义的函数,任何 Kotlin 代码都可以自由地调用所有的标准函数。在上一篇中已经介绍了 let 这个标准函数,它主要的作用就是配合 ?. 操作符进行判空处理。
with
with 函数接收一个 StringBuilder 对象,此时with 函数中整个 Lambda 表达式的上下文都会是这个 StringBuilder 对象。
val list = listOf("苹果", "香蕉", "橘子", "梨子", "葡萄")
val builder = StringBuilder()
builder.append("Start eating fruits.\n")
for (fruit in list) {
builder.append(fruit).append("\n")
}
builder.append("Ate all fruit.")
val result = builder.toString()
println(result)
val list = listOf("苹果", "香蕉", "橘子", "梨子", "葡萄")
val result = with(StringBuilder()) {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruit.")
toString()
}
println(result)
run
跟 with 函数不同之处在于,run 函数不能直接调用,而是要调用某个对象的 run 函数。
val list = listOf("苹果", "香蕉", "橘子", "梨子", "葡萄")
val result = StringBuilder().run {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruit.")
toString()
}
println(result)
apply
跟 run 函数一样,apply 函数不能直接调用,要在某个对象上调用。不同之处在于,apply 函数无法指定返回值,而是会返回调用对象本身。
val list = listOf("苹果", "香蕉", "橘子", "梨子", "葡萄")
val result = StringBuilder().apply {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruit.")
toString()
}
println(result)
1.2、定义静态方法
在 Java 中,定义一个静态方法,是在方法上声明一个 static 关键字,我们写工具类的时候就采用此方法。在 Kotlin 中,写一个工具类,可以将工具类定义为单例类,Kotlin 单例类中的所有方法都是类似 Java 静态类的方式。
// 调用
Util.doAction()
object Util {
fun doAction() {
println("do action")
}
}
如果只希望类中的某一个方法变成静态方法的调用方式,就需要用到 companion object,并且这时候不能再使用 object 修饰成单例类。
// 调用
Util.doAction2()
class Util {
fun doAction() {
println("do action")
}
companion object {
fun doAction() {
println("do action")
}
}
}
需要注意的是,上面的两种方式定义的都不是真正的静态方法,只是调用方法和静态方法类似,如果使用 Java 来调用是行不通的。Kotlin 中要定义真正的静态方法有两种:注解和顶层方法。
注解
// 调用
Util.doAction2()
class Util {
fun doAction() {
println("do action")
}
companion object {
@JvmStatic
fun doAction2() {
println("do action2")
}
}
}
顶层方法
定义顶层方法,需要新建 kotlin 的 file 文件。Kotlin 编译器会将所有的顶层方法全部编译成静态方法,在 Kotlin 代码中调用,可以在任何地方调用。而在 Java 代码中调用需要注意,比如下面定义的 Helper.kt 文件,直接是调用不到的,Kotlin 编译器会自动创建一个 HelperKt 的 Java 类,用这个类名才可以调用。
Kotlin 中调用。
doAction()
Java 中调用。
HelperKt.doAction();
二、延迟初始化和密封类
2.1、对变量延迟初始化
在 Kotlin 中,由于对于自动判空的语法特性,当一个类中全局变量越来越多时,就可能需要编写大量额外的判空处理代码。
private var person: Person? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
person = Person()
person?.eat()
}
这时候就可以选择使用 lateinit 关键字对变量进行延迟初始化,但是这也是有风险的,如果在变量初始化之前使用它,就会造成程序崩溃,所以在使用前还需要使用 ::person.isInitialized 并且取反来判断变量是否已经初始化了。
private lateinit var person: Person
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
person = Person()
if (!::person.isInitialized) {
person.eat()
}
}
2.2、使用密封类优化代码
用一个简单的例子说明。首先新建一个 Kotlin 文件 Result.kt,在其中定义了一个 Result 接口,用于表示某个操作的执行结果,接口中没有编写任何内容。还定义了两个类 Success 和 Failure 来分别实现这个接口,表示成功或者失败的结果。
interface Result
class Success(val msg:String):Result
class Failure(val error:Exception):Result
接下来定义一个 getResultMsg 方法来获取最终执行结果的信息。方法中接收一个 Result 参数,通过 when 语句来判断:如果 Result 数据 Success,那么就返回成功地消息;如果 Result 属于 Failure,那么就返回错误消息。
是我们还需要编写 else 条件,这就是 Kotlin 语法检查所要求的,虽然只有 Success 或者 Failure 两种情况。如果新增了一个 Unknown 类并实现 Result 接口,用于表示未知的执行结果,但是忘记在 getResultMsg 方法中添加相应的条件分支,运行时就会进入 else 条件里面。
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error.message
else -> throw IllegalArgumentException()
}
上面的代码可以使用密封类来优化,只需要把 interface 关键字改成 sealed class。由于密封类是一个可继承的类,继承它时需要加括号。这时在 getResultMsg 中就不需要加 else 条件,并且如果新增一个 Unknown 类,getResultMsg 会提示必须加 Unknown 的条件分支才能编译通过。
sealed class 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.message
}
三、扩展函数和运算符重载
3.1、扩展函数
定义:扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
这里以 Sting 类为例子来说明,一段字符串中可能包含字母、数字和特殊符号,需要统计出字符串中字母的数量。按照一般的写法代码如下。
object StringUtil {
fun lettersCount(str: String): Int {
var count = 0
for (char in str) {
if (char.isLetter()) {
count++
}
}
return count
}
}
// 使用
var str = "ABC123xyz!@#"
val count = StringUtil.lettersCount(str)
根据 Kotlin 扩展函数的特性,可以把 lettersCount 函数添加到 String 类中。扩展函数可以定义在任何一个现有类中,不过为了方便查找,建议向哪个类中添加扩展函数,就新建一个同名的 Kotlin 文件,比如这里可以新建一个 String.kt。同时最好定义成顶层方法,可以使扩展函数拥有全局的访问域。
当我们将 lettersCount 函数定义成 String 类的扩展函数时,函数中就自动拥有了 String 实例的上下文,就不用再传字符串参数了,而是直接遍历 this 即可,因为现在 this 就代表着字符串本身。
fun String.lettersCount(): Int {
var count = 0
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count
}
// 使用
val count = "ABC123xyz!@#".lettersCount()
3.2、运算符重载
Kotlin 的运算符重载允许我们让任意两个对象进行相加,或者进行更多其他的运算操作。虽然 Kotlin 有这种特性,在实际编程时也要考虑逻辑的合理性,比如让两个 Student 对象相加好像并没有什么实际意义,但是让两个 Money 对象相加就变得有意义了。
运算符重载使用的是 operator 关键字,只要在指定函数的前面加上 operator 关键字,就可以实现运算符重载的功能。要注意的是不同的运算符对应的重载函数也是不同的,比如说加号运算符对应的是 plus 函数,减号运算符对应的是 minus 函数。这里以加号运算符为例,实现让两个 Money 对象相加,注意此时 operator 和函数名 plus 是固定不变的。
class Money(val value: Int) {
operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}
}
val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
println(money3.value)
上面实现了让两个 Money 对象相加,Kotlin 中还允许我们对同一个运算符进行多重重载,例如再来实现让 Money 对象可以直接和数字相加。
class Money(val value: Int) {
operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}
operator fun plus(newValue: Int): Money {
val sum = value + newValue
return Money(sum)
}
}
val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
val money4 = money3 + 20
println(money4.value)
Kotlin 中允许我们重载的运算符和关键字很多,如果想重载其中某一种运算符或关键字,可参考下表。
表达式 | 实际调用函数 |
---|---|
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<=b | a.compareTo(b) |
a . . b | a.rangeTo(b) |
a[b] | a.get(b) |
a[b]=c | a.set(b,c) |
a in b | b.contains(a) |
举一个例子来结合使用扩展函数和运算符重载,让一个字符串能够和一个数字相乘,达到使用 str * n 的写法来表示让 str 字符串重复 n 次。要重载乘号运算符,函数名必须是 times,由于是 String 类的扩展函数,要在方法名前加上 String. 的语法结构,将下面的代码添加到 3.1 中创建的 String.kt 文件中。
fun String.lettersCount(): Int {
var count = 0
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count
}
operator fun String.times(n: Int): String {
val builder = StringBuilder()
repeat(n) {
builder.append(this)
}
return builder.toString()
}
// 打印结果是:abcabcabc
val str = "abc" * 3
println(str)
四、高阶函数详解
4.1、高阶函数定义
在 Java 中,定义一个需要传入参数的方法时,参数可以是字符串型、整型、布尔型等字段类型。而在 Kotlin 中定义方法的参数除了这些字段类型,还增加了一个函数类型。
语法规则如下,-> 左边部分用来声明该函数接收的参数类型,多个参数之间使用逗号隔开,如果不接收参数,就写一对空括号,-> 右边部分用于声明该函数的返回值类型,如果没有返回值就是用 Unit,相当于 Java 中的 void。
普通语法
(String, Int) -> Unit
完整语法,ClassName 可省略
ClassName.(String, Int) -> Unit
如果某个函数中添加了函数类型作为了参数声明或者返回值声明,那么这个函数就是一个高阶函数了。例如下面的 example 函数。
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}
高阶函数允许让函数类型的参数来决定函数的执行逻辑,即使是同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑和最终返回结果就可能是完全不同的。下面来定义一个 num1AndNum2 函数,代码如下。
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
由于 num1AndNum2 函数还接收了一个函数类型的参数 operation: (Int, Int) -> Int,因此还需要定义与函数类型相匹配的函数,这里定义了 plus 和 minus 两个函数,它们分别执行了不同的逻辑。
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}
这时候就可以调用 num1AndNum2 函数了,注意这里第三个参数,也就是函数类型的参数使用了 ::plus 和 ::minus 这种写法。这是一种函数引用方式的写法,表示将 plus 和 minus 函数作为参数传递给 num1AndNum2 函数。
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2, ::plus)
val result2 = num1AndNum2(num1, num2, ::minus)
println("result1 is $result1")
println("result2 is $result2")
运行结果:
result1 is 180
result2 is 20
使用这种函数引用的写法虽然能够正常工作,但是如果每次调用任何高阶函数时都需要定义与其函数类型参数相匹配的函数,就过于复杂。Kotlin 还支持其他多种方式来调用高阶函数,比如 Lambda 表达式、匿名函数、成员引用等。其中,Lambda 表达式是最常见也是最普遍的高阶函数调用方式。
上面调用 num1AndNum2 函数的代码如果使用 Lambda 表达式来实现的话,代码如下所示。
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) { n1, n2 -> n1 + n2 }
val result2 = num1AndNum2(num1, num2) { n1, n2 -> n1 - n2 }
println("result1 is $result1")
println("result2 is $result2")
4.2、内联函数的作用
Kotlin 的代码最终还是编译成 Java 字节码的,但 Java 中并没有高阶函数的概念。Kotlin 的编译器会将高阶函数的语法转换成 Java 支持的语法结构。
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
// 调用
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) { n1, n2 -> n1 + n2 }
考虑到可读性,这里对转换成的 Java 代码做了些许调整,并不是严格的对应了 Kotlin 转换成的 Java 代码。可以看到,在这里 num1AndNum2 函数的第三个参数变成了一个 Function 接口,这是一种 Kotlin 内置的接口,里面有一个待实现的 invoke 函数。
在调用 num1AndNum2 函数的时候,之前的 Lambda 表达式在这里变成了 Function 接口的匿名类实现,然后在 invoke 函数中实现了 n1+n2 的逻辑,并将结果返回。这就是 Kotlin 高阶函数背后的实现原理。
public static int num1AndNum2(int num1, int num2, Function operation) {
int result = operation.invoke(num1, num2);
return result;
}
// 调用
int num1 = 100;
int num2 = 80;
int result = num1AndNum2(num1, num2, new Function() {
@Override
public Integer invoke(Integer n1, Integer n2) {
return n1 + n2;
}
});
由于经常使用的 Lambda 表达式在底层被转换成了匿名类的实现方式。这就表明,每调用一次 Lambda 表达式,都会创建一个新的匿名类实例,这会造成额外的内存和性能开销。为了解决这个问题,Kotlin 提供了内联函数的功能,内联函数的用法很简单,只需要在定义高阶函数时加上 inline 关键字的声明即可。
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
内联函数的工作原理也并不复杂,Kotlin 编译器会将内联函数的代码在编译的时候自动替换到调用它的地方。
接下来再将内联函数中的全部代码替换到函数调用的地方。
最终的代码就被替换成如下所示。
4.3、noinline 与 crossinline
noinline
当一个高阶函数中接收了两个或多个函数类型的参数,这时给函数加上了 inline 关键字,那么 Kotlin 编译器会自动将所有引用 Lambda 表达式全部进行内联。这时如果想某个 Lambda 表达式不被内联,就可以在函数参数前添加一个 noinline 关键字。
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {}
noinline 关键字的目的在于,内联的函数参数类型在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数参数类型可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这就是内联函数的局限性。
内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的 Lambda 表达式中是可以使用 return 关键字来进行函数返回的,而非内联函数只能进行局部返回。
举一个例子,下面的非内联函数,如果字符串参数为空,那么就不打印,直接 return。注意,非内联函数在 Lambda 表达式中不允许直接使用 return 关键字,在这个例子中使用了 return@printString 的写法表示进行局部返回,不再执行 Lambda 表达式的剩余代码。从打印结果也可以看出进行的是局部返回。
// 调用
val str = ""
printString(str) { s ->
Log.d("yl", "lambda start")
if (s.isEmpty()) {
return@printString
}
println(s)
Log.d("yl", "lambda end")
}
fun printString(str: String, block: (String) -> Unit) {
Log.d("yl", "printString start")
block(str)
Log.d("yl", "printString end")
}
如果将 printString 声明成内联函数,在 Lambda 表达式中就可以直接使用 return 关键字了,此时的 return 代表的是返回外层的调用函数。
// 调用
val str = ""
printString(str) { s ->
Log.d("yl", "lambda start")
if (s.isEmpty()) {
return
}
println(s)
Log.d("yl", "lambda end")
}
inline fun printString(str: String, block: (String) -> Unit) {
Log.d("yl", "printString start")
block(str)
Log.d("yl", "printString end")
}
crossinline
将高阶函数声明成内联函数是一种良好的编程习惯,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。比如下面的 runRunnable 函数,在未声明成内联函数前是能正常工作,并不会报语法错误的。
前面说过,Lambda 表达式在编译时会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了函数类型参数 block。而这时候是不能直接使用 return 关键字进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,所以就会报语法错误。
也就是说,如果在高阶函数中创建了另外的 Lambda 或者匿名类的实现,并且在这些实现中调用函数类型参数,这时候如果将高阶函数声明成内联函数,就会报语法错误。如果在这种情况下还要声明成内联函数,就需要在调用的函数类型参数前加上 crossinline 的声明,代码就可以正常编译通过了。
inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
由于在内联函数的 Lambda 表达式中允许使用 return 关键字,和高阶函数的匿名类实现中不允许使用 return 关键字之间造成了冲突。这时 crossinline 关键字就像一个契约,用于保证在内联函数的 Lambda 表达式中一定不会直接使用 return 关键字进行返回。
声明了 crossinline 之后,就无法在调用 runRunnable 函数时的 Lambda 表达式中使用 return 关键字进行函数返回了,但是仍然可以使用 return@runRunnable 的写法进行局部返回。
4.4、Kotlin 语法知识点
-
A to B 的语法结构会创建一个 Pair 对象。
-
vararg 关键字:对应 Java 中的可变参数列表。
fun method(vararg str:String){ }
-
Any 是 Kotlin 中所有类的共同基类,相当于 Java 中的 Object。
-
Kotlin 中的 Smart Cast 功能。比如 when 语句进入 Int 条件分支后,这个条件下面的 value 会被自动转换成 Int 类型,不需要像在 Java 中那样再进行强转。这个功能在 if 语句中同样适用。
when(value){ is Int -> put(key,value) is String -> put(key,value) }