Kotlin lambda之-Capturing vs Non-Capturing Lambdas

整理了自己在实际开发中关于kotlin的学习和思考:深入学习Kotlin,感兴趣的可以查看,后续会不断地更新。

概述

相信很多人看到Capturing/non-capturing lambdas的时候,很疑惑,这是什么东西,好像实际使用过程中,并没有看到这个。其实这个东西我们在实际开发中每天都碰到,只是这个概念我们不是很熟悉,今天我就来说说这个东西。了解完这个概念之后,我们

什么是 Capturing/Non-Capturing Lambdas

那到底什么是Capturing/non-capturing lambdas呢?从字面上可以看到Capturing lambdasnon-capturing lambdas是两个相对的概念,这里我把他直译为捕获类型的lambda非捕获类型的lambda。lambda 我们都知道,就是指的 lambda 表达式,但是这里的捕获和非捕获是啥意思呢?我看下英文解释:

Lambdas are said to be "capturing" if they access a non-static variable or object that was defined outside of the lambda body

从解释可以看出,如果一个 lambda 没有引用外部的非静态变量或者对象,则把这个 lambda 称为non-capturing lambdas,如果引用了则称为Capturing lambdas。所以这里根据意思,我们可以把capturing理解为引用就很好理解了。

例子

我先举一个non-capturing lambdas的例子

kotlin 复制代码
class LambdaTest2(private val viewModel: TestViewModel, private val lifecycleOwner: LifecycleOwner) {
    fun initView() {
        viewModel.liveData.observe(lifecycleOwner) {
            println("receive data")
        }
    }
}

上面的给一个 监听LiveData数据的例子,使用的 lambda 就是属于non-capturing lambdas,因为内部没有引用任务外部的变量。再看一个Capturing lambdas的例子

kotlin 复制代码
class LambdaTest2(private val viewModel: TestViewModel, private val lifecycleOwner: LifecycleOwner) {
    private val message: String? = null

    fun initView2() {
        viewModel.liveData.observe(lifecycleOwner) {
            println("receive data,toast=${message}")
        }
    }
}

上面这个例子中,引用了外部的 message 变量,所以这是一个Capturing lambdas

注意:Capturing/non-capturing lambdas,这个概念并不是 kotlin 独有,他是一个语言级别的概念,只要一门语言支持 lambda,一般都有这个,比如 Java,C++等。

实质上的区别

那么了解了概念,他们到底有什么区别呢?引用外部变量和不引用外部变量有什么区别?要了解这个,就需要我们看下最终编译的结果是什么,我们先看一个例子:

kotlin 复制代码
class LambdaTest2(private val viewModel: TestViewModel, private val lifecycleOwner: LifecycleOwner) {
    private val message: String? = null

    // `Capturing lambdas`,引用了外部变量
    fun initView2() {
        viewModel.liveData.observe(lifecycleOwner) {
            println("receive data,toast=${message}")
        }
    }

    // `non-capturing lambdas`,没有引用外部变量
    fun initView() {
        viewModel.liveData.observe(lifecycleOwner) {
            println("receive data")
        }
    }
}

代码很简单,就是观察 LiveData 数据变化,一个内部引用了外部变量message,一个没有引用外部变量,我看下它编译之后的代码,内部代码做了简化:

java 复制代码
public final class LambdaTest2 {
    private final Context context;
    private final LifecycleOwner lifecycleOwner;
    private final TestViewModel viewModel;

    public LambdaTest2(TestViewModel viewModel, LifecycleOwner lifecycleOwner) {
        Intrinsics.checkNotNullParameter(viewModel, "viewModel");
        Intrinsics.checkNotNullParameter(lifecycleOwner, "lifecycleOwner");
        this.viewModel = viewModel;
        this.lifecycleOwner = lifecycleOwner;
    }

    public final void initView2() {
        // 注释1
        this.viewModel.getLiveData().observe(this.lifecycleOwner, new LambdaTest2$sam$androidx_lifecycle_Observer(new LambdaTest2$initView2$1(this)));
    }

    public final void initView() {
        // 注释2
        this.viewModel.getLiveData().observe(this.lifecycleOwner, new LambdaTest2$sam$androidx_lifecycle_Observer(LambdaTest2$initView$1.INSTANCE));
    }
}

final class LambdaTest2$initView$1 extends Lambda implements Function1<String, Unit> {
    // 静态变量,只会创建一次
    public static final LambdaTest2$initView$1 INSTANCE = new LambdaTest2$initView$1();
    ......
}

在上面代码中:

  • 注释 1 位置,也就是initView2方法,这里是Capturing lambdas,引用了外部的变量。编译之后,可以看到,编译器自己构建了一个Observer变量LambdaTest2$sam$androidx_lifecycle_Observer,并传入了一个参数new LambdaTest2$initView2$1(this),这个对象就是我们的 lambda 中的逻辑。因为引用了外部类的变量,所以这里把外部类的对象this传递。所以这里,可以知道每次调用 observe 方法,都会把 lambda 表达式,构建一个对应的新对象。

  • 注释 2 位置,也就是initView方法,这里是 non-capturing lambdas,没有引用外部变量。编译之后,可以看到,同样编译器自己构建了一个Observer变量LambdaTest2$sam$androidx_lifecycle_Observer,但是这里不一样的地方是传递的参数是一个静态对象LambdaTest2$initView$1.INSTANCE,这个对象也是对应的 lambda 的内容

综上比较可以知道:Capturing lambdas(捕获 lambda)会每次把调用的 lambda 的内容,创建一个对应的对象,而 non-capturing lambdas(未捕获 lambda),因为没有引用外部变量,只会创建一次对象,然后把对象当作静态变量传递进去。

那这有什么差异呢?如果单次调用和创建,可能没有什么差异,但是如果是多次调用呢,就有差异了,比如在一个循环体中使用 lambda,就会不一样,因为如果是Capturing lambdas(捕获 lambda)就会每次创建对象,而 non-capturing lambdas(未捕获 lambda)只会创建一个,所以很明显 non-capturing lambdas(未捕获 lambda)的性能更好一些,有效的防止内存的抖动。

所以者对我们实际开发的启示是:尽量使用non-capturing lambdas(未捕获 lambda),特别是在一些循环嵌套的情况下,这样能减少不少中间类的创建。

特殊情况

经过我自己的验证,发现如果在 kotlin 中调用 Java 定义的(ASM)接口时,并不会出现这种情况,比如:

kotlin 复制代码
    fun testForJava() {
        LinearLayout(context).apply {
            // 引用了外部类的变量
            setOnClickListener {
                println("receive data,toast=${message}")
            }
            // 没有应用外部类的变量
            setOnClickListener {
                println("receive data")
            }
        }
    }

如果按照上面的分类,那么编译后,第一个 setOnClickListener 中的 lambda 会 new 一个对象,而第二个 setOnClickListener 会是一个静态变量。但事实上并不是这样,我们可以看下编译后的代码

java 复制代码
    public final void testForJava() {
        LinearLayout $this$testForJava_u24lambda_u242 = new LinearLayout(this.context);
        // 自动创建了一个OnClickListener的匿名内部类
        $this$testForJava_u24lambda_u242.setOnClickListener(new View.OnClickListener() { // from class: com.example.effectkotlin.LambdaTest2$$ExternalSyntheticLambda0
            @Override // android.view.View.OnClickListener
            public final void onClick(View view) {
                // 调用一个静态方法
                LambdaTest2.testForJava$lambda$2$lambda$0(LambdaTest2.this, view);
            }
        });
        // 自动创建了一个OnClickListener的匿名内部类
        $this$testForJava_u24lambda_u242.setOnClickListener(new View.OnClickListener() { // from class: com.example.effectkotlin.LambdaTest2$$ExternalSyntheticLambda1
            @Override // android.view.View.OnClickListener
            public final void onClick(View view) {
                // 调用一个静态方法
                LambdaTest2.testForJava$lambda$2$lambda$1(view);
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void testForJava$lambda$2$lambda$0(LambdaTest2 this$0, View it) {
        Intrinsics.checkNotNullParameter(this$0, "this$0");
        System.out.println((Object) ("receive data,toast=" + this$0.message));
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void testForJava$lambda$2$lambda$1(View it) {
        System.out.println((Object) "receive data");
    }

从上面可以看出,kotlin 针对 Java 的 ASM 接口,并没有Capturing/non-capturing lambdas的概念,都是封装为一个静态方法,如果有外部引用,就当作方法参数进行传递。这里我不太理解为什么会这样做?但是只要记住这里是有差异的就行,在使用的时候注意到这些差别。

如果觉得对你有帮助,请点赞关注,或者关注我交流。也可以点击深入学习Kotlin看其他文章,希望一起学习进步,加油!

相关推荐
00后程序员张6 分钟前
iOS 应用程序使用历史记录和耗能记录怎么查?
android·ios·小程序·https·uni-app·iphone·webview
用户69371750013841 小时前
OS级AI Agent:手机操作系统的下一个战场
android·前端·人工智能
私人珍藏库1 小时前
[Android] 亿连车机版V7.0.1
android·app·软件·车机
用户69371750013842 小时前
315曝光AI搜索问题:GEO技术靠内容投喂操控答案,新型营销操作全揭秘
android·前端·人工智能
进击的cc2 小时前
彻底搞懂 Binder:不止是 IPC,更是 Android 的灵魂
android·面试
段娇娇2 小时前
Android jetpack LiveData (三) 粘性数据(数据倒灌)问题分析及解决方案
android·android jetpack
用户2018792831672 小时前
TabLayout被ViewPager2遮盖部分导致Tab难选中
android
法欧特斯卡雷特2 小时前
Kotlin 2.3.20 现已发布,来看看!
android·前端·后端
闻哥3 小时前
深入理解 MySQL InnoDB Buffer Pool 的 LRU 冷热数据机制
android·java·jvm·spring boot·mysql·adb·面试
我是唐青枫3 小时前
深入理解 C#.NET Task.Run:调度原理、线程池机制与性能优化
性能优化·c#·.net