Kotlin函数的定义与调用
接下来我们学习Kotlin的一个重要环节,函数的声明和调用。将从Kotlin集合、字符串和正则表达式作为重点,先来看看如何在Kotlin中创建集合
在Kotlin中创建集合
我们可以创建一个list或者map
1 2 3 4 5 6 7
| 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还不止这些。例如:
1 2 3 4 5
| val strings = listOf("first","second","fourteenth") println(strings.last()) //fourteenth val numbers = setOf(1,14,2) println(numbers.max()) //14
|
在讨论如何操作last和max这两个神奇的函数之前,我们先来学习一下函数声明
让函数更好的调用
java的集合都有一个默认的toString实现,但是它的格式化的输出是固定的,往往不是我们需要的样子
1 2
| val lists = listOf(1,2,3) println(lists) //[1, 2, 3]
|
假如我们要得到这样的打印效果(1;2;3) ,在Java项目会使用第三方库来完成。在Kotlin中,他的标准库中有一个专门的函数来处理这种情况。
下面的joinToString函数就展现了通过在元素间添加分割符号,在最前面添加前缀,在最末添加后缀的方式把集合元素逐个添加到一个StringBuilder的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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中,可以做的更优雅
1
| joinToString(lists,separator = " ",prefix = " ",postfix = ".")
|
当调用一个Kotlin定义的函数时,可以显式地标明一些参数的名称。如果调用一个函数时,指明了一个参数名称,为了避免混淆,之后所有的参数都需要标明名称。
默认参数值
java的另一个普遍存在的问题时,一些类的重载函数实在太多了。
在Kotlin中,可以在声明函数的时候,指定参数的默认值,这样就可以避免创建重载函数,我们尝试改进一下前面的joinToString函数
1 2 3 4 5 6 7 8
| 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框架,以及其他的第三方框架。
理论上说,扩展函数非常简单,他就是一个类的成员函数,不过定义在类的外面,我们来看一个例子
1 2 3 4 5
| fun String.lastChar() : Char = this.get(this.length - 1) 接收者类型是由扩展函数定义的,接收者对象是该类型的一个实例 fun main(args:Array<String>){ println("kotlin".lastChar()) }
|
我们可以看到,String就是接收者类型,而”kotlin”就是接收者对象
注意:扩展函数并不允许你打破它的封装性。和在类内部定义的方法不同的是,扩展函数不能访问私有的或者是受保护的成员。
导入和扩展函数
定义一个扩展函数,他不会自动地在整个项目范围内生效。所以,如果要使用它,需要进行导入,就像其他任何的类或者函数一样。这是为了避免偶然性的命名冲突。Kotlin允许用和导入类一样的语法来导入单个函数
1 2 3 4 5 6 7 8 9 10 11
| 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中调用的时候
1
| char c = StringUtilKt.lastChar("java");
|
这个扩展函数被声明为顶层函数,所以它会被编译为一个静态函数。在Java中导入lastChar函数,就可以直接使用它了。
作为扩展函数的工具函数
学习和了解了上面知识点,我们可以写一个joinToString函数的中级版本了,它和kotlin标准库中看到的一模一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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了
1 2 3
| val list = listOf(1,2,3) println(list.joinToStrings(" ")) //1 2 3
|
扩展函数无非就是静态函数的一个高效的语法糖,可以使用更具体的类型来作为接收者类型,而不是一个类.假设要一个join函数,只能有字符串集合来触发
1 2 3 4 5 6 7 8
| fun Collection<String>.join(separator: String=",", prefix: String="", postfix: String="") = joinToStrings(separator, prefix, postfix) println(listOf("one","two","three").join(";")) //one;two;three //不能用其他类型的对象列表来调用,会报错
|
扩展函数的静态性质也决定了扩展函数不能被子类重写
不可重写的扩展函数
扩展函数并不是类的一部分,它是声明在类之外的。尽管可以给基类和子类都分别定义一个同名的扩展函数,但是当这个函数被调用是,它会调用哪一个呢?这里它是由该变量的静态类型所决定的,而不是这个变量的运行时类型
1 2 3 4
| fun Any.showOff() = println("any") fun String.showOff() = println("string") val str:Any = String() str.showOff() //any
|
当调用一个类型为Any的变量的showOff函数时,对应的扩展函数会被调用,尽管实际上这个变量现在是一个String的对象
扩展属性
扩展属性提供了一种方法,用来扩展类的API,可以用来访问属性,用的是属性语法而不是函数的语法。
我们将上面的lastChar函数转换成一个属性试试
1 2
| 声明一个扩展属性 val String.lastChar:Char get() = get(length -1)
|
可以看到和扩展函数一样,扩展属性也像接收者的一个普通的成员属性一样。
1 2 3 4 5 6 7 8 9 10
| 声明一个可变的扩展属性 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
获取列表中最后一个元素并找到数字集合中的最大值
1 2 3 4 5
| 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则要求显式地解包数组,以便每个数组元素在函数中能作为单独的参数来调用。这被称为展开运算符。使用的时候在对应的参数前面放一个*
1 2
| val list = listOf("args:",*args) println(list)
|
上面代码通过展开运算符,可以在单个调用中组合开自数组的值和某些固定值。在java中并不支持。
键值对的处理:中缀调用和解构声明
可以用mapOf函数来创建map:
1
| val map = mapOf(1 to "one",7 to "seven",53 to "fifty-three")
|
代码中的单词 to 不是内置结构,而是一种特殊的函数调用,被称为中缀调用.
在中缀调用中,没有添加额外的分隔符,函数名称是直接放在目标对象名称和参数之间的。
1 2 3
| 下面两种调用方式是等价的 1.to("one") //一般to函数的调用 1 to "one" //使用中缀符号调用to函数
|
中缀调用可以与只有一个参数的函数一起调用,无论是普通的函数还是扩展函数。要允许使用中缀符号调用函数,需要使用infix修饰符来标记它。下面是一个简单的to函数的声明:
1
| infix fun Any.to(other: Any) = Pair(this,other)
|
to 函数会返回一个Pair类型的对象,Pair是Kotlin标准库中的类,它会用来表示一对元素。
字符串和正则表达式的处理
Kotlin字符串与Java字符串完全相同。Kotlin通过提供一系列游泳的扩展函数,使标准java字符串使用起来更加方便。
分割字符串
Kotlin提供了一个名为split的具有不同参数的重载的扩展函数。用来承载正则表达式的值需要一个Regex类型,而不是String。
这样确保了当有一个字符串传递给这些函数的时候,不会被当作正则表达式。
1
| println("12.345-6.A".split("\\.|-".toRegex())) //显式的创建一个正则表达式
|
在Kotlin中,可以使用扩展函数toRegex将字符串转换为正则表达式。但是对于一些简单的情况,就不需要使用正则表达式了。
Kotlin中的splite扩展函数的其他重载支持任意数量的纯文本字符串分隔符
1
| println("12.345-6.A".split(".","-")) //指定多个分隔符
|
这样的结果是想同的
正则表达式和三重引号的字符串
解析字符串在Kotlin中很容易,不需要正则表达式,我们来看代码
1 2 3 4 5 6 7 8 9 10
| 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标准库。
1 2 3 4 5 6 7 8
| 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") } }
|
这个函数中,正则表达式写在一个三重引号的字符串中。在这样的字符串中,不需要对任何字符进行转义,包括反斜线,所以可以有用.而不是\.来表示点
让你的代码更整洁:局部函数和扩展
我们来看看,怎么使用局部函数,来解决常见的代码重复问题
1 2 3 4 5 6 7 8 9 10 11 12 13
| ## 带重复代码的函数 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为空的名字
|
我们如果将验证码放到局部函数中,可以摆脱重复,并保持清晰的代码结构.
局部函数可以访问所在函数中的所有参数和变量
1 2 3 4 5 6 7 8 9 10 11
| 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类的扩展函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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为普通字符串和正则表达式都提供了大量的方便字符串处理的函数
- 局部函数帮助你保持代码整洁的同时,避免重复
上一篇:CSS背景和列表
下一篇:RxJava操作符及相关案例