Kotlin-lambda表达式和高阶函数操作符

Posted 程序员超时空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin-lambda表达式和高阶函数操作符相关的知识,希望对你有一定的参考价值。

前言

以一个java老鸟的角度,如何去看 kotlin。 Java源代码应该如何用Kotlin重构。 如何正确学习kotlin并且应用到实际开发中。本文将会探究。

本文分两大块,重难点和潜规则。

重难点:Kotlin中可以独立出来讲解的大块知识点。提供单独Demo。这部分大多数是Kotlin开创的新概念(相比于Java)。

潜规则:Kotlin是谷歌用来替换Java的,它和java百分百完全兼容,但是实际上java转成kotlin之后,需要我们手动修改很多东西,甚至某些部分必须打散重构来达到最优编码。其中,kotlin的某些特性和java不同,甚至完全反转。这部分知识点比较零碎,单独Demo不方便提供,就以小例子的形式来写。

正文大纲

  • 重难点

    • lambda以及操作符
    • 高阶函数以及操作符
    • Kotlin泛型
    • 集合操作
    • 协程
    • 操作符重载
  • 潜规则

    • Kotlin文件和类不存在一对一关系

    • 共生体

    • 继承

    • 修饰符

    • 空指针问题

正文

重难点

lambda表达式

lambda表达式是JDK1.8提出的,目的是为了改善Java中大量出现的只有一个方法的回调函数的累赘写法。这里不讨论Java的lambda. Kotlin中lambda已经融入代码核心,而不是像java一样是个注解+语法糖。

基础:

一个lambda表达式如图:

lambda 表达式的4个部分

  • 外围的大括号
  • 参数列表:x:Int,y:Int
  • 连接符 ->
  • 方法体 x+y

举个栗子

这是一个kotlin文件:

/**
 * 比如说这样,我要给这个doFirst方法传一个执行过程,类型就是一个输入2个Int,输出一个Int的拉姆达表达式
 */
fun calculate(p1: Int, p2: Int, event1: (Int, Int) -> Int, event2: (Int, Int) -> Int) 
    println("执行doFirst")
    println("执行event1 $event1(p1, p2)")
    println("执行event2 $event2(p1, p2)")


//测试lambda表达式
fun main() 
    val sum =  x: Int, y: Int ->
        print("求和 ")
        x + y
    
    val diff =  x: Int, y: Int ->
        print("求差 ")
        x - y
    
    //kotlin里面,可以把拉姆达表示当作一个普通变量一样,去传递实参
    calculate(p1 = 1, p2 = 2, event1 = sum, event2 = diff)

定义了一个calculate函数, p1,p2 是Int,而event1和event2 则是 lambda表达式. 高级语言特性,一等函数公民:函数本身可以被当作普通参数一样去传递,并且调用。那么, kotlin的lambda内核是怎么样的?

上图能够看出,

  1. calculate方法的后面2个参数,被编译成了 Function2 类型。
  2. 执行event1,event2,则是调用其invoke方法
  3. main函数中,出现了null.INSTANCE, 这里比较诡异,INSTANCE应该是用来获取实例的,但是为什么是null.INSTANCE

而看了Function2的源码,简直亮瞎了我的钛合金狗眼:

Function.kt文件中:

Function接口,Function2接口…Function22接口。好了,不要质疑谷歌大佬的设计思路,反正大佬写的就是对的…这里用22个接口(至于为什么是22个,猜想是谷歌觉得不会有哪个脑子秀逗的开发者真的弄22个以上的参数挤在一起吧),表示kotlin开发中可能出现的lambda表达式参数列表。

再举个栗子

给一个Button 设置点击事件,kotlin的写法是:

val btn = Button(this)
val lis = View.OnClickListener  println("111") 
btn.setOnClickListener(lis)

或者:

val btn = Button(this)
btn.setOnClickListener  println("111") 

setOnClickListener 方法的参数是 OnClickListener 接口:

public interface OnClickListener 
    void onClick(View v);

类似这种符合lambda表达式特征的接口,都可以使用上述两种写法来大大简化代码量。

最后一个栗子

不得不提一句,lambda表达式有一个变形,那就是:当lambda表达式作为方法的最后一个参数时,可以lambda表达式放到小括号外面。而如果只有一个参数就是lambda表达式,那么括号可以省略

这个非常重要,不了解这个,很多地方都会感觉很蛋疼。

fun testLambda(s: String, block: () -> String) 
    println(s)
    block()


fun testLambda2(block: () -> String) 
    block()


fun main() 
    testLambda("第一个参数") 
        println("block函数体")
        "返回值"
    
    testLambda2 
        println("block函数体")
        "返回值"
    

总结

Kotlin中lambda表达式可以当作普通参数一样去传递,去赋值,去使用。


高阶函数以及操作符

上文提到,kotlin中lambda表达式已经融入了语言核心,而具体的体现就是高阶函数,把lambda表达式当作参数去使用. 这种将lambda表达式作为参数或者返回值的函数,就叫高阶函数。

官方高阶函数

Kotlin谷歌已经给我们封装了一些高阶函数。

  • run
  • with
  • apply
  • also
  • let
  • takeif 和 takeunless
  • repeat
  • lazy

run函数详解

代码如下(这里为了展示代码全貌,忽视androidStudio的代码优化提示):

class A 
    val a = 1
    val b = "b"

fun testRun() 
    //run方法有两种用法,一个是不依赖对象,也就是作为全局函数
    run<String> //我可以规定返回值的类型
        println("我是全局函数")
        "返回值"
    
    val a = A()
    //另一种则是 依赖对象
    a.run<A,String> //这里同样可以规定返回值的类型
        println(this.a)
        println(this.b)
        "返回值"
    

fun main() 
    testRun()

如上所示:

run函数分为两类

  • 不依赖对象的全局函数。

  • 依赖对象的"类似"扩展函数。

    两者都可以规定返回值类型(精通泛型的话,这里应该不难理解,泛型下一节详解)。

阅读源码:

@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R 
    contract 
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    
    return block()


@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R 
    contract 
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    
    return block()

run函数被重载,参数有所不同

  • 前者 参数类型为 ()->R ,返回值为 R ,函数体内执行block(),并且返回执行结果
  • 后者 参数类型为 T.()->R ,返回值为R , T.() 明显是依赖 T类的(貌似T的扩展函数),返回值依然是R,执行完之后返回结果。
  • 并且,可以发现 两者都是内联函数 inline (执行代码时不进行方法的压栈出栈,而是类似于直接在目标处执行代码段)

所以,前者不需要依赖对象,后者必须依赖对象(因为它是T类的"扩展函数")

使用场景

根据run方法的特性,无论是不是依赖对象,它都是封装了一段代码,而且还是inline的。所以:

  • 如果你不想把一段代码独立成方法,又不想让它们看上去这么凌乱,最重要的是告诉后来的开发者 这段代码是一个整体,不要随便给我在里面插代码,或者拆分。那就用run方法,把他们整合到一个作用域中。

    run 
        println("这是一段代码逻辑很相近的代码段")
        println("但是我不想把他独立成一个方法")
        println("又担心别人会随便改")
        println("所以用run方法把他们放在同一个作用域中")
        println("小组中其他人看到这段,就知道不要把无关代码插进来")
    
    
  • 更神奇的是,这个run函数是有返回值的,参数返回值可以利用起来:

    fun testRun2(param1: String, param2: String, param3: String) 
        //我想让这几个参数都不为空,如果检查是空,就不执行方法主体
        val checkRes: Boolean = run<Boolean> 
            when 
                param1.isNullOrEmpty() -> 
                    false
                
                param2.isNullOrEmpty() -> 
                    false
                
                param3.isNullOrEmpty() -> 
                    false
                
                else -> true
            
        
    
        if (checkRes)
            println("参数检查完毕,现在可以继续接下来的操作了")
        else
            println("参数检查不通过,不执行主体代码")
        
    
    fun main() 
      testRun2("1", "2", "")
    
    

main运行结果:

参数检查完毕,现在可以继续接下来的操作了

小结论

run方法在整合小段代码的功效上,还是很实用的

其他高阶函数

上面列举出来的这些系统高阶函数原理上都差不多,只是使用场景有区别,因此除了run之外,其他的不再详解,而只说明使用场景。

apply

和run只有一个区别,run是返回block()执行之后的返回值,而,apply 则是返回this,因此 apply必须依赖对象。而由于返回了this,因此可以连续apply调用。

fun testApply() 
    val a = A()
    a.apply 
        println("如果一个对象要对它进行多个阶段的处理")
    .apply 
        println("那么多个阶段都挤在一起则容易混淆,")
    .apply 
        println("此时可以用apply将每一个阶段分开摆放")
    .apply 
        println("让程序代码更加优雅")
    


fun main() 
    testApply()

with

下面的代码应该都很眼熟,Glide图片加载框架的用法,with(context)然后链式调用

Glide.with(this).load(image).asGif().into(mImageView);

Kotlin中的with貌似把这一写法发扬光大了(只是类比,不用深究),场景如下:

class A 
    val a = 1
    val b = "b"

    fun showA() 
        println("$a")
    

    fun showB() 
        println("$a $b")
    


fun testWith() 
    val a = A()
    with(a) 
        println("作用域中可以直接引用创建出的a对象")
        this.a
        this.b
        this.showA()
        this
    .showB()


fun main() 
    testWith()

细节

  1. with(a) 大括号内的作用域里面,可以直接使用 当前a对象的引用,可以this.xxx 也可以 a.xxx
  2. with(a) 大括号作用域内的最后一行是 返回值,如果我返回this,那么with结束之后,我可以继续 调用a的方法
also

also和with一样,必须依赖对象,返回值为this。因此它也支持链式调用,它和apply的区别是:

apply的block,没有参数,但是 also 则将this作为了参数。这么做造成的差异是:

作用域 内调用当前对象的方式不同。

class A 
    val a = 1
    val b = "b"

    fun showA() 
        println("$a")
    

    fun showB() 
        println("$a $b")
    

fun testApply() 
    A().apply 
        this.showA()
        println("=======")
    .showB()


fun testAlso() 
    A().also 
        it.showA()
        println("=======")
    .showB()

apply 必须用this.xxx, also则用 it.xxx.

let

类比到run:

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

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

只有一个区别:run的block执行不需要参数,而let 的block执行时需要传入this。

造成差异为:

A().run 
    println("最后一行为返回值")
    this
.showA()

A().let 
    println("最后一行为返回值")
    it
.showA()

run 作用域中 只能通过this.xxx操作当前对象,let 则用 it.xxx

takeif 和 takeunless

这两个作用相反,并且他们必须依赖对象。看源码:

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

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? 
    return if (!predicate(this)) this else null

predicate 是 (T)->Boolean 类型的lambda表达式,表示断言判断,如果判断为true,则返回自身,否则返回空

class A 
    val a = 0
    val b = "b"

    fun showA() 
        println("$a")
    

    fun showB() 
        println("$a $b")
    


fun testTakeIfAndTakeUnless() 
    println("test takeIf")
    A().takeIf  it.a > 0 ?.showB()
    println("==========")
    println("test takeUnless")
    A().takeUnless  it.a > 0 ?.showB()


fun main() 
    testTakeIfAndTakeUnless()

执行结果:

test takeIf
==========
test takeUnless
0 b

takeIf / takeUnless适用于将条件判断的代码包在一个作用域中,然后 用 ?.xxxx的安全调用符 来 执行对象操作。

repeat

repeat是 多次循环的傻瓜版写法。

fun testRepeat() 
    repeat(10) 
        print("$it ")
    


fun main() 
    testRepeat()

执行结果:

0 1 2 3 4 5 6 7 8 9 
lazy

lazy的作用是: 延迟初始化val定义的常量。

class B 
    val i: Int by lazy 
        println("执行i初始化")
        20
    

    init 
        println("构造函数执行")
    

如果只是初始化B对象,却没有使用到变量i, 那么延迟初始化不会执行。

fun main() 
    B()

执行结果:

构造函数执行

而如果使用到了变量i,才会去在调用之前初始化:

fun main() 
    println("i 变量的值是:" + B().i)

执行结果:

构造函数执行
执行i初始化
i 变量的值是:20

总结

Kotlin官方提供的高阶函数,run,apply,also,let,with等,旨在使用**作用域**,将联系紧密的代码封在一个作用域内,让一个方法内部显得 有条不紊,阅读观感更好,可维护性更高,代码更优雅。上述,除了lazy之外,所有的 函数都在Standart.kt文件内部。


更多:Java进阶核心知识集

包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等

点击免费领取我的学习笔记:学习视频+大厂面试真题+微服务+MySQL+Java+Redis+算法+网络+Linux+Spring全家桶+JVM+学习笔记图

高效学习视频

).i)


执行结果:

```kotlin
构造函数执行
执行i初始化
i 变量的值是:20

总结

Kotlin官方提供的高阶函数,run,apply,also,let,with等,旨在使用**作用域**,将联系紧密的代码封在一个作用域内,让一个方法内部显得 有条不紊,阅读观感更好,可维护性更高,代码更优雅。上述,除了lazy之外,所有的 函数都在Standart.kt文件内部。


更多:Java进阶核心知识集

包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等

[外链图片转存中…(img-qt7NIBqo-1623570218479)]

点击免费领取我的学习笔记:学习视频+大厂面试真题+微服务+MySQL+Java+Redis+算法+网络+Linux+Spring全家桶+JVM+学习笔记图

高效学习视频

以上是关于Kotlin-lambda表达式和高阶函数操作符的主要内容,如果未能解决你的问题,请参考以下文章

Python 基础入门笔记———— 公共操作推导式函数lambda表达式高阶函数

Python 基础入门笔记———— 公共操作推导式函数lambda表达式高阶函数

Scala高阶

Java高阶进阶之Java函数式编程-Stream流-Lambda表达式

Java高阶进阶之Java函数式编程-Stream流-Lambda表达式

Python之列表表达式及高阶函数lamdazipenumeratemap和filter方法