你了解Kotlin的let,with,run,apply,also作用域函数的区别吗?

前言: 成和败要努力尝试,人若有志应该不怕迟。

一、概述

  Kotlin提供了不少比Java高级的语法,在Kotlin标准库中(Standard.kt)提供了一些Kotlin拓展的内置函数,可以优化编码,比如let,with,run,apply,also等。Standard.kt是Kotlin库的一部分,它定义了一些基本函数,是一种使代码更简洁的方法,功能非常强大。下面会详细讲解,同时也涉及到takeIftakeUnless函数。

1.1 Kotlin回调函数的优化

Kotlin中对Java一些接口的回调做了优化,可以使用lambda函数来替代,可以简化代码和一些不必要的嵌套回调方法。但是注意:在lambda表达式,只支持单抽象方法模型,也就是说设计的接口里面只有一个抽象方法,才符合lambda表达式的规则,多个回调方法不支持。

  • 1、用Java代码实现一个接口回调
    mView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
			//TODO
        }
    });
  • 2、在Kotlin中实现一个接口的回调,不使用lambda表达式(这种方法非常适用于Kotlin中一个接口有多个回调方法)
    mView.setOnClickListener(object : View.OnClickListener {
        override fun onClick(view: View?) {
			 //TODO
        }
    })
  • 3、如果在Kotlin中接口的回调方法只有一个,那么就符合使用lambda函数,我们可以把以上代码简化:
    mView.setOnClickListener({
        view: View? ->
        //TODO
    })
    
    //再进一步简化,可以把View?直接直接省略
    mView.setOnClickListener({
        view ->
        //TODO
    })
  • 4、如果上面的参数view没有使用到的话,可以直接把view去掉:
    mView.setOnClickListener({
        //TODO
    })
  • 5、上面的代码还可以做进一步的调整,如果setOnClickListener()函数的最后一个参数是一个函数的话,可以把函数{}的实现提到圆括号()外面:
    mView.setOnClickListener() {
        //TODO
    }
  • 6、如果setOnClickListener()函数只有一个参数的话,则可以直接省略圆括号():
    mView.setOnClickListener {
        //TODO
    }

经过层层简化最终可以写成 mView.setOnClickListener { //TODO }这样的简洁模式。但是注意了, 这种简化模式支持接口里面只有一个回调方法,多个回调方法不支持。

二、作用域函数let,with,run,apply,also详解

2.1 let函数

  let函数实际是一个作用域函数,当你需要定义一个变量在一个特定的作用域范围内,let函数是一个不错的选择,它的另一个作用是避免写一些判断null的操作。

(1)let函数的底层

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

表示以this值作为参数调用指定的函数[block]并返回结果。let函数的底层是inline拓展函数+lambda结构模式,从结构来看它只有一个lambda函数块[block]作为参数的函数,调用T类型对象的let函数,则该对象为该函数的参数。在代码块内可以通过it替代该对象。返回值为函数块的最后一行。

(2)let函数的一般使用语法

	//1.在函数体内使用it替代object对象去访问其公有的属性和方法
	object.let{
   		it.todo()//it即表示object,通过it可以操作对象的相关方法
   		...
	}

	//2.判断object为null的操作
	object?.let{//表示object不为null的条件下,才会去执行let函数体
   		it.todo()
   		···
	}

(3)let函数的Kotlin和Java同等含义转化

	//Kotlin
    private fun letForKotlin() {
        val result = "HelloWord".let {
            Log.e(TAG, "let == length:" + it.length)
            1818
        }
        Log.e(TAG, "let == length:" + result)
    }

	//Java
    private void letForJava() {
        String str = "HelloWord";
        Log.e(TAG, "let == length:" + str.length());
        int result = 1818;
        Log.e(TAG, "let == result:" + result);
    }

上面的Kotlin和Java两种写法所表示的意义和结果是一样的,打印log如下:
在这里插入图片描述
(4)let函数的使用场景

  • 场景A:处理一个可为null的对象,统一做空判断处理;
  • 场景B:需要去明确一个变量所处的特定作用域范围内可使用。

(5)Kotlin中使用let函数的前后对比

我们经常使用某个对象,每次都使用该对象做空判断处理然后操作相关方法,这样看起来不够优雅,如下:

    //let函数优化前
    mTextView?.setLines(2)
    mTextView?.setText("HelloWord")
    mTextView?.setOnClickListener(this)
    mTextView?.setTextColor(ContextCompat.getColor(this, R.color.colorAccent))

使用let函数优化后:

   //let函数优化后
   mTextView?.let {
        it.setLines(2)
        it.setText("HelloWord")
        it.setOnClickListener(this)
        it.setTextColor(ContextCompat.getColor(this, R.color.colorAccent))
    }

这样使用let优化后代码就相对美观很多了,上面提到let函数里面的it表示mTextView对象。

2.2 with函数

  一个非拓展函数,上下文对象作为参数传递,但是在lambda内部,它作为[receiver] (this)可用,返回值是lambda结果。当你需要的一个对象在一个特定的作用域范围内多次使用到其方法时,可以省去对象名,直接访问对象的公有属性和方法。

(1)with函数的底层

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

表示使用给定的[receiver]作为接收方调用指定的函数[block]并返回结果。with函数与前面的函数略有不同,因为它不是以拓展的形式存在的。with接收了两个参数,分别为T类型的对象receiver和一个lambda函数块。在函数块内可以通过this指定该对象,返回值为函数的最后一行。

(2)with函数的一般使用语法

 	with(object){
 		//函数块内的this表示传入的object对象,同时可以直接调用该对象的方法
   		//todo
 	}

(3)with()函数的Kotlin和Java同等含义转化

	//Kotlin
    private fun withForKotlin() {
        var user = User("张三", 27, "男")
        val result = with(user, {
            "姓名:" + name + ", 年龄:" + age + ", 性别:" + sex
        })
        Log.e(TAG, "with == " + result)
    }

	//Java
    private void withForJava() {
        User user = new User("张三", 27, "男");
        String result = "姓名:" + user.getName() + ", 年龄:" + user.getAge() + ", 性别:" + user.getSex();
        Log.e(TAG, "with == " + result);
    }

上面的Kotlin和Java两种写法所表示的意义和结果是一样的,打印数据如下:
在这里插入图片描述
(4)with函数的使用场景

适用于同一个对象的多个方法时,可以省去类名重复,直接调用类的方法即可。

  • 场景A:建议在不提供lambda结果的情况下调用上下文对象上的函数,在代码中,with可以理解为“使用这个对象,执行以下的操作”。
    val list = mutableListOf("one", "two", "three")
    with(list) {
        Log.e(TAG, "with == argument:" + this + ", 列表长度为:" + size)
    }

打印数据如下:
在这里插入图片描述

  • 场景B:with()函数的另一个用例是引入一个helper对象,它的属性或者函数将用于计算值。
    val list = mutableListOf("one", "two", "three")
    val result = with(list) {
        "list第一个参数:" + first() + ", 最后一个参数是:" + last()
    }
    Log.e(TAG, "with == " + result)

打印数据如下:
在这里插入图片描述
(5)Kotlin中使用with函数的前后对比

在RecyclerView中adapter的onBindItemHolder()方法中,数据model映射到UI上面,比较适合适用with()函数。

   override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
        val user = mDataList[position] ?: return
        
        val name = user.getName()
        val age = user.getAge()
        val sex = user.getSex()
        
        holder?.tv_name.text = name
        holder?.tv_age.text = age
        holder?.tv_sex.text = sex
    }

使用with函数优化后:

    override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
        val user: User = mDataList[position] ?: return

        with(user) {
            holder?.tv_name.text = name
            holder?.tv_age.text = age
            holder?.tv_sex.text = sex
        }
    }

2.3 run函数

  run函数上下文对象可以用作接收方[receiver] (this),返回值是lambda结果。与with执行相同的操作,但是作为上下文对象的拓展函数调用let函数。当lambda包含对象初始化和返回值的计算时,run非常有用。除了调用在[receiver]对象上运行之外,还可以用作非拓展函数。非拓展运行在需要表达的地方执行多个语句块。

(1)run函数的底层

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

表示以this值作为接收方[receiver] 调用指定的函数[block]并返回其结果。底层是inline拓展函数+lambda结构模式,从结构来看它只有一个lambda函数块[block]作为参数的函数,调用T类型对象的run函数。它只接收一个lambda函数为参数,以闭包的形式返回,返回值是最后一行。

(2)run函数的一般使用语法

	object.run{
		//函数块内的this表示object对象,同时可以直接调用该对象的公有属性和方法
		//todo
	}

(3)run函数的Kotlin和Java同等含义转化

	//Kotlin
    private fun runForKotlin() {
        var user = User("李思思", 18, "女")
        val result = user?.run {
            val userStr = "姓名:" + name + ", 年龄:" + age + ", 性别:" + sex
            Log.e(TAG, "run == user:" + userStr)

            1919
        }
        Log.e(TAG, "run == result:" + result)
    }
    
	//Java
    private void runForJava() {
        User user = new User("李思思", 18, "女");
        String userStr = "姓名:" + user.getName() + ", 年龄:" + user.getAge() + ", 性别:" + user.getSex();
        Log.e(TAG, "run == user:" + userStr);
        int result = 1919;
        Log.e(TAG, "run == result:" + result);
    }

上面的Kotlin和Java两种写法所表示的意义和结果是一样的,打印log如下:
在这里插入图片描述
(4)run函数的使用场景

适用于let函数和with函数的任何场景。run函数其实就是 letwith两个函数的结合体,准确来说它弥补了let函数在函数内必须适用it参数替代对象;另一方面它弥补了with函数传入对象判空问题。所以run函数可以像with函数那样省略对象参数直接访问对象的公有属性和方法,同时像let函数那样对对象做空判断处理。

(5)Kotlin中使用run函数的前后对比

在RecyclerView中adapter的onBindItemHolder()方法中,获取数据先空判断item数据,然后再获取具体数据:

    override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
        val user: User = mDataList[position] ?: return

        with(user) {
            holder?.tv_name.text = name
            holder?.tv_age.text = age
            holder?.tv_sex.text = sex
        }
    }

使用run函数优化后:

    override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
        mDataList[position]?.run {
            holder?.tv_name.text = name
            holder?.tv_age.text = age
            holder?.tv_sex.text = sex
        }
    }

这样就可以先做item数据对象空判断处理,再直接引用对象的公有属性和方法。

2.4 apply函数

  上下文对象可用作接收者(this),返回值是对象本身。对于没有返回值并且主要对接收方对象的成员进行操作的代码块使用applyapply函数的常见情况是对象配置,这样的调用可以理解为“对对象的应用以下赋值”。

(1)apply函数的底层

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

this值作为接收方[receiver]调用指定函数[block]并且返回this值。底层是inline拓展函数+lambda结构模式,从结构来看apply函数和run函数很像,只有一个lambda函数块[block]作为参数的函数,调用T类型对象的apply函数,它只接收一个lambda函数为参数,返回值是对象本身。

(2)apply函数的一般使用语法

	object.apply{
		//类似run函数,不同的是返回值为对象本身,即object
		//todo
	}

(3)apply函数的Kotlin和Java同等含义转化

	//Kotlin
    private fun applyForKotlin() {
        val list = mutableListOf("one", "two", "three")
        val result = list.apply {
        	val value = "apply == list第一个参数::" + first() + ", 列表长度为:" + size
            Log.e(TAG, value )
            2020
        }
        Log.e(TAG, "apply == list:" + result)
    }
    
	//Java
    private void applyForJava() {
        List<String> list = new ArrayList<>();
        list.add("one");
        list.add("two");
        list.add("three");
		val i = 2020
		
        String value = "apply == list第一个参数:" + list.get(0) + ", 列表长度为:" + list.size();
        Log.e(TAG, value);
        String result = "apply == list:" + list;
        Log.e(TAG, result);
    }

上面的Kotlin和Java两种写法所表示的意义和结果是一样的,打印log如下:
在这里插入图片描述
(4)apply函数的使用场景

apply函数整体上和run函数相似,唯一不同就是它的返回值是对象本身。apply函数一般用于对象实例初始化的时候,需要对对象中的属性进行赋值;或者动态inflate一个View的时候需要给View绑定数据。

(5)Kotlin中使用apply函数的前后对比

    mHeadView = View.inflate(activity, R.layout.head_task_view, null)
    mHeadView?.tv_name?.text = "姓名XXX"
    mHeadView?.tv_age?.text = "20"
    mHeadView?.tv_sex?.text = "女"
    mHeadView?.tv_name?.setOnClickListener(this@KExampleActivity)
    
    mAdpter.addHeadView(mHeadView)

使用apply函数优化后:

    mHeadView = View.inflate(activity, R.layout.head_task_view, null).apply {
        tv_name?.text = "姓名XXX"
        tv_age?.text = "20"
        tv_sex?.text = "女"
        tv_name?.setOnClickListener(this@KExampleActivity)
    }
    
    mAdpter.addHeadView(mHeadView)

apply函数通过将接收方[receiver] this作为返回值,可以轻松地将apply包含到调用链中,以便进行更复杂的处理。

2.5 also函数

  上下文对象可以作为参数it使用,返回值是对象本身。也适用于执行一些将上下文作为参数对象的操作,也可用于需要引用对象而不是引用对象的属性和函数的操作,或者当你不想从外部作用域隐藏该引用时。你可以理解为“并对该对象执行以下操作”。

(1)also函数的底层

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

表示以this值作为接受方[receiver]调用指定的函数[block]并返回结果。底层是inline拓展函数+lambda结构模式,从结构来看它只有一个lambda函数块[block]作为参数的函数,调用T类型对象的also函数。返回值是传入对象本身this

(2)also函数的一般使用语法

	object.also{
		//与let函数类似,返回值为object对象本身
		//todo
	}

(3)also函数的Kotlin和Java同等含义转化

	//Kotlin
    private fun alsoForKotlin() {
        val result = "HelloWord".also {
            Log.e(TAG, "also == length:" + it.length)
            2121
        }
        Log.e(TAG, "also == result:" + result)
    }

	//Java
    private void alsoForJava() {
        String result = "HelloWord";
        Log.e(TAG, "also == length:" + result.length());
        int i = 2121;
        Log.e(TAG, "also == result:" + result);
    }

上面的Kotlin和Java两种写法所表示的意义和结果是一样的,打印log如下:
在这里插入图片描述
(4)also函数的使用场景

适用于let函数的任何场景,与let函数不同的是let函数以闭包的形式返回函数块最后一行的值,如果最后一行值为空则返回一个Unit类型的默认值,而also函数返回的是传入对象本身。同时可以对传入的对象进行操作,一般用于多个拓展函数的链式调用。

(5)Kotlin中使用also函数的前后对比

在遍历List元素后再添加一个子元素

    val list = mutableListOf("one", "two", "three")
    for (i in list.indices) {
        Log.e(TAG, "apply == element:" + list[i])
    }
    list.add("four")

also函数优化后:

    val list = mutableListOf("one", "two", "three")
    list.also {
        for (i in it.indices) {
            Log.e(TAG, "apply == element:" + it[i])
        }
    }.add("four")

三、总结及其他用法

3.1 作用域函数总结

为了更好理解和选择函数的正确范围,我们用表格总结一下:

函数 函数块对象引用 返回值 是否拓展函数 使用场景
let it Lambda表达式结果 1.适用于处理不为null的操作场景;
2.明确一个变量所处的特定作用域范围内可使用。
with this Lambda表达式结果 否(上下文对象作为参数) 适用于同一个对象的公有属性和函数调用。
run this Lambda表达式结果 适用于let函数和with函数的任何场景。对对象中的属性进行赋值和计算结果;
或者在需要表达式的地方运行语句。
否(调用时没有上下文对象)
apply this 返回this
(对象本身)
1.一般用于对象实例初始化的时候,需要对对象中的属性进行赋值;
2.动态inflate一个View的时候需要给View绑定数据。
also it 返回this
(对象本身)
适用于let函数的任何场景,对传入的对象进行操作,
一般用于多个拓展函数的链式调用。

总的来说,不同函数的功能相互重叠,可以根据实际情况来使用作用域函数。尽管作用域函数是一种使代码更简洁的方法,但是要避免过度使用,它会降低代码的可读性并导致错误。避免嵌套作用域函数,在链式调用时要注意,当前上下文对象和this或者it的值。

3.2 takeIf和takeUnless

  除了作用域函数外,标准库还提供了takeIftakeUnless函数,这些函数允许你在调用链中嵌入对对象状态的检查。

在提供某条件的对象上调用,如果与某条件匹配,则takeIf返回该对象,否则它返回nulltakeIf是针对单个对象的过滤函数。反过来,如果不匹配某条件,则takeUnless返回对象,如果匹配则返回null,对象可以作为lambda参数(it)使用。

(1)takeIf和takeUnless函数的底层

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}

这是takeIf函数源码,表示如果满足给定条件,则返回this值(对象本身),如果不满足则返回null

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (!predicate(this)) this else null
}

这是takeUnless函数源码,表示如果不满足给定条件,则返回this值(对象本身),如果满足则返回null

takeIf函数和takeUnless函数的底层都是inline拓展函数+lambda结构模式,从结构来看它只有一个lambda函数块[predicate]作为参数的函数,函数块内返回值类型必须为Boolean类型。调用T类型对象的takeIf或者takeUnless函数,该对象为该函数的参数。

(2)takeIf和takeUnless函数的一般使用语法

	object.takeIf{
		//函数体是Boolean类型,条件成立返回number,不成立返回null
		//todo
	}

	object.takeUnless{
		//函数体是Boolean类型,条件成立返回null,不成立返回number
		//todo
	}

(3)takeIf和takeUnless函数的Kotlin和Java同等含义转化

  • takeIf
	//Kotlin
    private fun takeIfForKotlin(number: Int) {
        val result = number.takeIf {//条件成立返回number,不成立返回null
            it > 0
        }
        Log.e(TAG, "takeIf == result:" + result)
    }

	takeIfForKotlin(2222)//调用

	//Java
	private void takeIfForJava(int number) {
        Integer result;
        if (number > 0) {
            result = number;
        } else {
            result = null;
        }
        Log.e(TAG, "takeIf == result:" + result);
    }

	takeIfForJava(2222);//调用

上面takeIf函数的Kotlin和Java两种写法所表示的意义和结果是一样的,打印log如下:
在这里插入图片描述

  • takeUnless
	//Kotlin
    private fun takeUnlessForKotlin(number: Int) {
        val result = number.takeUnless {
            //条件成立返回null,不成立返回number
            it > 0
        }
        Log.e(TAG, "takeIf == result:" + result)
    }
    
    takeUnlessForKotlin(2323)//调用
    
	//Java
    private void takeUnlessForJava(int number) {
        Integer result = number > 0 ? null : number;
        Log.e(TAG, "takeUnless == result:" + result);
    }
    
	takeUnlessForJava(2323);//调用

上面takeUnless函数的Kotlin和Java两种写法所表示的意义和结果是一样的,打印log如下:
在这里插入图片描述
(4)takeIf和takeUnless函数与作用域函数

takeIftakeUnless函数与作用域函数一起使用特别有用,当在takeIftakeUnless之后链接其他函数,必须执行空检查或者安全调用(?.),因为它们的返回值可能为空(null)。

比如将它们和let函数链接起来,以便在匹配给定某条件的对象上运行代码块,所以在对象上调用takeIf或者takeUnless,需要使用安全调用(?.)调用let,对于不匹配某条件的对象,返回nulllet函数不被调用。

	//Kotlin
    private fun synForKotlin(str: String) {
        str.takeIf { !it.isNullOrEmpty() }?.let {
            Log.e(TAG, "syn == result:" + it.toUpperCase())
        }
    }
    //调用
    synForKotlin("HelloWord")
    synForKotlin("")

	//Java
	private void synForJava(String str) {
        if (!TextUtils.isEmpty(str)) {
            Log.e(TAG, "syn == result:" + str.toUpperCase());
        }
    }
    //调用
	synForJava("HelloWord");
    synForJava("");

上面Kotlin和Java两种写法所表示的意义和结果是一样的,打印log如下:
在这里插入图片描述
总之一句话:takeIf表示如果满足给定条件,则返回this值(对象本身),如果不满足则返回null。takeUnless表示如果不满足给定条件,则返回this值(对象本身),如果满足则返回null。两个正好相反。

至此!本文结束。


源码地址:https://github.com/FollowExcellence/KotlinDemo-master

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章