Kotlin第三讲-拓展函数及其他

集合的创建与遍历

Kotlin没有采用它自己的集合类,而是采用标准的Java集合类。大部分Kotlin的标准库是由Java类的拓展函数组成的。

创建集合

Kotlin中对集合增加了一个新的接口MutableList,实现该接口的集合是可变集合。Kotlin中,集合分为可变集合和不可变集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface MutableList<E> : List<E>, MutableCollection<E> {

override fun add(element: E): Boolean

override fun remove(element: E): Boolean

override fun addAll(elements: Collection<E>): Boolean

public fun addAll(index: Int, elements: Collection<E>): Boolean

override fun removeAll(elements: Collection<E>): Boolean
override fun retainAll(elements: Collection<E>): Boolean
override fun clear(): Unit

public operator fun set(index: Int, element: E): E

public fun add(index: Int, element: E): Unit

public fun removeAt(index: Int): E

override fun listIterator(): MutableListIterator<E>

override fun listIterator(index: Int): MutableListIterator<E>

override fun subList(fromIndex: Int, toIndex: Int): MutableList<E>
}

MutableList接口提供了增加和删除集合元素的能力。

创建不可变集合

val list = listOf<String>("a", "b", "c")
val letter = list[0]
var list1 = listOfNotNull<Int>(1, 4, 8)

创建可变集合

val list2 = arrayListOf<Int>(1, 2, 3, 4)
list2.set(0, 10)
list2[0] = 10
list2.add(5)
println("list2 = $list2")

val list3 = mutableListOf("a", "b", "c")
list3.add("d")
println("e = $list3")
println("last element = ${list3.last()}")

val list4 = mutableMapOf<String, String>("1" to "A", "2" to "B")
val list5 = mutableSetOf<String>("B", "C", "D")

参数

Kotlin的函数比Java函数强大的地方之一是入参可以有默认值,即默认参数;

在Kotlin调用函数时,可以指定入参的名称,即命名参数;

与Java不同,Koltin表示可变参数,不是参数后面加三个点,,而是在入参前加vararg关键词即可。Kotlin中存在一个展开运算符 – *(星号),它和可变参数搭配使用;作用是把一个数组展开成可变参数传入

详细说明,可看这篇文章Kotlin里的输入参数

顶层函数与属性

  1. 很多代码并不能归属到任何一个类中,有时一个操作对应两个不同的类的对象,而且重要性相差无几。
  2. 有时存在一个基本的对象,但不想通过实例函数来添加操作,让它的API继续膨胀。

在Java里,我们使用静态方法。Kotlin里没有static修饰词,它一种方式,使用顶层函数来实现相同的效果。

顶层函数

实现一个功能,把集合中元素添加前缀,后缀,用分隔符间隔展示

在kt类里直接写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const val counter: Int = 0

fun <T> joinToString(
collection: Collection<T>,
separator: String,
prefix: String,
postfix: String
): String {
val sb = StringBuffer(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) {
sb.append(separator)
}
sb.append(element)
}
sb.append(postfix)

return sb.toString()
}

顶层函数是包内成员,包内直接访问。若包外访问,需要import(IDEA等开发工具会为你自动import)

顶层函数是都是静态函数,默认函数所在的文件名加KT作为容器类,比如上述joinToString方法是在Example3_2文件名下的顶层函数,在Java里调用是时

Example3_2Kt.joinToString(collection, ",", "[", "]");

如果我想更改调用静态方法的容器类的类名为StringUtils,则需要在kotlin文件里添加

@file:JvmName("StringUtils")

这时候调用形式如下:

StringUtils.joinToString(collection, "," , "[", "]");

顶层属性

counter就是顶层属性,等效于容器类的静态成员变量,即Java里如下写法

public static final int counter = 0;    

拓展函数与属性

拓展函数基本使用

StringUtils.joinToString(collection, "," , "[", "]");

每次调用上述实现的joinToString方法传入四个参数,有点多,我希望减少入参数量。StringUtils是工具类类名,工具类类名在整个调用过程中是不够高效的。达到优雅的途径之一就是做到高效而简洁。这个工具类具体是什么名字并不会影响这个函数的输入和输出,这个类名的意义是作为joinToString容器的标示,如果能把其中一个入参名作为类名,这个入参同时做两件事:传入自身到函数体里,作为调用的句柄名。

强大的Kotlin为我们实现了这样的能力:扩展函数。把上述方法生命为一个拓展函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
fun <T> Collection<T>.joinToString(separator: String = ",", prefix: String = "(", postfix: String = ")"){
val sb = StringBuilder()
sb.append(prefix)
for((index, element) in this.withIndex()){
if(index > 0){
sb.append(separator)
}
sb.append(element.toString())
}
sb.append(postfix)
}

接收者类型:函数名前的类,上例Collection就是该扩展函数的接收者类型

接收者对象:接收者类的实例,上例this.withIndex方法的this指代的就是Collection对象,是该扩展函数接收者对象

这时候我们要使用joinToString方法,变成这样用了

val list3 = mutableListOf("a", "b", "c")
list3.joinToString("_", "[", "]")

导入范围

需要进行导入扩展函数才能生效,好在开发工具为我们自动导入了。如果定义的扩展函数所在的类和其接收者类型的类在一个包下,可以不需要显式导入。

有一种情况,如果你定义的扩展函数名和其他包里定义的函数名相同,你需要导入全类名以示区分。还有另一种方式,通过as在导入的地方重命名扩展函数名

import sugarya.chapter3.joinToString as jts

这时候,就可以调用jts就相当于调用joinToString方法

扩展函数的本质和特性

其实,Kotlin把上述代码翻译到JVM上运行时。拓展函数的本质是:把接收者对象作为第一个入参的函数。通常我们在顶层位置定义扩展函数,这样扩展函数就能被其他包的文件调用。因此,扩展函数并没有改变接收者类里的代码,扩展函数并不是类的一部分,它是声明在类之外的,却能像成员变量那般使用。

像成员变量那般使用,扩展函数和成员变量不是一回事,它们之间是有区别的

  1. 扩展函数不能访问私有或者受保护的成员,因为接收者对象只是静态方法的一个入参,这个入参有大的访问能力,扩展函数就是多大访问能力。
  2. 扩展函数不能被接收者类的子类重写/继承。前面说了,扩展函数只是静态方法,并不是真实的接收者里的成员,自然也就无法重写了。

对于第2点的理解,我们举一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person(name: String, var age: Int) : Animal(name)

//拓展定义是写在Example2_4.Kt文件里
fun Animal.move(){
println("animal move")
}

fun Person.move(){
println("Person move")
}

val animal: Animal = Person("Kotlin", 5)
animal.move()

输出结果:

animal move

animal.move是拓展函数,转化为静态方法是Example2_4.move(animal),所以,move方法调用的就是Animal类下的move。

扩展属性

扩展属性是对扩展函数能力的弱化/简化使用。相当于Java里第一个参数是接收者对象的静态getter方法和setter方法。扩展函数和扩展属性搭配使用,在扩展函数里访问扩展属性。举个例子

val Animal.length: Int get() = this.name.length * 10

fun Animal.move(){
    println("animal move ${this.length}")
}

扩展函数的应用

看几个扩展函数的应用例子

分割字符串

有一个字符串“ab.cd12.ef”,需要分割成三部分:ab, cd12, ef

使用Java,我们很容易写成这样

String msg = "ab.cd12.ef";
String[] strings = msg.split(".");

java里split()方法入参的字符串表示的正则表达式,在正则表达式里“.”表示任意字符,所以,如果照上面所写,返回为空,找不到字符。

使用Java正确实现是:

String msg = "ab.cd12.ef";
String[] strings = msg.split("\\.");

Kotlin在此基础上,通过扩展函数扩展字符串方法,通过默认参数实现重载效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Splits this char sequence to a list of strings around occurrences of the specified [delimiters].
*
* @param delimiters One or more strings to be used as delimiters.
* @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`.
* @param limit The maximum number of substrings to return. Zero by default means no limit is set.
*
* To avoid ambiguous results when strings in [delimiters] have characters in common, this method proceeds from
* the beginning to the end of this string, and matches at each position the first element in [delimiters]
* that is equal to a delimiter in this instance at that position.
*/
public fun CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> {
if (delimiters.size == 1) {
val delimiter = delimiters[0]
if (!delimiter.isEmpty()) {
return split(delimiter, ignoreCase, limit)
}
}

return rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) }
}

Kotlin实现

"ab.cd12.ef"split(".")

Kotlin里用Regex类表示正则,使用正则实现如下

val regex = Regex("\\.")
val result = "ab.cd12.ef".split(regex.toPattern())

解析字符串在Kotlin变得更容易了,除了split,Kotlin还提供了其他方法,再看一个例子

解析文件路径

解析一个文件路径:“/Users/mine/Documents/MyDocument/Photoes/546294_308008399296566_779316797_n.jpg”,获取目录路径,文件名,文件拓展名

Kotlin代码实现

val msg = "/Users/mine/Documents/MyDocument/Photoes/546294_308008399296566_779316797_n.jpg"
val dirPath = msg.substringBeforeLast("/")
val filePath = msg.substringAfterLast("/")
val fileName = filePath.substringBeforeLast(".")
val extendName = filePath.substringAfterLast(".")

println("directory path = $dirPath, fileName = $fileName, extendName = $extendName")

输出:

directory path = /Users/mine/Documents/MyDocument/Photoes, fileName = 546294_308008399296566_779316797_n, extendName = jpg

局部属性

在Java里,函数的最小的作用域是在一个类里(private修饰的方法),而Kotlin引入局部函数–允许在函数里定义一个函数,让函数(方法)的最小作用域降到一个函数体里。提供更小粒度的复用,这样有什么意义呢?

这样是有意义的。

没有局部函数的特性的Java语言里,对方法最小作用域的组织方式是这样的:一个复杂的类里有很多方法,当方法A里的代码行数很多时,通常拆分出几个新的方法a1,a2,a3等等,这些新的方法之间如果存在整体的逻辑关系,就能组合成一个内部类,a1,a2,a3是该内部类的方法。直接在A里新建内部类并调用即可。外部类的其他方法比如方法B也能方便的调用。

Kotlin局部函数提供了比上述Java更细致的代码组织方式:如果我们只在一个方法A里多次用到,这时候在方法A里,定义a1,a2,a3,在方法A里多次使用方法a1,a2,a3。这种方式相较于上面的内部类组织方式,带来的益处是降低定义内部类带来的语法开销。

对于什么时候引入局部函数,我们有了下述认识:
当需要在方法粒度上多次调用一段逻辑时。具体的场景有,登录验证,表单数据校验。

中缀调用

  1. 对只有一个参数的函数使用中缀调用
  2. 中缀调用的函数,需要对其使用inflix修饰符
  3. 中缀不仅适用于成员函数也适用于扩展函数

举个中缀的例子

val pair: Pair<String, String> = "a" to2 "A"

上面的中缀调用是怎么定义呢?

infix fun <T, V> T.to2(v: V): Pair<T, V> = Pair(this, v)

三重引号的字符串

三重引号字符串不仅在于避免转义符,而且可以包含任何字符,包括换行符。

看一个佛祖镇楼的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    val bless = """
_ooOoo_
o8888888o
88" . "88
(| -_- |)
O\ = /O
____/`---'\____
.' \\| |// `.
/ \\||| : |||// \
/ _||||| -:- |||||- \
| | \\\ - /// | |
| \_| ''\---/'' | |
\ .-\__ `-` ___/-. /
___`. .' /--.--\ `. . __
."" '< `.___\_<|>_/___.' >'"".
| | : `- \`.;`\ _ /`;.`/ - ` : | |
\ \ `-. \_ __\ /__ _/ .-` / /
======`-.____`-.___\_____/___.-`____.-'======
`=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
佛祖保佑 永无BUG
"""
println(bless)

这样控制台按原样格式输出佛祖图

小结

这是Kotlin实战第三章涉及的所有知识点,结合自己的理解整理归纳成本篇文章。