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 <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为普通字符串和正则表达式都提供了大量的方便字符串处理的函数
  • 局部函数帮助你保持代码整洁的同时,避免重复
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容