Kotlin函数的定义与调用

作者: Rocdo | 来源:发表于2018-07-03 16:00 被阅读7次

    我们学习Kotlin的一个重要环节,函数的声明和调用。将从Kotlin集合、字符串和正则表达式作为重点,先来看看如何在Kotlin中创建集合

    在Kotlin中创建集合

    我们可以创建一个list或者map

     val set = hashSetOf(1,7,53)
     val list = arrayListOf(1,7,53)
     val map = hashSetOf(1 to "one",7 to "seven",53 to "fifty-three")
    
     println(set.javaClass)  //class java.util.HashSet
     println(list.javaClass) //class java.util.ArrayList
     println(map.javaClass)  //class java.util.HashSet
    
    • to 并不是一个特殊的结构,只是一个普通的函数。后面我会学习关于它

    从上面可以看出kotlin并没有采用自己的集合类,而是采用的标准的java集合类。
    尽管kotlin的集合类和Java的集合类完全一致,但是Kotlin还不止这些。例如:

    val strings = listOf("first","second","fourteenth")
    println(strings.last())     //fourteenth
    
    val numbers = setOf(1,14,2)
    println(numbers.max())  //14
    
    

    在讨论如何操作last和max这两个神奇的函数之前,我们先来学习一下函数声明

    让函数更好的调用

    java的集合都有一个默认的toString实现,但是它的格式化的输出是固定的,往往不是我们需要的样子

    val lists = listOf(1,2,3)
    println(lists)  //[1, 2, 3]
    

    假如我们要得到这样的打印效果(1;2;3) ,在Java项目会使用第三方库来完成。在Kotlin中,他的标准库中有一个专门的函数来处理这种情况。
    下面的joinToString函数就展现了通过在元素间添加分割符号,在最前面添加前缀,在最末添加后缀的方式把集合元素逐个添加到一个StringBuilder的过程

    fun <T> joinToString(collection: Collection<T>,
                             separator: String,
                             prefix: String,
                             postfix: String):String{
            val result = StringBuilder(prefix)
    
            for ((index,element) in collection.withIndex()){
                println("$index---$element")
                if (index > 0) result.append(separator)
                result.append(element)
            }
            result.append(postfix)
            return result.toString()
    }
    
    println(joinToString(lists,";","(",")"))
    

    这个函数是泛型:它可以支持元素为任意类型的集合。
    这个方法是可行的,但是怎么让它更简洁呢?避免每次都调用的时候都传入4个参数

    命名参数

    我们要关注函数的可读性,比如joinToString(lists," "," ","."),我们看不出String对应的都是什么参数。
    但是在Kotlin中,可以做的更优雅

    joinToString(lists,separator = " ",prefix = " ",postfix = ".")
    

    当调用一个Kotlin定义的函数时,可以显式地标明一些参数的名称。如果调用一个函数时,指明了一个参数名称,为了避免混淆,之后所有的参数都需要标明名称。

    默认参数值

    java的另一个普遍存在的问题时,一些类的重载函数实在太多了。
    在Kotlin中,可以在声明函数的时候,指定参数的默认值,这样就可以避免创建重载函数,我们尝试改进一下前面的joinToString函数

    fun <T> joinToString(collection: Collection<T>,
                             separator: String=",",
                             prefix: String="",
                             postfix: String=""):String
     现在可以用所有参数来调用这个函数,或者省略掉部分参数
     joinToString(list,", ","", "") //1, 2, 3
     joinToString(list)                 //1, 2, 3
     joinToString(list,";")             //1; 2; 3
    

    当使用常规调用语法时,必须按照函数声明中定义的参数顺序来给定参数,可以省略只排在末尾的参数,如果使用命名参数,可以省略中间的一些参数,也可以以任意顺序只给定你需要的参数

    给别人的类添加方法:扩展函数和属性

    Kotlin的一大特色,就是可以平滑地与现有代码集成。甚至,纯Kotlin的项目都可以基于Java库构建,如:JDK、Android框架,以及其他的第三方框架。
    理论上说,扩展函数非常简单,他就是一个类的成员函数,不过定义在类的外面,我们来看一个例子

    fun String.lastChar() : Char = this.get(this.length - 1)
    接收者类型是由扩展函数定义的,接收者对象是该类型的一个实例
    fun main(args:Array<String>){
        println("kotlin".lastChar())
    }
    

    我们可以看到,String就是接收者类型,而"kotlin"就是接收者对象

    注意:扩展函数并不允许你打破它的封装性。和在类内部定义的方法不同的是,扩展函数不能访问私有的或者是受保护的成员。

    导入和扩展函数

    定义一个扩展函数,他不会自动地在整个项目范围内生效。所以,如果要使用它,需要进行导入,就像其他任何的类或者函数一样。这是为了避免偶然性的命名冲突。Kotlin允许用和导入类一样的语法来导入单个函数

    import base.lastChar
    val c = "kotlin".lastChar()
    
    当然也可以用 * 来导入
    
    import base.*
    val c = "kotlin".lastChar()
    
    可以使用关键字as来修改导入的类或者函数名称
    import base.lastChar as last
    val c = "kotlin".last()
    

    当在不同的包中,有一些重名的函数,再导入时给它重新命名就显得很有必要了,就可以在同一个文件中使用它们。
    对于扩展函数,kotlin的语法要求使用简短的名称,那么关键字as就是解决命名冲突问题的唯一方式

    从Java中调用扩展函数

    实质上,扩展函数时静态函数。调用这个静态函数,然后把接收者对象作为第一个参数穿进去即可。
    假设这个方法声明在一个叫作StringUtil.kt的文件中,那么在java中调用的时候

    char c = StringUtilKt.lastChar("java");
    

    这个扩展函数被声明为顶层函数,所以它会被编译为一个静态函数。在Java中导入lastChar函数,就可以直接使用它了。

    作为扩展函数的工具函数

    学习和了解了上面知识点,我们可以写一个joinToString函数的中级版本了,它和kotlin标准库中看到的一模一样

    fun <T> Collection<T>.joinToString(separator: String=",",
                                            prefix: String="",
                                            postfix: String=""):String {
            val result = StringBuilder(prefix)
            for ((index,element) in this.withIndex()){
                if (index > 0) result.append(separator)
                result.append(element)
            }
            result.append(postfix)
            return result.toString()
        }
    
        val list = listOf(1,2,3)
        println(list.joinToString(separator = "; ",prefix = "(",postfix = ")"))
    

    这样元素的集合类添加一个扩展函数,然后给所有的参数添加一个默认值。然后就可以像使用一个类的成员函数一样去调用joinToString了

    val list = listOf(1,2,3)
    println(list.joinToStrings(" "))
    //1 2 3
    

    扩展函数无非就是静态函数的一个高效的语法糖,可以使用更具体的类型来作为接收者类型,而不是一个类.假设要一个join函数,只能有字符串集合来触发

    fun Collection<String>.join(separator: String=",",
                                    prefix: String="",
                                    postfix: String="") = joinToStrings(separator, prefix, postfix)
    
    println(listOf("one","two","three").join(";"))
    //one;two;three
    
    //不能用其他类型的对象列表来调用,会报错
    

    扩展函数的静态性质也决定了扩展函数不能被子类重写

    不可重写的扩展函数

    扩展函数并不是类的一部分,它是声明在类之外的。尽管可以给基类和子类都分别定义一个同名的扩展函数,但是当这个函数被调用是,它会调用哪一个呢?这里它是由该变量的静态类型所决定的,而不是这个变量的运行时类型

    fun Any.showOff() = println("any")
    fun String.showOff() = println("string")
    val str:Any = String()
    str.showOff()   //any
    

    当调用一个类型为Any的变量的showOff函数时,对应的扩展函数会被调用,尽管实际上这个变量现在是一个String的对象

    扩展属性

    扩展属性提供了一种方法,用来扩展类的API,可以用来访问属性,用的是属性语法而不是函数的语法。

    我们将上面的lastChar函数转换成一个属性试试

    声明一个扩展属性
    val String.lastChar:Char get() = get(length -1)
    
    

    可以看到和扩展函数一样,扩展属性也像接收者的一个普通的成员属性一样。

    声明一个可变的扩展属性
    var StringBuilder.lastChar:Char
        get() = get(length -1)
        set(value) {
            this.setCharAt(length-1,value)
        }
        
     val sb = StringBuilder("kotlin!")
        sb.lastChar = '?'
        println(sb) //kotlin?
    

    关于扩展函数和属性的概念我们已经了解了一些,我们回到集合的话题,看一些库提供的能帮助你处理集合的函数,以及伴随而来的语言特性

    处理集合:可变参数、中缀调用和库的支持

    我们来学习Kotlin标准库中用来处理集合的一些方法

    扩展Java集合的API

    获取列表中最后一个元素并找到数字集合中的最大值

    val strings:List<String> = listOf("first","second","three")
    println(strings.last())
        
    val numbers: Collection<Int> = setOf(1,2,3)
    println(numbers.max())
    

    函数last和max都被声明成了扩展函数,许多扩展函数在Kotlin标准库中都有声明

    可变参数:让函数支持任意数量的参数

    Kotlin的可变参数与java类似,但语法略有不同:
    当需要传递的参数已经包装在数组中时,调用该函数的语法。在Java中可以按原样传递数组,而Kotlin则要求显式地解包数组,以便每个数组元素在函数中能作为单独的参数来调用。这被称为展开运算符。使用的时候在对应的参数前面放一个*

    val list = listOf("args:",*args)
    println(list)
    
    

    上面代码通过展开运算符,可以在单个调用中组合开自数组的值和某些固定值。在java中并不支持。

    键值对的处理:中缀调用和解构声明

    可以用mapOf函数来创建map:

    val map = mapOf(1 to "one",7 to "seven",53 to "fifty-three")
    
    

    代码中的单词 to 不是内置结构,而是一种特殊的函数调用,被称为中缀调用.
    在中缀调用中,没有添加额外的分隔符,函数名称是直接放在目标对象名称和参数之间的。

    下面两种调用方式是等价的
    1.to("one")     //一般to函数的调用
    1 to "one"      //使用中缀符号调用to函数
    

    中缀调用可以与只有一个参数的函数一起调用,无论是普通的函数还是扩展函数。要允许使用中缀符号调用函数,需要使用infix修饰符来标记它。下面是一个简单的to函数的声明:

    infix fun Any.to(other: Any) = Pair(this,other)
    

    to 函数会返回一个Pair类型的对象,Pair是Kotlin标准库中的类,它会用来表示一对元素。

    字符串和正则表达式的处理

    Kotlin字符串与Java字符串完全相同。Kotlin通过提供一系列游泳的扩展函数,使标准java字符串使用起来更加方便。

    分割字符串

    Kotlin提供了一个名为split的具有不同参数的重载的扩展函数。用来承载正则表达式的值需要一个Regex类型,而不是String。
    这样确保了当有一个字符串传递给这些函数的时候,不会被当作正则表达式。

    println("12.345-6.A".split("\\.|-".toRegex()))   //显式的创建一个正则表达式
    

    在Kotlin中,可以使用扩展函数toRegex将字符串转换为正则表达式。但是对于一些简单的情况,就不需要使用正则表达式了。
    Kotlin中的splite扩展函数的其他重载支持任意数量的纯文本字符串分隔符

    println("12.345-6.A".split(".","-"))        //指定多个分隔符
    

    这样的结果是想同的

    正则表达式和三重引号的字符串

    解析字符串在Kotlin中很容易,不需要正则表达式,我们来看代码

    fun parsepath(path:String){
        val directory = path.substringBeforeLast("/")
        val fullName = path.substringAfterLast("/")
    
        val fileName = fullName.substringBeforeLast(".")
        val extension = fullName.substringAfterLast(".")
        println("Dir: $directory,name: $fileName,ext: $extension")
    }
    parsepath("/Users/yole/kotlin-book/chapter.adoc")
    //Dir: /Users/yole/kotlin-book,name: chapter,ext: adoc
    

    使用substringBeforeLast和substringAfterLast函数将一个路径分割为目录、文件名和扩展名

    如果你确实要使用正则表达式来完成,也可以使用Kotlin标准库。

    fun parsepaths(path:String){
        val regex = """(.+)/(.+)\.(.+)""".toRegex()
        val matchResult = regex.matchEntire(path)
        if (matchResult != null) {
            val (directory,filename,extension) = matchResult.destructured
            println("Dir: $directory,name: $filename,ext: $extension")
        }
    }
    

    这个函数中,正则表达式写在一个三重引号的字符串中。在这样的字符串中,不需要对任何字符进行转义,包括反斜线,所以可以有用.而不是\.来表示点

    让你的代码更整洁:局部函数和扩展

    我们来看看,怎么使用局部函数,来解决常见的代码重复问题

    ## 带重复代码的函数
    class Users(val id:Int,val name:String,val address: String)
    
    fun saveUser(users: Users){
        if (users.name.isEmpty()){
            throw IllegalArgumentException("不能保存用户${users.id}为空的名字")
        }
        if (users.address.isEmpty()){
            throw IllegalArgumentException("不能保存用户${users.address}为空的地址")
        }
    }
    saveUser(Users(1,"",""))
    //Exception in thread "main" java.lang.IllegalArgumentException: 不能保存用户1为空的名字
    

    我们如果将验证码放到局部函数中,可以摆脱重复,并保持清晰的代码结构.
    局部函数可以访问所在函数中的所有参数和变量

    class Users(val id:Int,val name:String,val address: String)
    fun saveUsers(users: Users){
        fun validate(value: String, fieldName:String){
            if (value.isEmpty()){
                throw IllegalArgumentException("不能保存用户${users.id}为空的$fieldName")
            }
        }
        
        validate(users.name,"Name")
        validate(users.address,"Address")
    }
    

    我们还可以继续改进,把验证逻辑放在Users类的扩展函数中

    class Users(val id:Int,val name:String,val address: String)
    fun Users.validateBeforeSave() {
        fun validate(value: String, fieldName:String){
            if (value.isEmpty()){
                throw IllegalArgumentException("不能保存用户$id 为空的$fieldName")
            }
        }
    
        validate(name,"Name")
        validate(address,"Address")
    }
    //调用扩展函数
    fun saveUser(users: Users){
        users.validateBeforeSave()
    }
    

    扩展函数也可以被声明为局部函数,所以这里可以将users.validateBeforeSave作为局部函数放在saveUser中,但是深度嵌套的局部函数让人费解,因此我们一般不建议使用多层嵌套

    小结

    • Kotlin没有定义自己的集合类,而是在java集合类的基础上提供了更丰富的API
    • Kotlin可以给函数参数定义默认值,这样大大降低了重载函数的必要性,而且命名参数让多函数的调用更加易读。
    • Kotlin允许更灵活的代码结构:函数和属性都可以直接在文件中声明,而不仅仅是在类中作为成员
    • Kotlin可以用扩展函数和属性来扩展任何类的API,包括在外部库中定义的类
    • 中缀调用提供了处理单个参数的,类似调用运算符方法的简明语法
    • Kotlin为普通字符串和正则表达式都提供了大量的方便字符串处理的函数
    • 局部函数帮助你保持代码整洁的同时,避免重复

    微信公众号:aduroidpc

    微信公众号logo

    相关文章

      网友评论

      • 唐_夏影:Emmmm...终于更新了T^T,一直在等待你王者归来
        Rocdo::joy:最近有点忙,简书上更新的少,我在我博客里更新了几篇,aduroidpc.top

      本文标题:Kotlin函数的定义与调用

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