个人感觉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
,它接受两个参数:
- 一个lambda表达式,该表达式的类型为
Function<R>
,其中R
是函数的返回类型。这个lambda表达式是你希望在原地(in place)执行的代码。 - 一个
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 上的原理:大概是定义了一个函数,将调用者传递进去,把扩展函数的实现在新的函数里面实现了。