本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

写代码犹如写作文,有些代码言简意赅,而有些则啰里吧嗦。

这一篇从项目实战代码动身叙述怎么运用 Kotlin 的域办法Scope functions来简化烦琐的代码。

本篇会包括如下 Kotlin 知识点:扩展函数、带接纳者的lambda、apply()、also()、let()、run()、with()、安全调用运算符、Elvis运算符。

引子

在 Android 将多个动画组合在一起会用到 AnimatorSet

AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(
    tvTitle,
    PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
    PropertyValuesHolder.ofFloat("translationY", 0f, 100f)
);
objectAnimator.setInterpolator(new AccelerateInterpolator());
objectAnimator.setDuration(300);
ObjectAnimator objectAnimator2 = ObjectAnimator.ofPropertyValuesHolder(
   ivAvatar,
   PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
   PropertyValuesHolder.ofFloat("translationY", 0f, 100f)
);
objectAnimator2.setInterpolator(new AccelerateInterpolator());
objectAnimator2.setDuration(300);
animatorSet.playTogether(objectAnimator, objectAnimator2);
animatorSet.start();

上述代码用 Java 一起对 tvTitle 和 ivAvatar 控件做透明度和位移动画,并设置了动画时刻和插值器。

整个代码的表达略显烦琐,主要表现在冗余的目标名:animatorSetobjectAnimatorobjectAnimator2。其间榜首个目标或许还有存在的价值,比如在某个时分中止或重播动画都需求它。而别的两个目标就显得很冗余,从它们的命令就能够看出很唐塞,其实我不想给他们取一个姓名,由于它们是暂时的目标,用完就弃。但为了给每个子动画设置特点,在 Java 中不得不声明一个目标。

而且得读到最后一行代码才知道这段代码的用意,代码的语义无法做到一望而知。

apply

为了解决这些问题,Kotlin 运用体系预界说了一系列域办法。当时场景就能够用到其间的apply()

val span = 300
AnimatorSet().apply {
    playTogether(
            ObjectAnimator.ofPropertyValuesHolder(
                    tvTitle,
                    PropertyValuesHolder.ofFloat("alpha", 0f, 1.0f),
                    PropertyValuesHolder.ofFloat("translationY", 0f, 100f)).apply {
                interpolator = AccelerateInterpolator()
                duration = span
            },
            ObjectAnimator.ofPropertyValuesHolder(
                    ivAvatar,
                    PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f),
                    PropertyValuesHolder.ofFloat("translationY", 0f,100f)).apply {
                interpolator = AccelerateInterpolator()
                duration = span
            }
    )
    start()
}

首要代码中没有出现任何一个目标名,这得益于 apply() :

  1. object.apply() 接纳一个 lambda 作为参数。它的语义是:将lambda应用于object目标,其间的 lambda 是一种特别的 lambda,称为带接纳者的lambda。这是 kotlin 中特有的,java 中没有。

    带接纳者的lambda的函数体除了能拜访其所在类的成员外,还能拜访接纳者的一切非私有成员,这个特性是它具有魅力的要害。

    上述代码中紧跟在 apply() 后的 lambda 函数体除了拜访其外部的变量 span ,还拜访了 AnimatorSet 的 playTogether() 和 start() 办法,就好像在 AnimatorSet 类内部相同。(也能够在这两个函数前面加上this,省掉了更简洁)。

  2. object.apply()的另一个特点是:在它对 object 目标进行了一段操作后还会回来 object 目标自身。

apply()的语义能够归纳为 “我要构建一个目标并一起为其设置特点”

其次,上述代码是有层次的。当去除了冗余目标名后,代码层次就瓜熟蒂落了。在最外层,构建的的目标是 AnimatorSet,其内部又构建了两个 ObjectAnimator 目标,而且它们被组织成一同播放。代码的层次瞬间表达出了这种层次联系(从属联系)。

原理

apply()为啥会具有简化代码的法力?下面是它的源码:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    ...
    block()
    return this // 回来调用目标自身
}

apply 被声明为 T 的扩展办法,T 表示泛型。扩展办法是在类体外为类新增功用的手法。在扩展函数中,能够像类的其他成员函数相同拜访类目标以及它的公共特点和办法。

扩展办法实质是一个静态办法,而且办法的榜首个参数是调用目标,这样在办法内部就能方便地拜访到调用者。

在 apply 中,把调用者把自己作为 lambda 的接纳者,这样在 lambda 内部就能够经过 this 来引证。

apply 在办法内部先履行了传入的 lambda,然后回来调用目标自身。

其间让 lambda 履行的block()语法称为invoke约好,它简化了 lambda 的履行(原型应该是block.invoke()),关于约好背后原理的具体解析能够点击你的代码太烦琐了 | 这么多办法调用?。

用一个简略的 demo 看看 apply() 语法糖背后的完成:

"abcd".apply {
    substring(0,1).length
}

上述代码创建了一个 String 目标abcd,然后对其调用 apply 办法,在其 lambda 内部调用 String.subString()取字串并核算长度。看看编译成 Java 代码是怎么样的:

String var2 = "abce"; // 原始目标
byte var6 = 0;
byte var7 = 1;
// 字串局部变量
String var10000 = var2.substring(var6, var7); 
var10000.length(); // 对局部变量求长度

看完 java 的完成就毫无神奇可言了,便是经过声明冗余布局变量完成的,作为 apply 参数的 lambda 和其调用目标处于同一个 Java 上下文中,所以在 lambda 中能够方便地拜访到原始目标。

陷阱

回看 apply() 的界说:

public inline fun <T> T.apply(block: T.() -> Unit): T {}

apply 被声明为 T 的扩展办法,这里的 T 能够为 null。假定下面这个场景:

class Test {
    fun get():String {
        return "B"
    }
}
class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val test:Test? = null
        test.apply {
            Log.d("test", "${get()}")
        }
    }
    fun get(): String {
        return "A"
    }
}

你猜输出成果是 A 仍是 B?

成果是 A,由于当时 test 目标是 null,所以 apply lambda 中的 this 也是 null。而${get()}隐含的意思是${this.get()},明显这会报空指针反常。幸亏 Activity 中又界说了一个相同签名的 get() 办法,所以就优先指向了它。明显这违背了咱们的原意。

假如把 test 目标改为非空,成果就符合预期了:

class Test {
    fun get():String {
        return "B"
    }
}
class MainActivity : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val test:Test = Test()
        test.apply {
            Log.d("test", "${get()}") // 输出 B
        }
    }
    fun get(): String {
        return "A"
    }
}

这种办法指向目标的变换极具躲藏性,所以在运用 apply 时关于可控类型的调用要十分小心。

let()

let()apply()十分像,但由于下面的两个差异,使得它的应用场景和 apply() 不太相同:

  1. 它接纳一个普通的 lambda 作为参数。
  2. 它将 lambda 的值作为回来值。

在项目中有这样一个场景:启动一个 Fragment 并传 bundle 类型的参数,假如其间的 duration 值不为 0 则显现视图A,不然显现视图B。

public class FragmentA extends Fragment{
    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Bundle argument = getArguments();
        if (argument != null) {
            Bundle bundle = argument.getBundle(KEY);
            if (bundle != null) {
                Long duration = bundle.get(DURATION);
                if (duration != 0) {
                    showA(duration);
                } else {
                    showB()
                }
            }
        }
    }
}

其间声明了3个零时变量:argument,bundle,duration。而且分别对它们做了判空处理。

用 Kotlin 预界说的let()办法简化如下:

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        arguments?.let { arg ->
            arg.getBundle(KEY)
            ?.takeIf { it[DURATION] != 0 }
            ?.let { duration ->showA(duration)} 
            ?: showB()
        }
    }
}

上述代码展现了let()的三个用法常规:

  1. 一般情况下 let() 会和安全调用运算符?一起运用,即object?.let(),它的语义是:假如object不为空则对它做一些操作,这些操作能够是调用它的办法,或许将它作为参数传递给另一个函数

    apply()对比一下,由于 apply() 一般用于构建新目标( let() 用于既有目标),新建的目标不或许为空,所以不需求?,而且就运用习惯而言,apply() 后的 lambda 中一般只要调用目标的办法,而不会将目标作为参数传递给另一个函数(虽然也能够这么做,只要传this就能够)

  2. let() 也会结合Elvis运算符?:完成空值处理,当调用 let() 的目标为空时,其 lambda 中的逻辑不会被履行,假如需求指定此时履行的逻辑,能够运用?:

  3. 当 let() 嵌套时,显现地指明 lambda 参数名称防止it的歧义。在 kotlin 中假如 lambda 参数只要一个则可将参数声明省掉,并用 it 指代它。但当 lambda 嵌套时,it 的指向就有歧义。所以代码顶用arg显现指明这是 Fragment 的参数,用duration显现指明这是 Bundle 中的 duration。

除了上面这种用法,还能够把 let() 当做变换函数运用,就好像 RxJava 中的map()操作符。由于 let() 将 lambda 的值作为其回来值。

比如界说一个回来当时周一的毫秒时办法:

fun thisMondayInMillis() = Calendar.getInstance().let { c ->
    if (c.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) c.add(Calendar.DATE, -1)
    c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
    c.set(Calendar.HOUR_OF_DAY, 0)
    c.set(Calendar.MINUTE, 0)
    c.set(Calendar.SECOND, 0)
    c.set(Calendar.MILLISECOND, 0)
    c.timeInMillis
}

要构建的目标是 Calendar,要回来的确是毫秒时,而且毫秒时的获取依赖于构建的目标。

let() 的源码如下:

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

在办法内部履行了 lambda,而且将调用目标作为参数传入,以便能够经过 it 引证。

with()

上面这个核算毫秒时的例子依然有一些烦琐的成分,由于有重复的目标名.办法()

with() 就用来对此进一步简化:

fun thisMondayInMillis() = with(Calendar.getInstance()) {
    if (get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) add(Calendar.DATE, -1)
    set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
    set(Calendar.HOUR_OF_DAY, 0)
    set(Calendar.MINUTE, 0)
    set(Calendar.SECOND, 0)
    set(Calendar.MILLISECOND, 0)
    timeInMillis
}

一切的目标名都被躲藏了(默许躲藏 this)。

with() 的源码如下:

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

with() 不是一个扩展办法,而是一个顶层办法,它就相当于 Java 中的静态函数,能够在任何地方拜访到。

with() 的榜首参数是一个目标,该目标会成为第二个 lambda 参数的接纳者,这样 lambda 中就能经过 this 引证它。with() 的回来值是 lambda 的核算成果。

with 的语义能够归纳为:我要用当时目标核算出另一个值

run()

还有一个 with() 类似的办法:

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

调用目标也是作为 lambda 的接纳者,而且将 lambda 的值作为整体回来值。

唯一的差异是,run() 是一个扩展办法。

用 run() 改造上面的例子:

fun thisMondayInMillis() = Calendar.getInstance().run {
    if (get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) add(Calendar.DATE, -1)
    set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
    set(Calendar.HOUR_OF_DAY, 0)
    set(Calendar.MINUTE, 0)
    set(Calendar.SECOND, 0)
    set(Calendar.MILLISECOND, 0)
    timeInMillis
}

我想不出 run() 和 with() 具体的运用场景上的差异,彻底看你是喜欢目标.run {}仍是with(目标) {}

also()

also()简直和 let() 相同,唯一的却别是它会回来调用者自身而不是将 lambda 的值作为回来值。

和相同回来调用者自身的apply()比较:

  1. 就传参而言,apply() 传入的是带接纳者的lambda,而 also() 传入的是普通 lambda。所以在 lambda 函数体中前者经过this引证调用者,后者经过it引证调用者(假如不界说参数姓名,默许为it)
  2. 就运用场景而言,apply()更多用于构建新目标并履行一顿操作,而also()更多用于对既有目标追加一顿操作。

在项目中,有一个界面初始化的时分需求加载一系列图片并保存到一个列表中:

listOf(
    R.drawable.img1,
    R.drawable.img2, 
    R.drawable.img3,
    R.drawable.img4, 
).forEach { resId ->
    BitmapFactory.decodeResource(
        resources, 
        resId, 
        BitmapFactory.Options().apply {
            inPreferredConfig = Bitmap.Config.ARGB_4444
            inMutable = true
        }
    ).also { bitmap -> imgList.add(bitmap) }
}

这个场景顶用let()也没什么不能够。可是假如还需求将解析的图片轮番显现出来,用also()就再好不过了:

listOf(
    R.drawable.img1,
    R.drawable.img2, 
    R.drawable.img3,
    R.drawable.img4, 
).forEach {resId->
    BitmapFactory.decodeResource(
        resources, 
        resId, 
        BitmapFactory.Options().apply {
            inPreferredConfig = Bitmap.Config.ARGB_4444
            inMutable = true
        }
    ).also { bitmap ->
        //存储逻辑
        imgList.add(bitmap) 
    }.also { bitmap ->
        //显现逻辑
        ivImg.setImageResource(bitmap)   
    }
}

由于also()回来的是调用者自身,所以能够also()将不同类型的逻辑分段,这样的代码更容易理解和修改。这个例子逻辑比较简略,只要一句话,将他们合并在一起也没什么不好。

also() 的源码如下:

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

在办法内部履行 lambda 并回来目标自身。它的 lambda 不带有接纳者,而是直接把调用者作为 lambda 的参数传入,所以不能经过 this 拜访到调用者。

知识点总结

  • 扩展函数是一种能够在类体外为类新增功用的特性,在扩展函数体中能够拜访类的成员(除了被private和protected润饰的成员)
  • 带接纳者的lambda是一种特别的lambda,在函数体中能够拜访接纳者的非私有成员。能够把它理解成接纳者的扩展函数,只不过这个扩展函数没有函数名。
  • apply() also() let() with() run() 是体系预界说的扩展函数。它们被称为域办法scope funciton,它们都用于在一个目标上履行一顿操作,并回来一个值。差异在于怎么引证目标,以及回来值(详见下表)。域办法的价值在于将和目标相关的操作内聚在一个域(lambda)中,以削减冗余目标的声明,打到简化代码的效果。
  • ?.称为安全调用运算符,若object?.fun()中的 object 为空,则fun()不会被调用。
  • ?:称为Elvis运算符,它为 null 供给了默许逻辑,funA() ?: funB(),假如 funA() 回来值不为 null 则履行它并将它的回来值作为整个表达式的回来值,不然履行 funB() 并采用它的回来值。
域办法 回来值 引证调用者方式 语义
apply 调用者自身 this(可省掉,不行重命名) 构建目标的一起设置特点
let lambda 的值 it(不行省掉,可重命名) 高雅的空安全写法
also 调用者自身 it(不行省掉,可重命名) 将对同一目标不同类型的操作分段处理
with lambda 的值 this(可省掉,不行重命名) 利用当时目标核算出另一个值
run lambda 的值 this(可省掉,不行重命名) 利用当时目标履行一段操作并回来另一个值

参阅

Kotlin(run,apply)陷阱

Scope functions | Kotlin (kotlinlang.org)

推荐阅读

事务代码参数透传满天飞?(一)

事务代码参数透传满天飞?(二)

全网最高雅安卓控件可见性检测

全网最高雅安卓列表项可见性检测

页面曝光难点分析及应对方案

你的代码太烦琐了 | 这么多目标名?

你的代码太烦琐了 | 这么多办法调用?