[基础篇]Kotlin第三讲-扩展函数及其他

集合的创建与遍历

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

创建集合

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

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类里直接写

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为我们实现了这样的能力:扩展函数。把上述方法生命为一个拓展函数,如下所示:

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点的理解,我们举一个例子

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在此基础上,通过扩展函数扩展字符串方法,通过默认参数实现重载效果。

/**
 * 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)

三重引号的字符串

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

看一个佛祖镇楼的例子

    val bless = """
                   _ooOoo_
                  o8888888o
                  88" . "88
                  (| -_- |)
                  O\  =  /O
               ____/`---'\____
             .'  \\|     |//  `.
            /  \\|||  :  |||//  \
           /  _||||| -:- |||||-  \
           |   | \\\  -  /// |   |
           | \_|  ''\---/''  |   |
           \  .-\__  `-`  ___/-. /
         ___`. .'  /--.--\  `. . __
      ."" '<  `.___\_<|>_/___.'  >'"".
     | | :  `- \`.;`\ _ /`;.`/ - ` : | |
     \  \ `-.   \_ __\ /__ _/   .-` /  /
======`-.____`-.___\_____/___.-`____.-'======
                   `=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         佛祖保佑       永无BUG
         """
    println(bless)     

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

小结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容