阅读 162

六、kotlin的函数式编程

lambda表达式

是什么?

答: 在kotlin中是一种{} 限定作用域, 以 -> 区分参数和函数体的表达式, 叫 lambda表达式, 其本质是代码块, 你也可以理解成可调用的函数类型对象(但根据反编译发现其实不是, 它的实现方式有很多. 比如: 生成一个函数, 然后传递函数引用等等, 方式还挺多)

这里找时间分析分析, 都有哪些实现 lambda 的方式

image.png

// val funcType: (Int, Int) -> Int = {x, y -> x + y}
val funcType: (Int, Int) -> Int = {x: Int, y: Int -> x + y}


val sum01 = funcType.invoke(1, 2)
// 简略成这样: 
val sum02 = funcType(1, 2)

// {x: Int, y: Int -> x + y}(1, 2)
{x: Int, y: Int -> x + y}.invoke(1, 2)
复制代码

其中 {x: Int, y: Int -> x + y} 就是lambda表达式

funcType.invoke(1, 2){x: Int, y: Int -> x + y}.invoke(1, 2) 表示 lambda函数对象调用 函数

有什么优缺点?

优点:

  1. 代码比较简洁
  2. lambda 带来的参数捕获, 很便利, 如果用的好 lambda 用习惯了, 匿名对象的方式反而不想用了(object : InterfaceName { override xxxxx })

缺点:

  1. 代码可读性较差(用习惯了, 反而比较简单)

  2. 使用 lambda 有些情况下需要注意 this , 有时候没有, 但有些时候又有(这在后面有解释), 究其原因是大家把 lambda 和 匿名对象 做了比较, 其实他们还是有区别的, 匿名 new 出来的对象, 有一个 this 指向的是该对象自己, 而 lambda 则没有, lambda的this通常都是捕获的外部作用域的 this , 如果 外部作用域没有 this (比如lambda写在顶层函数内部, this是没有的), 则 lambda 就没有 this

怎么用? 有什么应用场景?

最主要的用法用于参数传递, 一般是函数参数为 函数类型, 我们传递个lambda表达式过去

下面是我效仿 maxBy 函数写的

fun <T, R : Comparable<R>> List<T>.maxBy(selector: (T) -> R): R? = listIterator().run {
    if (!hasNext()) return null
    val maxElement = next()
    var maxVal = selector(maxElement)
    while (hasNext()) {
        val nextElement = next()
        val nextVal = selector(nextElement)
        if (nextVal > maxVal) {
            maxVal = nextVal
        }
    }
    return maxVal
}
复制代码

而调用方式

    val list: List<Int> = listOf(100, 2, 3, 400, 59999, 66, 700)
//    println(list.maxBy({ number -> number }))
//    println(list.maxBy { number -> number })
    println(list.maxBy { it })
复制代码

我们在使用lambda中需要注意这些问题:

  1. list.maxBy({ number -> number })) 当作普通参数传递
  2. { number -> number } 如果只有一个函数参数的话, 可以直接省略掉参数, 如果 函数类型参数在参数最后的位置比如: sun(a: Int, b: Int, func: (Int, Int) -> Int) 调用的时候可以这样:
sum(1, 2) { a, b -> a + b }
复制代码
  1. 如果只有一个参数的函数参数类型, 可以直接用 it 代替. 比如:selector: (T) -> R 参数只有一个 T, 所以T在使用的时候可以用 it 代替, 所以在使用的时候可以直接:
list.maxBy { it }
复制代码
  1. lambda的函数体可以有多行, 默认最后一行是返回值
val sum = {x: Int, y: Int -> 
    println("x = ${x}, y = ${y}")
    x + y
}
复制代码

lambda在kotlin和java中的区别

在java中 lambda 使用外部局部变量需要添加 final 修饰, 但在 kotlin中则不需要, 在 kotlin 中这种情况的变量的值可以改变, 在kotlin中不会仅限于访问 final 变量, 在kotlin内部可以修改该变量

val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
var odd = 0
var even = 0
list.forEach {
    if (0 == it % 2) {
        even++
    } else {
        odd++
    }
}
println("单数: $odd, 双数: $even")
复制代码

image.png

注意在 lambda 的代码未必马上执行, 比如: button.onClink { i++ } 该函数只有在触发事件时才会 i++

kotlin 支持lambda 内部修改变量的实现方式是:

将 lambda 捕获的变量全部进行包装, 比如 在 java 中可以把对象改成成 AtomicInteger 等类似这种的对象, 然后将值存放在 AtomicInteger 内部, 这样即使 lambda 捕获了 AtomicInteger 对象, 也仅仅捕获的是 引用 该引用也被添加了 final , 而我们修改的是引用背后的值, 所以可以实现在 lambda 内部修改值外部也能被修改的功能

kotlin 也用的这种方式, 不过它存放的不是 AtomicXXXXX 系列的类, 而是一个叫 Ref 的类

闭包

前面的章节说过, 闭包就是内部函数可以访问外部函数的局部变量和各种内部类之类的, 但是外部函数无法使用函数内部的变量, lambda 也是

在我看来, lambda这种方式巧妙的地方在于, 定义lambda和调用lambda两方

在 定义方, 只要是在定义 lambda 之前的变量都可以在 lambda 里面无条件使用(说白了, 就是lambda捕获了外部类的引用)

由于 lambda 会捕获外部类的引用, 所以需要注意 串行化 的问题

在 调用方, lambda 参数贯穿了调用方的作用域, 只要调用方往里头传递参数, 那么lambda就可以使用调用放的部分变量, 如果调用方传递的是 this , 那么lambda同时掌握了两方的 this 所能访问的所有变量

image.png

成员引用

这一章节可以当作 c++ 的 & 引用 来学习

成员引用是一个取值的过程, 类似于定义一个引用, 引用指向了 目标 的地址(静态成员拿到的是偏移地址)

:: 引用操作符可以对 成员属性/成员函数/扩展函数/扩展属性/顶层属性/顶层函数/类 等 使用

image.png

Person 限定了它在哪个类, :: 表示取引用, age 表示目标

// 这就是拿到 Person 类 name 属性的 偏移地址
val refName: KMutableProperty1<Person, String> = Person::name
复制代码

然后你会发现 import kotlin.reflect.KMutableProperty1 是 reflect , 是反射包里面的类型

所以我们操作 refName 更会和反射产生联系

成员引用可以和 lambda 互换使用

这里就大概说了说他的用法, 抽空专门搞个章节学学

集合和lambda

val list = listOf(Person("haha", 22), Person("xixi", 21), Person("dd", 23))
list.maxBy(Person::age)

println(list.filter { it.age > 22 }.map(Person::name))
复制代码

lambda 看起来简单, 使用起来不注意可能让程序效率更慢, 就像上面的两个函数, maxBy 底层遍历了一轮, 而 下面的那行代码, 程序执行了 两轮 遍历, 程序员看到的仅仅是一行代码

下面那段代码让程序员手动写效率会更高

println(list.filter { it.age > 22 }.map(Person::name))

var nameList: MutableList<String> = mutableListOf()
list.forEach {
    if (it.age > 22) {
        nameList.add(it.name)
    }
}
println(nameList)
复制代码

all any count find 对集合应用的判断

  • all, 都是判断集合中所有的元素是否满足条件, 只要有一个不满足直接返回 false, 否则返回 true
  • any 判断集合中是否存在至少一个满足条件的, 如果满足返回 true, 否则返回 false
  • count 判断集合内有几个满足条件判断的
  • find 查找集合内第一个满足条件的元素
val list = listOf(1, 2, 3)
val all = list.all { it > 2 }
println(all)
val any = list.any { it > 2 }
println(any)
val count = list.count { it >= 2 }
println(count)
val find = list.find { it == 2 }
println(find)
复制代码

groupBy 分组

val map = list.groupBy { it.name.first() }
for (entry in map.entries) {
    println("key = ${entry.key}, value = ${entry.value}")
}
复制代码
key = h, value = [Person{name = heihei, age = 34}, Person{name = haha, age = 22}, Person{name = hoho, age = 23}]
key = z, value = [Person{name = zhazha, age = 23}]
key = d, value = [Person{name = dd, age = 12}]
复制代码

学过 sql 的应该都知道, 这就是那个的分组

flatMap 和 flatten 处理嵌套集合中的元素

flat: 铺平

private val list = listOf(
   Book("k language", listOf("zhazha", "haha", "xixi", "heihei", "hoho")),
   Book("v language", listOf("zhazha", "haha", "heihei", "hoho")),
   Book("l language", listOf("zhazha", "haha", "xixi", "heihei")),
   Book("j language", listOf("zhazha", "haha", "xixi", "hoho"))
)

val map = list.flatMap { it.title.toList() }
map.forEach {
   print("$it")
}
复制代码

flat map 的功能是从一堆杂物(对象)里挑选一块或者很多块砖头(属性或者集合属性), 把他们分堆, 然后铺平, 最后连接在一起, 这就是 flat map 的功能

flatten 函数的功能和上面的差不多, 不过它面对的是 List<List<String>> 这种方式的

惰性集合操作: 序列

list.asSequence().filter { it.age > 60 }.map { it.name }.toList().forEach {println(it)}
复制代码

它是惰性的, 他避免了 filter 时创建的临时对象, 还有 map 计算时的临时对象, 它借助 Interator 来实现惰性操作

但在实际操作中, 我没看出来它有多快速

// 加载对象
val list = mutableListOf<Person>()
for (i in 1..10000) {
   list.add(Person(UUID.randomUUID().toString(), Random.nextInt() % 150))
}
// lambda正常方式
var start = Instant.now()
list.filter { it.age > 60 }.map { it.name }
var duration = Duration.between(start, Instant.now())
println()
println(duration.toMillis())

// 惰性方式
start = Instant.now()
list.asSequence().filter { it.age > 60 }.map { it.name }
duration = Duration.between(start, Instant.now())
println()
println(duration.toMillis())

// 手动写代码方式
start = Instant.now()
val mutableList = mutableListOf<String>()
list.forEach {
   if (it.age > 60) {
      mutableList.add(it.name)
   }
}
duration = Duration.between(start, Instant.now())
println()
println(duration.toMillis())
复制代码
20   17

34   22

3    4
复制代码

不管我试了几次, 都是这样的情况, 也许是对象不够??? 才会导致惰性不行???

我感觉并不是, 使用惰性的方式可能效率没提高多少, 但在节省内存方面应该是显著

但不管怎样, 手动编码方式效率还是最高的

===================2021.10.05==============================

发现惰性慢可能的原因了, 从 inline 和 sequence 序列的角度看的

序列的函数在使用 lambda 时, 没有 inline 内联处理, 所以每次调用函数就会产生对象, 而普通集合函数都是内联的, 但是它每次调用一个新的方法都会操作背后的中间集合, 也慢

所以还是看情况斟酌得用

数列的中间和末端操作

中间操作始终是惰性的, 末端操作能够触发所有惰性操作的延迟时间, 惰性操作直接开始执行

数列和集合的区别

  1. 数列操作是一个元素一个元素的执行的, 一个元素执行一系列函数完毕后留下, 切换另一个元素, 而数列的操作是一个集合一个集合的操作, 一个函数执行完毕留下一个中间集合, 然后传递到下一个函数, 在进行操作

image.png

比如上图, 明显两个的源码大意是:

listOf(1, 2, 3, 4).map { it * it }.find { it > 3 }
listOf(1, 2, 3, 4).asSequence().map { it * it }.find { it > 3 }
复制代码

左边就跟学校一个班级一个班级的学生去打疫苗一样, 这些学生去打第一针(map), 等全班学生都打完第一针后, 再去验验都有谁产生抗体了(find)

右边就跟社会人去打针一样, 预约拿号, 排队打疫苗(map), 打完疫苗后, 不用等别人, 直接去验下是否产生了抗体(find)

一个打完要等别人, 一个打完直接去做下一项

  1. 集合需要注意调用函数的顺序, 数列不用
listOf(1, 2, 3.0, 4).map{ it * it }.filter{ it is Double }
listOf(1, 2, 3.0, 4).filter { it is Double }.map{ it * it }
复制代码

image.png

这种区别, 不用我多说看图就懂(看不懂的, 回小学学习去) 1.gif

你过滤的越多, 后续集合越少, 效率越高

lambda的实现方式

fun postponeComputation(id: Int, runnable: () -> Unit) {
   println("id = $id")
   runnable()
}

fun handleComputation(id: String) {
   postponeComputation(1000) { println(id) }
}
复制代码

lambda本质上是可以传递给其他函数的一小段代码, 我们可以把它当作一个匿名函数的引用 + 函数(函数参数列表和函数体), 该函数引用可以当作参数传递

按照上面的编码情况, 我们显示查看底层的过程

fun postponeComputation(id: Int, runnable: Function0<Unit>) {
   println("id = $id")
   runnable()
}
复制代码

image.png

然后下面那个函数的代码会变成这样:

fun handleComputation(id: String) {
   postponeComputation(1000, object : Function0<Unit> {
       val id = id
       fun void invoke(): Unit {
           println(this.id)
       }
   })
}
复制代码

当然实际上代码可能不是这样写的, 但主要思想差不多

lambda this 和 匿名对象this的探讨

fun postponeComputation(id: Int, runnable: () -> Unit) {
   println("id = $id")
   runnable()
}

fun handleComputation() {
   postponeComputation(1000) {
      // 这里直接报错
      println(this) // error
   }
   postponeComputation(1999, object : Function0<Unit> {
      override fun invoke() {
         println(this)
      }
   })
}
复制代码

上面这段代码会报错
image.png

问题也很明朗, handleComputation 函数是静态的, 所以根本没有 this, 但是下面的 postponeComputation(1999, object : Function0<Unit>this 能够使用且指向的对象是 匿名对象本身

但是如果代码这样写,

class LambdaRealizationDemo01  {
   fun handleComputation() {
      postponeComputation(1000) {
         // 没有报错
         println(this)
      }
      postponeComputation(1999, object : Function0<Unit> {
         override fun invoke() {
            println(this)
         }
      })
   }
}
复制代码
id = 1000
lambda09.LambdaRealizationDemo01@6108b2d7
id = 1999
lambda09.LambdaRealizationDemo01$handleComputation$2@13969fbe
复制代码

可以很直接的看出来, 俩 this 指向的对象根本不一样, lambda在一些使用场景特别要注意 this 到底指向的是谁?

下面代码是 java 源码:

public final class LambdaRealizationDemo01 {
    public final void handleComputation() {
        LambdaRealizationDemo01Kt.postponeComputation(1000, (Function0<Unit>)((Function0)new Function0<Unit>(this){
            final /* synthetic */ LambdaRealizationDemo01 this$0;
            {
                this.this$0 = $receiver;
                super(0);
            }

            public final void invoke() {
                LambdaRealizationDemo01 lambdaRealizationDemo01 = this.this$0;
                boolean bl = false;
                System.out.println(lambdaRealizationDemo01);
            }
        }));
        LambdaRealizationDemo01Kt.postponeComputation(1999, (Function0<Unit>)((Function0)new Function0<Unit>(){

            public void invoke() {
                boolean bl = false;
                System.out.println(this);
            }
        }));
    }
}
复制代码

仔细看这俩的区别:

LambdaRealizationDemo01Kt.postponeComputation(1000, (Function0<Unit>)((Function0)new Function0<Unit>(this)

LambdaRealizationDemo01Kt.postponeComputation(1999, (Function0<Unit>)((Function0)new Function0<Unit>()

复制代码

结论:
lambda有个功能叫捕获, 它会捕获外部作用域的一些变量, 在上面的例子中, 该lambda捕获了外部函数作用域中的 this, 而该函数作用域的 this 就是 LambdaRealizationDemo01 对象

★★★带接收者的 lambda

带接收者的 lambda 函数

fun buildString(builderAction: StringBuilder.() -> Unit): String {
   val sb = StringBuilder()
   sb.builderAction()
   return sb.toString()
}

fun main() {
   val s = buildString {
      append("Hello ")
      append("World!!!")
   }
   println(s)
}
复制代码
fun buildString(builderAction: StringBuilder.() -> Unit): String {
   return StringBuilder().apply { builderAction() }.toString()
}
复制代码

带接收者的函数类型

val StringBuilder.appendExcel1: StringBuilder
   get() = this.append("01!")

val appendExcel2: StringBuilder.() -> StringBuilder =  { this.append("02!") }

fun main() {
   val stringBuilder = StringBuilder("Hi")
   stringBuilder.appendExcel1
   // 这里直接调用了
   stringBuilder.appendExcel2()
   println(stringBuilder)
}
复制代码

带接收者的函数类型很好理解, 把它当作类型返回就行 , StringBuilder.() -> StringBuilder, 当作普通 类型 就行, 然后在参数前面加上接收者StringBuilder..

  1. 在定义处不用管其中隐藏的 this, 按照普通的函数类型使用就好() -> StringBuilder
  2. 在调用处, 就需要stringBuilder.appendExcel2()或者appendExcel2(stringBuilder)这样传入 this 对象本体

带接收者的 lambda 可以用在 dsl 中

image.png

image.png

不过这些都是后话了

他的使用场景很多, 必须掌握

fun main() {
   val yesterday: LocalDateTime = 1.days.ago
   println(yesterday)
   val tomorrow: LocalDateTime = 1.days.formNow
   println(tomorrow)
}

private val Period.formNow: LocalDateTime
   get() = LocalDateTime.now() + this
private val Int.days: Period
   get() = Period.ofDays(this)
private val Period.ago: LocalDateTime
   get() = LocalDateTime.now() - this
复制代码

这里我再次总结下在 kotlin 里什么是扩展.

扩展是一种提供, 也是一种限定, 它为我们的属性, 函数和函数类型提供了使用 this 指针的可能, 但同时也限定了 this 指针的类型

image.png

image.png

SAM转换

有些情况下, 你不得不用 new 接口的方式 new 一个匿名对象, 比如下图

image.png

它需要一个 JavaInterface 接口对象, 而不是 () -> Unit 类型的对象

所以你不得不用上 object : 接口 这种方式

interface JavaInterface {
   fun doSomething(person: Person)
}

fun delegateWork(j: JavaInterface) {
   val person = Person("zha", "zha")
   j.doSomething(person)
}

fun main() {
   delegateWork(object : JavaInterface {
      override fun doSomething(person: Person) {
         println("name = ${person.firstName + person.lastName}")
      }
   })
}
复制代码

但 kotlin 还提供了效率更好的方式, 这种方式就叫做 SAM构造函数 , 一种将 lambda 转换成 构造函数 的方案

但使用这种方式有前提:

  1. kotlin 版本在 1.4 之后

  2. 接口需要特殊性, 特殊接口有两种

  • 接口需要声明为 SAM 接口, 在 kotlin 中是这样: fun interface JavaInterface { }, 也就是在接口前面添加 fun

  • 接口为 java 的接口, 则可以直接使用

上面这两种接口才能够实现SAM转换

首先我机器上 kotlin 的版本是:

image.png

1.5版本

然后, 我们先试试将 JavaInterface 修改为 java 的接口

image.png

delegateWork(JavaInterface { "zhazha" })
// 我们还可以隐藏 JavaInterface 接口的名称
delegateWork { "zhazha" }
复制代码

现在 将 JavaInterface 接口修改为 kotlin 的接口

fun interface KotlinInterface {
   fun doSomething(str: String?): String?
}

fun kotlinDelegateWork(k: KotlinInterface) {
   k.doSomething("hello kotlin")
}
复制代码

注意 interface 需要添加 fun 成为 kotlin 的 SAM函数式接口

kotlinDelegateWork(KotlinInterface { "zhazha" })
// 在 kotlin 1.4 之前不可以像下面这样用, 现在可以了
kotlinDelegateWork { "zhazha" }
复制代码

kotlin 的 lambda 使用效率比较低, 所以一般使用到 lambda 的地方都可以使用 inline 修饰符 修饰函数, 比如 inline fun doSomething(f: (Int) -> Int): Int

注意 inline 修饰符仅合适函数类型参数, 不能够使用在 SAM 函数式接口的参数下使用, 比如: inline fun doSomething(j: JavaInterface) 这样就不合适使用 inline

文章分类
Android
文章标签