• 主页
  • 随笔
所有文章 友链 关于我

  • 主页
  • 随笔

Kotlin的Lambda与函数内联

2022-11-20

在kotlin种,函数是一等公民,我们就需要对kotlin的函数进行一些较深入的了解,主要是下面四块,其他一些常见的,比如顶层函数就不说了
主要是四块

  1. 函数引用与匿名函数
  2. Lambda表达式
  3. 函数内联
  4. 函数接受者

函数引用与匿名函数

函数引用

在Kotlin种一切皆是对象,函数也不例外,我们可以把一个函数(实质上是一个对象)作为参数去传递,通过::的方式可以把一个函数转换一个指向该函数的引用,然后该引用就可以作为参数传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun test(a:Int){
println(a)
}

fun test2(f:(Int)->Unit){
f(2)
}

fun main() {
// 得到指向test方法的引用,他指向了一个kt帮我们生成的对象
val obj = ::test
obj(1) // 这样也可以直接的调用
test2(obj)
}

匿名函数

::的方式是Kotlin会为我们生成一个匿名对象,调用该对象的invoke方法或者是按照方法调用该对象,即等同于调用该方法。除了通过::的方式,我们还可以通过匿名函数来得到一个指向该方法的引用(也是生成了对象):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// fun后不需要函数名称
val f = fun(a: Int) {
println(a)
}
fun test(f: (Int) -> Unit) {
f(3)
}
fun main() {
f(1)
f.invoke(2)
test(f)
//这种我们一般也不这么去写,会写成Lambda的形式
test(fun(a: Int) {
println(a)
})
}

在kotlin种,我们可以把匿名函数作为变量传递,所以本质上匿名函数不是一个函数,而是一个对象。通过上面匿名函数的例子,我们可以看到匿名函数进行一定的转换,他是可以变为lambda的,比如上面的

1
2
3
4
5
6
7
8
9
10
11
12
val f ={a:Int->
println(a)
}
fun test(f: (Int) -> Unit) {
f(3)
}
fun main() {
test(f)
test{
println(it)
}
}

从本质上说,Lambda也是一个匿名函数类型的对象,下面我们开始分析下Lambda的一些特点,在调用Lambda与匿名函数的时候,有一个区别是能否使用return,后者可以,前者不可以,后者可以在匿名方法的声明中使用return关键字,有返回值的时候也只能使用return关键字,前者在非inline函数中无法使用return,但是可以直接return@

Lambda表达式

一般Lambda会有三种用法:

  1. 声明为对象被调用
  2. 声明为函数的参数(这样的函数也叫高阶函数)
  3. 作为函数参数返回(这种也是高阶函数)

声明为对象的时候

1
2
3
4
5
6
7
8
// 无参的
val l = {}
// 有参数的需要同时说明参数类型,使用->分割参数与lambda体,多个参数不能使用()包裹,lambda体不能在使用{}包裹。
val l1 = {a:Int,b:Int->
a+b
}
val l2 = {a:Int,b:String->
}

作为函数的参数的时候(注意Lambda的参数个数相同是看作同样的方法,他们的方法名称不能一样,其中Lambda表达式的参数部分的()不能省略)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 无参和无返回值的
fun test(f:()->Unit){
}
// 有参数的,其中Lambda种的参数名称a是可以省略的,下面的没省略
fun test(f:(a:Int)->Unit){

}
// 有返回值的,他的方法名称不能与上面的类似
fun test1(f:(Int)->Boolean){

}
// 有默认值的,与普通参数的默认值一样
// 传入的Lambda的默认值是空的时候,是类型体包裹一个()?,不能包裹函数的变量
fun test2(f:(()->Unit)?=null) {
}
// 非空默认值
fun test1(f: (Int, Int) -> Boolean = { a: Int, b: Int -> a + b > 0 }) {
}

作为函数返回值(闭包),因为Lambda表达式它最终来说还是一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

fun test(): () -> Unit {
return {
println("xx")
}
}
fun test(a: Int): (String) -> String {
val lambda = { text: String ->
if (a > 0) {
text + a
} else {
text + a
}
}
return lambda
}
fun main() {
test()
val result = test(1)
println(result("sss"))
// 也可以直接 test(1)("sss")
}

Lambda表达式的返回值:在Kotlin的Lambda中,是他的最后一行作为返回值返回的,我们不需要声明返回值的类型,也不需要使用return,他会自动推断

1
2
3
4
5
6
7
8
9
10
11
12
13
val sum = {a:Int,b:Int->a+b} //返回值是Int
val result=sum(1,1) // result=2
val compare = { a: Int, b: Int ->
if (a > b) 1
else if (a < b) -1
else 0
} //返回值是Int
val result = compare(1,2) //result=-1

val test = {
println("test")
} // 返回值是Unit
test()

假如我们需要终止Lambda的后续调用,可以使用return@的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun test(f: (Int) -> Boolean) {
}

fun main() {
val result = test {
if(it>0){
return@test true
}
return@test false
}
// 也可以直接
val result2 = test {
it>0
}
}

Lambda的别名,有时候,我们写函数参数的时候需要有挺多个或者是重复的Lambda,我们可以通过定义别名的方式来简便写法,使用typealias关键字,这样代码看起来就简洁很多。

1
2
3
4
5
6
7
8
9
10
typealias FunOne = ()->Unit

fun test3(f:FunOne){
}
fun test4(f:FunOne?=null){
}
//
test3{}
//f可空就可以不创
test4()

Lambda的传参调用例子:

  1. 当Lambda表达式只有一个参数的时候可以省略,使用it指代
  2. 当参数不需要被调用的时候,可以使用_。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 当lambda变量是参数的最后一个的时候,可以写在调用的外面,当只有一个参数而且该参数是lambda的时候,可以省略()
fun main() {
test{}
test(0){}
test(0,{}){}
}

fun test(f:()->Unit){
}
fun test(a:Int,f:()->Unit){
}
fun test(a:Int,f:()->Unit,f2:()->Unit){
}

//当Lambda的参数只有一个的时候,可以省略,it代表了当前的传入的参数
fun main() {
test {
println(it) //打印 hello work
}
// 不使用参数,可以使用_
test{_,_->

}
}
fun test(f: (String) -> Unit) {
f("hello work")
}
fun test(f:(String,String)->Unit){

}

Lambda与SAM转换:参考文章Kotlin的SAM转换

  1. SAM接口,即单一方法的Java定义的接口其实就是Kotlin种的Lambda兼容了Java中的SAM,让他的调用可以像Kotlin调用普通的高阶函数一样
  2. kotlin定义的接口类型作为参数,不能使用Lambda,需要使用匿名内部类的方式(kotlin在1.4之后做了一个优化,使用fun修饰的只有单一方法的interface可以)
  3. KClass,就是Kotlin种定义的Class对象,可以使用return,但是需要显示的转换为KClass,不能像SAM那样省略,这种做法也一般不推荐。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
1. 
// java定义的
public interface ITest {
boolean doSomething(int a);
}

// kotlin种调用
fun test(test: ITest) {
}
fun main() {
test {
if (it > 0) {
// 不在执行下面的语句
return@test false // return@test就表示结束test Lambda的调用
}
// 直接返回true,当然也可以加上return@test
true
}
}

1.
// 假如是普通的kotlin单一方法接口 interface则无法使用,因为官方更希望使用函数式的编程,因为一般单一接口类型的变量都可以使用Lambda代替,当然我们也可以使用上面说的typealias来代替一般的接口也可以。

interface ITest2{
fun doSomething(a:Int):Boolean
}
// 使用了fun 修饰的
fun interface ITest3{
fun doSomething(a:Int):Boolean
}
//ITest2作为参数
fun test(test: ITest2) {
}
fun test2(test: ITest3) {
}
fun main() {
// 不能写成Lambda的方式,只能写出匿名内部类
test(object :ITest2{
override fun doSomething(a: Int): Boolean {
return false
}
})
test2 {
it <= 0
}
}
3.
// 使用KClass的时候,我们可以使用return关键字
class Test(val doSomething: (Int) -> Boolean)
fun test(test: Test) {
}
fun main() {
test(Test {
if (it > 0) {
return@Test false
}
true
})
}

函数内联

函数内联使用的关键字是inline,用于修饰方法。inline函数会在被调用的地方复制一份方法代码(代码内嵌),同时会把函数参数也给铺平(即方法体+传参的lambda都会被铺平),而不是通过方法栈的形式被调用。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fun main(args: Array<String>) {
test()
}

inline fun test(){
println("h")
val a = 1+1
}
// 方法栈的调用深度等同于,编译之后也是类似这样的
fun main(args: Array<String>) {
println("h")
val a = 1+1
}

// 铺平例子
inline fun test(f:()->Unit){
println("hi")
f()
}
fun main() {
test{
println("hello")
}
// 相当于下面代码,他会把test铺平,也会把传入的lambda参数也铺平
println("hi")
println("hello")
}

那么inline内联函数有什么作用呢?经过上面对于匿名函数与Lambda的梳理,我们知道他们从本质上来说是一类对象,那么这里就会引出一个问题,假如我们频繁的调用高阶函数,那么他每一次就会产生一个对象,假如循环数较大就会耗费较多的内存。比如

1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
for (i in 0..100){
test{}
}
}
fun test(f:()->Unit){
println("h")
val a = 1+1
}

在循环中调用了test()方法,就会创建大量的看不到的函数对象,这时候我们引入了inline方法就可以解决这个问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main(args: Array<String>) {
for (i in 0..100){
test{}
}
}
inline fun test(f:()->Unit){
println("h")
val a = 1+1
}
// 等同于
fun main(args: Array<String>) {
for (i in 0..100){
println("h")
val a = 1+1
}
}

这样也就不会每次循环都生成一个函数对象了,inline修饰的方法会在编译的时候把该方法复制一份到被调用处。
对于inline函数的函数体不适宜过大,因为随意调用可能会导致inline函数被到处复制代码从而导致编译之后的包变大,一般而言是对于循环中调用的高阶函数使用该修饰符,普通方法中是不推荐使用inline的。当然了,对于我们一些lib库中定义的高阶方法,我们是推荐使用inline修饰的,因为你不知道外部调用者是否会在循环体中调用该方法,所以最好是加上inline修饰。

inline的函数调用支持return去终止方法的后续调用,即假如在inline函数中使用了return而不是return@的形式,他会直接终止后续的调用,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun test(f: () -> Unit) {
}
inline fun test2(f: () -> Unit) {
}
fun main() {
println("start")
test {
println("test")
// 只能使用return@,不会结束外部函数的调用
return@test
}
test2 {
println("test2 1")
// 只能使用return@,不会结束外部函数的调用
return@test2
}

test2 {
println("test2 2")
// 会结束了main的调用
return
}
// 不会被执行
println("will no print")
}

inline与refied关键字:首先refied是修饰泛型参数的,用在inline方法中。我们在Java中是无法通过泛型来获取Class对象的,但是kotlin可以做到,即通过inline修饰的方法加上使用refied修饰的泛型即可拿到。

1
2
3
4
5
6
7
8
public <T> void test(List<T> list){
// 无法获取T的Class类型,下面代码会报错
T.class
}
// kitlin 代码,可以获取到T的Class
inline fun <reified T> test(t:T) {
T::class.javaClass
}

需要注意的是使用了refield修饰的泛型不会像java一样会被搽除,因为编译的时候他会把方法体复制并具体化。

noinline,用于修饰lambda参数(不能修饰普通参数),他是与inline相反的,使用它修饰的lambda参数不会被铺平,会生成函数对象,同时它只能是在inline函数中使用,这时候是只有调用的方法体被铺平了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline fun test(f: () -> Unit, noinline f2: () -> Unit) {
println("hi")
f()
f2()
}
fun main() {
test({
println("hello")
}, {
println("hello1")
})
// 相当于
println("hi")
println("hello")
({
println("hello1")
}).invoke()
}

noinline作用在于可以做到让方法返回参数中的函数引用,在inline修饰的方法中,我们是直接让方法体全都铺平了,包括作为参数传入的lambda,他是无法做到返回一个作为参数传入的函数对象的。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inline fun test(f: () -> Unit): () -> Unit {
println("hi")
f()
// 编译报错,因为f在test被调用的时候也会铺平
return f
}

inline fun test2(noinline f: () -> Unit): () -> Unit {
println("hi")
f()
//可以返回传入的函数参数f
return f
}

inline fun test3(noinline f: () -> Unit): () -> Unit {
println("hi")
f()
//这里如果不需要返回f,可以去掉noinline
return {}
}

noinlie总来的来说用来局部的指向性的关闭inline优化,同时注意在noinlie的参数中也是不允许使用return的,因为他是相当于普通的函数参数。

noinlie的使用还有一个点,在一个inline函数调用一个非inline函数的时候,假如是需要把参数中的lambda参数传递给非inline函数,则该参数需要使用
noinlie修饰,否则的话会报错,因为inline函数的函数参数他会被铺平,但是被调用者是不会的,这时候他胡产生冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
fun test(f:()->Unit){
}

inline fun test2( f:()->Unit){
println("hi")
// 编译报错,Illegal usage of inline-parameter
test(f)
}
inline fun test2(noinline f:()->Unit){
println("hi")
// 正常执行
test(f)
}

crossinline,它也是用于修饰inline方法的函数参数的,与noinline的一些作用相反,在inline函数调用非inline函数的时候,我依然希望传入lambda是铺平的,它也可以传递给别的非inline,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun test(f: () -> Unit) {
}

inline fun test2( f: () -> Unit) {
// 编译报错,假如我们不需要f内联,可以使用noinline
test{
f()
}
}
inline fun test3(crossinline f: () -> Unit) {
// 编译正常
test{
f()
}
}

注意,就算我们使用了crossinline修饰,参数也不能作为参数传递给另外一个非inline函数,假如需要传递,还是需要noinlie,需要函数参数作为返回值也是需要使用noinline。例如

1
2
3
4
inline fun test4(crossinline f: () -> Unit) {
// 编译报错,要传递f只能使用noinlie
test(f)
}

还有一点就是我们的函数参数被非inline方法间接调用,使用了crossinline来加强内联,则我们调用处是不能使用return关键字的,因为不知道是结束哪一处的调用,比如

1
2
3
4
5
6
fun main() {
test3 {
// 编译报错
return
}
}

总结就是:

  1. inline修饰函数,一般只修饰高级函数,修饰之后会把方法体和函数对象铺平调用,使用了return关键字之后会结束外部方法的调用。
  2. noinline用于修饰inline方法中的函数类型参数,被修饰的参数可以传递给非inline函数,也可以作为返回值,不能使用return。
  3. crossline,假如我们函数参数需要被间接调用,而且我们希望该参数也内联,则使用crossline,调用时不能使用return。

函数接受者

我们在kotlin中经常的使用apply,also,let,run等,我们可以看一下他们的声明

1
2
3
4
5
6
7
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

我们可以看到参数block中的函数类型使用了T.(),这就是一个函数接受者的例子,表示可以在调用的时候,直接使用T中的变量和方法,block也需要被T的对象调用,所以apply中的this代表的是调用对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person(val name:String,val age:Int){
fun sayHi(){

}
}
fun <T> T.test(f:T.()->Unit){

}
fun main() {
val p = Person("a",10)
p.test {
// this代表的是p对象
println(this.age)
// 也可以简写为
println(age)
sayHi()
}
}

T().与(T)的区别,比如apply与also,

1
2
3
4
5
6
7
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

调用的时候also中的this是当前类,调用者是通过it来获取,这就是T().与(T)的区别。

参考文档

  1. 一文彻底搞懂Kotlin inline
  2. Kotlin 中的 lambda,这一篇就够了
  3. Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?
  4. Kotlin 的 Lambda,大部分人学得连皮毛都不算
  • Kotlin
基于MVVM的换肤方案
Kotlin的异常处理
  1. 1. 函数引用与匿名函数
    1. 1.1. 函数引用
    2. 1.2. 匿名函数
  2. 2. Lambda表达式
  3. 3. 函数内联
  4. 4. 函数接受者
  5. 5. 参考文档
© 2023 liweijie
Hexo Theme Yilia
  • 所有文章
  • 友链
  • 关于我

tag:

  • Android
  • Jetpack
  • Java
  • Dart
  • OpenSource
  • MiniProgress
  • JavaScript
  • RN
  • 开发总结
  • Kotlin
  • 开发规范
  • IM
  • Java 设计模式

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

职业:移动开发者

Skill:Java,Kotlin,RN,Flutter