我们学习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为普通字符串和正则表达式都提供了大量的方便字符串处理的函数
- 局部函数帮助你保持代码整洁的同时,避免重复