阅读 367

Kotlin 扩展课堂

Kotlin 中的小魔术

字符串内嵌表达式

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)")
}
复制代码

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

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")
}
复制代码
/**
 * 可以给参数设定默认值。
 * 正因为此功能,它可在很大程度上替代次构造函数的作用。
 */
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 这个标准函数在上面已经学过了,它的主要作用是配合 ?. 操作符来进行辅助判空处理的。

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 关键字。

class TestClass {

    fun doAction1(){}

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

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

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 了。

  /**
     * 可以使用 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)
    }
复制代码

使用密封类优化代码

/**
 * 用于表示某个操作的执行结果
 */
//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 分支了。
}
复制代码

扩展函数和运算符重载

大有用途的扩展函数

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 对象相加就变得有意义了,因为钱是可以相加的。

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 + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)
a++a.inc()
a--a.dec()
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a == ba.equals(b)
a > b
a < b
a >= ba.compareTo(b)
a <= b
a..ba.rangeTo(b)
a[b]a.get(b)
a[b] = ca.set(b,c)
a in b(判断 a 是否在 b 中)b.contains(a)(判断 b 是否包含 a)

备注

参考资料

第一行代码(第3版)

官方文档

官方中文翻译站

欢迎关注微信公众号:非也缘也

文章分类
Android
文章标签