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

本文介绍了Kotlin语言的魅力,特别是其函数编程特性,以及泛型扩展函数如apply、let、also的使用。作者探讨了如何在Kotlin中利用这些扩展函数进行链式调度,对比了Java的实现方式,并提到了`InlineOnly`注解和`Unit`、`Nothing`类型在函数返回中的作用。
摘要由CSDN通过智能技术生成

前言

个人感觉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 上的原理:大概是定义了一个函数,将调用者传递进去,把扩展函数的实现在新的函数里面实现了。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值