Kotlin函数的定义与调用

我们学习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  joinToString(collection: Collection,
                         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  joinToString(collection: Collection,
                         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){
    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  Collection.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.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 = listOf("first","second","three")
println(strings.last())
    
val numbers: Collection = 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

Kotlin函数的定义与调用_第1张图片
微信公众号logo

你可能感兴趣的:(Kotlin函数的定义与调用)