kotlin常用的内置函数also apply let run with之间的异同点

378 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

前言

在Kotlin的源码 Standard.kt 中提供了一些 Kotlin 扩展的内置函数可以优化kotlin的编码。

我们常用的几个函数 also apply let run with 都是来自与 Standard 类,它们的唯一目的是在对象的上下文中执行代码块。当对一个对象调用这样的函数并提供一个 lambda 表达式时,它会形成一个临时作用域。在此作用域中,可以访问该对象而无需其名称。这些函数称为作用域函数

那他们有什么区别呢?使用的时候又该如何使用?

一、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()
}

一种是直接用的,另一种是扩展方法,对象调用的。

其实一个参数是高阶函数,一个是扩展方式的高阶函数,我在之前的文章已经讲过很多次了,其本质就是高阶函数。

他们唯一的区别就是this和it的区别,this不可以用自定义变量来代替,而it可以使用自定义变量名来接收。

这里 callsInPlace 方法是一个校验方法,这个函数有两个作用,一是确认当前函数调用的位置是否恰当,二是确认当前函数调用的次数。

run的使用的两种方式:

非扩展的使用:

        val num = 123

        val num1 = kotlin.run {
            YYLogUtils.w("num :" + num)
            return@run num + 1
        }
        YYLogUtils.w("num1 :" + num1)

这样是非扩展的方式,可以定义一个作用域,同时我们可以return一个对象出去。

只定义代码边界

        run outside@{
            mViewModel.mAllDeployments?.forEach {
                //替换掉休息时间
                if (it.id == deployment_id) {
                    it.break_time = time
                    return@outside
                }
            }
        }

比如我们可以在遍历中,协程中或者其他的一些任务中很方便的返回一个对象,因为他是一个作用域函数,run函数创建了一个作用域,我们直接指定返回到这个作用域,这个循环就会终止。

扩展方式的run方法使用

webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

因为扩展方式的run函数,它的参数是高阶扩展函数,所以我们可以直接使用this来设置属性。

二、with 函数

说完run函数之后,我们再看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()
}

它和run函数的区别:函数的参数不同,需要传入对象,由于第二个参数也是高阶扩展函数,所以其他的使用都是相同的

        val num5 = with(num) {
            toString().trim()
            return@with 111
        }
        YYLogUtils.w("num5 :$num5")

打印结果:

三、let 函数

有点不同了哦,它和run的扩展方法相比,一个参数是高阶扩展函数,一个是高阶函数。 block: (T) -> Rblock: T:() -> R 的区别而已,本质还是一样的。

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

由于是高阶函数作为参数,所以我们使用it来操作。

        val num4 =  num.let {
            it.toString().trim()
            return@let "222"
        } 
        YYLogUtils.w("num4 :$num4")

Log如下

run with let的区别


with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}

webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}

webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

let 和 run 其实差不多,都是返回最后一行或者指定return

let和run的返回值相同,它们的区别主要在于作用域内使用it和this的区别。 一般来说,如果调用者的属性和类中的属性同名,则一般会使用let,避免出现同名的赋值引起混乱。

四、apply 函数

又有一点不同了,如同把前三者做一个比较一样,后两者之间常常用作一个比较,他们都是返回this,也就是对象本身。

只是他们区分了this和it,也就是高阶扩展函数当参数还是高阶函数当参数的问题。

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

由于使用的是高阶扩展函数,所以使用的时候要用this。

    num.apply {
        toString().trim()
    }

由于返回的this,使用的时候压根就不能return,如果强制return也会直接报错。

五、also 函数

also是与apply想对应的函数,它的参数是高阶函数,所以我们使用的私有使用it接收,当然我们也可以指定参数接收

    num.also { value ->
        value.toString().trim()
    }

同样的也不能手动的指定返回,因为返回的是对象本身。

从本质上来说他们都差不多,都可以用,但是我们根据函数的名称,和一些使用习惯,它们在使用的时候,还是有一些细微的差别的, also强调的是【与调用者无关的操作】,而apply强调的是【调用者的相关操作】,例如下面的这个例子。

test?.also {
    //跟自己无关的逻辑
    println("some log")
}?.apply {
    //修改自己内部的属性
    name = "xys"
}

总结

用一个图片来总结他们就是:

其实我们常用的就是run与let apply与also。他们两两相对,主要是以是否返回自身作为区别。

run与let返回的是最后一行或者手动return的数据,而apply与also他们返回的是自身,且不支持自定义return,会编辑器报错。

用一个示例来说明就是:

 //new一个animset,传入的是对象本身,apply用run好像也可以
 AnimatorSet().apply {
    ObjectAnimator.ofPropertyValuesHolder(
            textView,
            PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f),
            PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f)
    ).apply {
        duration = 300L
        interpolator = LinearInterpolator()
    }.let {
        play(it).with(
                ObjectAnimator.ofPropertyValuesHolder(
                        button,
                        PropertyValuesHolder.ofFloat("translationX", 0f, 100f)
                ).apply {
                    duration = 300L
                    interpolator = LinearInterpolator()
                }
        )
        play(it).before(
                ValueAnimator.ofInt(ivRight,screenWidth).apply { 
                    addUpdateListener { animation -> imageView.right= animation.animatedValue as Int }
                    duration = 400L
                    interpolator = LinearInterpolator()
                }
        )
    }
    addListener(object : Animator.AnimatorListener {
        override fun onAnimationRepeat(animation: Animator?) {}
        override fun onAnimationEnd(animation: Animator?) {
            Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
        }
        override fun onAnimationCancel(animation: Animator?) {}
        override fun onAnimationStart(animation: Animator?) {}
    })
    start() 
}

上述代码是构架一个动画,将动画1和动画2一起播放,将动画3在动画1之后播放。

其实源码非常的简单,我们有一次复习了高阶函数参数与高阶扩展函数参数的区别,关于这一点可以看看我之前的文章。

好了,本文的全部代码与Demo都已经开源。有兴趣可以看这里。项目会持续更新,大家可以关注一下。

如有讲解不到位或错漏的地方,希望同学们也可以指出交流。

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。