美文网首页KotlinKotlin编程之旅
[基础篇]Kotlin第三讲-扩展函数及其他

[基础篇]Kotlin第三讲-扩展函数及其他

作者: sugaryaruan | 来源:发表于2018-05-07 21:36 被阅读9次

    集合的创建与遍历

    Kotlin没有采用它自己的集合类,而是采用标准的Java集合类。大部分Kotlin的标准库是由Java类的拓展函数组成的。

    创建集合

    Kotlin中对集合增加了一个新的接口MutableList,实现该接口的集合是可变集合。Kotlin中,集合分为可变集合和不可变集合。

    public interface MutableList<E> : List<E>, MutableCollection<E> {
       
        override fun add(element: E): Boolean
    
        override fun remove(element: E): Boolean
    
        override fun addAll(elements: Collection<E>): Boolean
    
        public fun addAll(index: Int, elements: Collection<E>): Boolean
    
        override fun removeAll(elements: Collection<E>): Boolean
        override fun retainAll(elements: Collection<E>): Boolean
        override fun clear(): Unit
    
        public operator fun set(index: Int, element: E): E
    
        public fun add(index: Int, element: E): Unit
    
        public fun removeAt(index: Int): E
    
        override fun listIterator(): MutableListIterator<E>
    
        override fun listIterator(index: Int): MutableListIterator<E>
    
        override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>
    }
    
    

    MutableList接口提供了增加和删除集合元素的能力。

    创建不可变集合

    val list = listOf<String>("a", "b", "c")
    val letter = list[0]
    var list1 = listOfNotNull<Int>(1, 4, 8)
    

    创建可变集合

    val list2 = arrayListOf<Int>(1, 2, 3, 4)
    list2.set(0, 10)
    list2[0] = 10
    list2.add(5)
    println("list2 = $list2")
    
    val list3 = mutableListOf("a", "b", "c")
    list3.add("d")
    println("e = $list3")
    println("last element = ${list3.last()}")
    
    val list4 = mutableMapOf<String, String>("1" to "A", "2" to "B")
    val list5 = mutableSetOf<String>("B", "C", "D")
    

    参数

    Kotlin的函数比Java函数强大的地方之一是入参可以有默认值,即默认参数;

    在Kotlin调用函数时,可以指定入参的名称,即命名参数;

    与Java不同,Koltin表示可变参数,不是参数后面加三个点,,而是在入参前加vararg关键词即可。Kotlin中存在一个展开运算符 -- *(星号),它和可变参数搭配使用;作用是把一个数组展开成可变参数传入

    详细说明,可看这篇文章Kotlin里的输入参数

    顶层函数与属性

    1. 很多代码并不能归属到任何一个类中,有时一个操作对应两个不同的类的对象,而且重要性相差无几。
    2. 有时存在一个基本的对象,但不想通过实例函数来添加操作,让它的API继续膨胀。

    在Java里,我们使用静态方法。Kotlin里没有static修饰词,它一种方式,使用顶层函数来实现相同的效果。

    顶层函数

    实现一个功能,把集合中元素添加前缀,后缀,用分隔符间隔展示

    在kt类里直接写

    const val counter: Int = 0
    
    fun <T> joinToString(
            collection: Collection<T>,
            separator: String,
            prefix: String,
            postfix: String
                                    ): String {
        val sb = StringBuffer(prefix)
        for ((index, element) in collection.withIndex()) {
            if (index > 0) {
                sb.append(separator)
            }
            sb.append(element)
        }
        sb.append(postfix)
    
        return sb.toString()
    }
    
    

    顶层函数是包内成员,包内直接访问。若包外访问,需要import(IDEA等开发工具会为你自动import)

    顶层函数是都是静态函数,默认函数所在的文件名加KT作为容器类,比如上述joinToString方法是在Example3_2文件名下的顶层函数,在Java里调用是时

    Example3_2Kt.joinToString(collection, ",", "[", "]");
    

    如果我想更改调用静态方法的容器类的类名为StringUtils,则需要在kotlin文件里添加

    @file:JvmName("StringUtils")
    

    这时候调用形式如下:

    StringUtils.joinToString(collection, "," , "[", "]");
    

    顶层属性

    counter就是顶层属性,等效于容器类的静态成员变量,即Java里如下写法

    public static final int counter = 0;    
    

    拓展函数与属性

    拓展函数基本使用

    StringUtils.joinToString(collection, "," , "[", "]");
    

    每次调用上述实现的joinToString方法传入四个参数,有点多,我希望减少入参数量。StringUtils是工具类类名,工具类类名在整个调用过程中是不够高效的。达到优雅的途径之一就是做到高效而简洁。这个工具类具体是什么名字并不会影响这个函数的输入和输出,这个类名的意义是作为joinToString容器的标示,如果能把其中一个入参名作为类名,这个入参同时做两件事:传入自身到函数体里,作为调用的句柄名。

    强大的Kotlin为我们实现了这样的能力:扩展函数。把上述方法生命为一个拓展函数,如下所示:

    fun <T> Collection<T>.joinToString(separator: String = ",", prefix: String = "(", postfix: String = ")"){
        val sb = StringBuilder()
        sb.append(prefix)
        for((index, element) in this.withIndex()){
            if(index > 0){
                sb.append(separator)
            }
            sb.append(element.toString())
        }
        sb.append(postfix)
    }
    
    

    接收者类型:函数名前的类,上例Collection就是该扩展函数的接收者类型

    接收者对象:接收者类的实例,上例this.withIndex方法的this指代的就是Collection对象,是该扩展函数接收者对象

    这时候我们要使用joinToString方法,变成这样用了

    val list3 = mutableListOf("a", "b", "c")
    list3.joinToString("_", "[", "]")
    

    导入范围

    需要进行导入扩展函数才能生效,好在开发工具为我们自动导入了。如果定义的扩展函数所在的类和其接收者类型的类在一个包下,可以不需要显式导入。

    有一种情况,如果你定义的扩展函数名和其他包里定义的函数名相同,你需要导入全类名以示区分。还有另一种方式,通过as在导入的地方重命名扩展函数名

    import sugarya.chapter3.joinToString as jts
    

    这时候,就可以调用jts就相当于调用joinToString方法

    扩展函数的本质和特性

    其实,Kotlin把上述代码翻译到JVM上运行时。拓展函数的本质是:把接收者对象作为第一个入参的函数。通常我们在顶层位置定义扩展函数,这样扩展函数就能被其他包的文件调用。因此,扩展函数并没有改变接收者类里的代码,扩展函数并不是类的一部分,它是声明在类之外的,却能像成员变量那般使用。

    像成员变量那般使用,扩展函数和成员变量不是一回事,它们之间是有区别的

    1. 扩展函数不能访问私有或者受保护的成员,因为接收者对象只是静态方法的一个入参,这个入参有大的访问能力,扩展函数就是多大访问能力。
    2. 扩展函数不能被接收者类的子类重写/继承。前面说了,扩展函数只是静态方法,并不是真实的接收者里的成员,自然也就无法重写了。

    对于第2点的理解,我们举一个例子

    class Person(name: String, var age: Int) : Animal(name)
    
    //拓展定义是写在Example2_4.Kt文件里
    fun Animal.move(){
        println("animal move")
    }
    
    fun Person.move(){
        println("Person move")
    }
    
    val animal: Animal = Person("Kotlin", 5)
    animal.move()
    
    

    输出结果:

    animal move
    

    animal.move是拓展函数,转化为静态方法是Example2_4.move(animal),所以,move方法调用的就是Animal类下的move。

    扩展属性

    扩展属性是对扩展函数能力的弱化/简化使用。相当于Java里第一个参数是接收者对象的静态getter方法和setter方法。扩展函数和扩展属性搭配使用,在扩展函数里访问扩展属性。举个例子

    val Animal.length: Int get() = this.name.length * 10
    
    fun Animal.move(){
        println("animal move ${this.length}")
    }
    

    扩展函数的应用

    看几个扩展函数的应用例子

    分割字符串

    有一个字符串“ab.cd12.ef”,需要分割成三部分:ab, cd12, ef

    使用Java,我们很容易写成这样

    String msg = "ab.cd12.ef";
    String[] strings = msg.split(".");
    

    java里split()方法入参的字符串表示的正则表达式,在正则表达式里“.”表示任意字符,所以,如果照上面所写,返回为空,找不到字符。

    使用Java正确实现是:

    String msg = "ab.cd12.ef";
    String[] strings = msg.split("\\.");
    

    Kotlin在此基础上,通过扩展函数扩展字符串方法,通过默认参数实现重载效果。

    /**
     * Splits this char sequence to a list of strings around occurrences of the specified [delimiters].
     *
     * @param delimiters One or more strings to be used as delimiters.
     * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`.
     * @param limit The maximum number of substrings to return. Zero by default means no limit is set.
     *
     * To avoid ambiguous results when strings in [delimiters] have characters in common, this method proceeds from
     * the beginning to the end of this string, and matches at each position the first element in [delimiters]
     * that is equal to a delimiter in this instance at that position.
     */
    public fun CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> {
        if (delimiters.size == 1) {
            val delimiter = delimiters[0]
            if (!delimiter.isEmpty()) {
                return split(delimiter, ignoreCase, limit)
            }
        }
    
        return rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) }
    }
    

    Kotlin实现

    "ab.cd12.ef"split(".")
    

    Kotlin里用Regex类表示正则,使用正则实现如下

    val regex = Regex("\\.")
    val result = "ab.cd12.ef".split(regex.toPattern())
    

    解析字符串在Kotlin变得更容易了,除了split,Kotlin还提供了其他方法,再看一个例子

    解析文件路径

    解析一个文件路径:“/Users/mine/Documents/MyDocument/Photoes/546294_308008399296566_779316797_n.jpg”,获取目录路径,文件名,文件拓展名

    Kotlin代码实现

    val msg = "/Users/mine/Documents/MyDocument/Photoes/546294_308008399296566_779316797_n.jpg"
    val dirPath = msg.substringBeforeLast("/")
    val filePath = msg.substringAfterLast("/")
    val fileName = filePath.substringBeforeLast(".")
    val extendName = filePath.substringAfterLast(".")
    
    println("directory path = $dirPath, fileName = $fileName, extendName = $extendName")
    

    输出:

    directory path = /Users/mine/Documents/MyDocument/Photoes, fileName = 546294_308008399296566_779316797_n, extendName = jpg
    

    局部属性

    在Java里,函数的最小的作用域是在一个类里(private修饰的方法),而Kotlin引入局部函数--允许在函数里定义一个函数,让函数(方法)的最小作用域降到一个函数体里。提供更小粒度的复用,这样有什么意义呢?

    这样是有意义的。

    没有局部函数的特性的Java语言里,对方法最小作用域的组织方式是这样的:一个复杂的类里有很多方法,当方法A里的代码行数很多时,通常拆分出几个新的方法a1,a2,a3等等,这些新的方法之间如果存在整体的逻辑关系,就能组合成一个内部类,a1,a2,a3是该内部类的方法。直接在A里新建内部类并调用即可。外部类的其他方法比如方法B也能方便的调用。

    Kotlin局部函数提供了比上述Java更细致的代码组织方式:如果我们只在一个方法A里多次用到,这时候在方法A里,定义a1,a2,a3,在方法A里多次使用方法a1,a2,a3。这种方式相较于上面的内部类组织方式,带来的益处是降低定义内部类带来的语法开销。

    对于什么时候引入局部函数,我们有了下述认识:
    当需要在方法粒度上多次调用一段逻辑时。具体的场景有,登录验证,表单数据校验。

    中缀调用

    1. 对只有一个参数的函数使用中缀调用
    2. 中缀调用的函数,需要对其使用inflix修饰符
    3. 中缀不仅适用于成员函数也适用于扩展函数

    举个中缀的例子

    val pair: Pair<String, String> = "a" to2 "A"
    

    上面的中缀调用是怎么定义呢?

    infix fun <T, V> T.to2(v: V): Pair<T, V> = Pair(this, v)
    

    三重引号的字符串

    三重引号字符串不仅在于避免转义符,而且可以包含任何字符,包括换行符。

    看一个佛祖镇楼的例子

        val bless = """
                       _ooOoo_
                      o8888888o
                      88" . "88
                      (| -_- |)
                      O\  =  /O
                   ____/`---'\____
                 .'  \\|     |//  `.
                /  \\|||  :  |||//  \
               /  _||||| -:- |||||-  \
               |   | \\\  -  /// |   |
               | \_|  ''\---/''  |   |
               \  .-\__  `-`  ___/-. /
             ___`. .'  /--.--\  `. . __
          ."" '<  `.___\_<|>_/___.'  >'"".
         | | :  `- \`.;`\ _ /`;.`/ - ` : | |
         \  \ `-.   \_ __\ /__ _/   .-` /  /
    ======`-.____`-.___\_____/___.-`____.-'======
                       `=---='
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
             佛祖保佑       永无BUG
             """
        println(bless)     
    

    这样控制台按原样格式输出佛祖图

    小结

    这是Kotlin实战第三章涉及的所有知识点,结合自己的理解整理归纳成本篇文章。

    相关文章

      网友评论

        本文标题:[基础篇]Kotlin第三讲-扩展函数及其他

        本文链接:https://www.haomeiwen.com/subject/fvkbrftx.html