扩展函数及其匿名扩展函数笔记

个人感觉kotlin语言的魅力在于2点,一是基于函数编程,另外一个就是编译器的强大。

对于Standard提供的泛型扩展函数的叫法蛮多的,有叫高阶函数的,有叫标准函数的,我还是更喜欢叫他泛型扩展函数,他的实现也是基于扩展函数特性嘛。无论是apply()还是let,用得蛮多的,但是还是没有感受到那种函数为第一等公民的舒适感,试图把一个业务逻辑转换为函数式编程,整成一个链式调度,真正实现一行代码实现一个业务诉求。在Java中可能需要定义很多接口或者基于泛型接口处理,所以说,这个玩意有一个核心问题,就是泛型处理逻辑得转得快,同时Java哪怕是接入Rxjava,能一口气写出一个链式调度的情况还是蛮少的。

ok,回到kotlin,kotlin 提供了丰富的扩展函数,这使得我们写链式调度更容易了,但是内置的扩展函数有的时候并不能完全满足业务诉求,当我们对于泛型和调度不熟悉的时候,就感觉自己写一套适合自己业务诉求的链式调度较为困难。那么,就从最简单的泛型扩展函数开始学习。

正文

这里有几个大的知识前提,我们写链式调度,得知道函数可以作为什么?

函数的基础知识

都是一些基础知识。

函数可以直接定义到非class 里面

这是常识了,比如apply等泛型扩展函数就是没有定义到一个class。我们定义扩展函数的时候也没有定义到一个class里面。那么提问:扩展函数可以定义到一个class里面吗 答案是肯定的,例如:

fun main() {
 Demo().printCode()
}
class Demo {
    fun printCode(){
        "printCode".code()
    }
     fun String.code(){
        println("-------------${this}")
    }
}

我们在Demo class 里面对于String扩展了一个code 函数。那么在函数main 里面没法直接调用,得再class 内部进行调用。

函数可以作为一个一个变量的

例如这个,fun1是一个无入参,无返回值的函数。

val fun1:()->Unit={
    
}

函数可以作为一个函数的入参

fun main() {
 fun2(fun1)
}

val fun1:()->Unit={
    println("----------")
}

fun fun2(f:()->Unit):Unit{
    // 这么可以调用f
    f()
    // 这么也可以调用f
    f.invoke()
}

可以看到,我们定义了fun1,没有返回参数,定义fun2,入参是一个没有入参没有返回值的函数。然后再fun2中对于入参进行执行。那么提问:我们把fun1()的入参和返回参数改变了,fun2(fun1)会报错吗? ,答案:肯定报错啊,类型都不一样了。

函数可以作为一个函数的返回参数

fun main() {
    // 执行
    fun2().invoke(5)
    // 执行
    fun2()(5)
}

val fun3:(Int)->Int={
    println(it)
    1
}

fun fun2():(Int)->Int{
   return fun3
}

可以看到,fun2的返回参数是一个函数,并且函数的入参和返回参数都是Int。调用方式也又两种。

函数中可以继续写函数

我们直接再上面函数作为返回参数上面改。

fun main() {
    // 执行
    fun2().invoke(5)
    // 执行
    fun2()(5)
}

fun fun2():(Int)->Int{
    fun fun5(){
        println("fun5")
    }
    val fun6:()->Unit={
        
    }
    fun5()
    fun6()
   return fun(it: Int):Int{
       println("...........")
       return it
   }
}

可以看到,我们再fun2里面定义了3哥方法,fun5是正常定义函数,fun6是将函数作为一个变量。return 返回了一个入参和返参是Int的函数。

泛型的基础知识

这里就只是有基础,比如说,in、out、* 等就就不阐述了。比如说我们定义一个函数。至于为什么写这个,因为kotlin 有类型推断,所以说,有的时候,有的代码并没有写上泛型类型。

入参是泛型的

fun main() {
    // string
    fun1("---")
    // int
    fun1(1)
    // float
    fun1(1f)
    // 函数
    fun1{}
}
fun <T> fun1(it:T){
    println(it)
}

可以看到,当我们入参确定的时候,T的类型就已经被kotlin 推断出来了。那么多个参数也是同理了

fun <T,R,A,B,C,D> fun3(t:T,r:R,a: A,b: B,c: C,d: D){
    
}

fun3("",1,1f,true, listOf("")){} 这么也可以推断出来。

返回参数是泛型

fun main() {
    val result= fun2("")
}

fun <T> fun2(it:T):T{
    return it
}

kotlin 类型推断出来 result 的类型就是string

入参和返参不一致

fun main() {
    val result= fun2<String,Int>("5")
     val result1:Int= fun2("5")
}

fun <T,R>  fun2(it:T):R{
    return it as R
}

可以看到,我们调用的时候,就得写上具体类型了。result1这么定义了接受者类型也可以。

Standard 下的扩展函数

上面水了这么多的函数的基础知识和泛型函数都只是为了我们更快的读懂Standard 定义的扩展函数。打开这个文件就可以看到几乎每个函数上都有一个注解@kotlin.internal.InlineOnly:

这是一个注解(annotation),通常在Kotlin的内部函数或者内部类中使用。这个注解的作用是告诉Kotlin编译器,这个函数或者类只能在内联函数中使用。

换句话说,如果你尝试在非内联函数中使用这个函数或者类,编译器将会报错。内联函数是Kotlin中的一种特殊函数,它可以在编译时期将函数调用的代码替换为函数体中的代码,以减少运行时的函数调用开销。因此,内联函数通常用于性能敏感的代码,例如循环或者高频调用的函数。

请注意,你通常不需要自己使用@kotlin.internal.InlineOnly注解,因为Kotlin编译器会自动处理这些情况。这个注解主要用于Kotlin编译器内部,以及一些需要特殊处理的库函数。

还比如说,这里面定义的所有函数都是inline函数。

TODO

这个并不是扩展函数。只是因为他写到了这里。我们继承或者实现某个类的时候,编辑器就会自动帮我们在函数上添加这个代码。如果没有删除这个代码,运行到这里程序就退出了。

public inline fun TODO(reason: String): Nothing = throw NotImplementedError("An operation is not implemented: $reason")

可以看到,他其实是抛出了一个NotImplementedError异常。那么可以学到什么呢

  • 这是一个inline函数。编译后这个代码会整体拷贝到调用的函数里面去。

  • throw 抛出异常和JAVA类似。

  • 但是返回类型不是Unit而是noting

    Kotlin中的Unit和Nothing是两种特殊类型,主要用于函数的返回类型声明。

    Unit类型在Kotlin中相当于Java的Void,表示一个没有返回值的函数。然而,Unit不仅是Void的等价物,它还是一个完备的类型,可以作为类型参数使用。这意味着在Kotlin中,可以将Unit类型作为函数或方法的参数类型或返回类型。

    与Unit相比,Nothing在Kotlin中是一个更为特殊的类型。它用于声明那些不会正常返回的函数。在函数声明中使用Nothing类型,意味着这个函数永远不会正常返回,只会抛出异常。这是Kotlin为提高函数调用的可读性而采用的一种方式,也是Java中没有的。

    总结来说,Unit和Nothing在Kotlin中都主要用于函数返回类型的声明,但它们的使用场景和含义有所不同。Unit主要表示一个没有返回值的函数,而Nothing则用于声明那些不会正常返回、只会抛出异常的函数。

contract

至于为什么先水这个,因为大多数泛型扩展函数的执行里面都有一段这样的代码:

contract {
    callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}

这是一个Kotlin的函数签名,函数名为callsInPlace,它接受两个参数:

  1. 一个lambda表达式,该表达式的类型为Function<R>,其中R是函数的返回类型。这个lambda表达式是你希望在原地(in place)执行的代码。
  2. 一个InvocationKind枚举值,类型为InvocationKind,默认值为InvocationKind.UNKNOWN。这个参数用于指定该函数的调用约定。

返回值类型为CallsInPlace

这个函数可能是用于在函数式编程中指定函数的调用约定,并在编译时进行一些优化。

重点,这个玩意应该不用管,包含这种代码的函数复制出来换一个名字,添加提示的注解没有实现他描述的效果。这也应该是很多直播课堂上老师也没有讲这个玩意的原因

但是contract{} 并不是学不到东西,总不能白来一次嘛,可以看到他源码:

public inline fun contract(builder: ContractBuilder.() -> Unit) { }

提问:ContractBuilder.() 这是什么东西?

这种东西咋理解呢?结合上面的代码 callsInPlace(block, InvocationKind.EXACTLY_ONCE),他其实是等价于``ContractBuilder(). callsInPlace(block, InvocationKind.EXACTLY_ONCE)。 所以这种 xxxx.()` 代表执行这个class 的函数,如xxxx 里面没有这个函数,就编译不通过。

那么这个玩意能做什么呢?apply() 和with() 就是这种写法。主要是限制这个lambda 内部的调用函数范围,减少代码量,所以apply 和with里面,逻辑上不要写和调用对象无关的函数。至于更详细的,后续再整。

run(block: () -> R): R 与 T.run(block: T.() -> R): R

没看源码之前,还在想,为啥run{}1.run{} 这种是怎么整出来的。结果他定义了两个函数。

public inline fun <R> run(block: () -> R): R {
    return block()
}

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

结合上面的知识点,删除掉干扰我们对代码,可以看到两个run函数都是返回了block() 的执行结果。区别在于run{} 因为没有调度对象,所以返回类型是不变的。而objet.run{},返回类型是基于逻辑运算的。不写return,返回之后一行代码的执行结果。

with(receiver: T, block: T.() -> R): R

直接看源码:

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

可以看到,这个函数需要传递一个对象。然后直接调用receiver.block()。所以这个玩意可以不用写 this就可以直接调用到传递进来receiver 对象的函数,同时有一个返回值,这个返回值基于逻辑返回。不写return,返回之后一行代码的执行结果。

T.apply(block: T.() -> Unit): T

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

可以看到:他执行block 之后,直接返回了他自己本身

T.also(block: (T) -> Unit): T

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

可以看到,他和apply类似返回的是this 本身。同时将this作为入参传递到了block()函数内部。那么这个可以做什么呢?

also 常常被用于以函数式编程风格来处理对象。它允许你在一个对象上执行额外的操作,然后返回这个对象本身,无需使用中间变量。例如,当你需要在一个对象上进行多次操作时,你可以使用 also 将这些操作串联起来。

<T, R> T.let(block: (T) -> R): R

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

let 函数将调用者对象传递给了block,同时返回了block的结果。

T.takeIf(predicate: (T) -> Boolean): T?

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    return if (predicate(this)) this else null
}

takeif,将调用者对象传递到predicate函数中,predicate返回了一个boolean类型,如果返回值是true 就是返回当前对象本身,否则返回null。那么这个就可以做校验逻辑。而takeUnless 逻辑与takeif 刚好相反,如果返回false 则返回对象本身,否则就返回空。

repeat(times: Int, action: (Int) -> Unit)

public inline fun repeat(times: Int, action: (Int) -> Unit) {
    for (index in 0 until times) {
        action(index)
    }
}

可以看到,这个就是设置一个循环的最大值,然后从0开始循环到最大值,然后每次循环都调用一次action。

总结

从源码的角度上讲,Standard定义个函数还是比较简单的,主要是泛型的定义,函数的调用,然后是泛型对象的函数调用。最终回归本质,基于泛型定义了一大片扩展函数。而扩展函数的实现方式也较为简单,主要还是认知扩展,毕竟看得懂代码还是蛮重要的。至于扩展函数在jvm 上的原理:大概是定义了一个函数,将调用者传递进去,把扩展函数的实现在新的函数里面实现了。

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

推荐阅读更多精彩内容