你的代码太啰嗦了 | 这么多对象名?

简介: 你的代码太啰嗦了 | 这么多对象名?

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


这一篇从项目实战代码出发讲述如何使用 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,省略了更简洁)。


  1. 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 作为参数。


  1. 它将 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就可以)


  1. let() 也会结合Elvis运算符?:实现空值处理,当调用 let() 的对象为空时,其 lambda 中的逻辑不会被执行,如果需要指定此时执行的逻辑,可以使用?:


  1. 当 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)


  1. 就使用场景而言,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)


推荐阅读


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


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


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


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


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


你的代码太啰嗦了 | 这么多对象名?


你的代码太啰嗦了 | 这么多方法调用?


目录
相关文章
|
2月前
|
JavaScript 前端开发 开发者
方法引用符、引用类方法、引用对象的实例方法、引用类的实例方法及引用构造器
方法引用符、引用类方法、引用对象的实例方法、引用类的实例方法及引用构造器
95 0
记录几个经常用到的泛型方法定义模板
记录几个经常用到的泛型方法定义模板
79 0
常量对象可以更改
常量对象可以更改
94 0
|
Python
Python面向对象、类的抽象、类的定义、类名遵循大驼峰的命名规范创建对象、类外部添加和获取对象属性、类内部操作属性魔法方法__init__()__str__()__del__()__repr__()
面向对象和面向过程,是两种编程思想. 编程思想是指对待同一个问题,解决问题的套路方式.面向过程: 注重的过程,实现的细节.亲力亲为.面向对象: 关注的是结果, 偷懒.类和对象,是面向对象中非常重要的两个概念object 是所有的类基类,即最初始的类class 类名(object): 类中的代码PEP8代码规范:类定义的前后,需要两个空行 创建的对象地址值都不一样如dog和dog1的地址就不一样,dog的地址为2378043254528dog1的地址为2378044849840 8.类内部操作属性 sel
267 1
Python面向对象、类的抽象、类的定义、类名遵循大驼峰的命名规范创建对象、类外部添加和获取对象属性、类内部操作属性魔法方法__init__()__str__()__del__()__repr__()
lodash创建一个新的对象,对象的属性名是和传入对象一样,值则在函数中修改
lodash创建一个新的对象,对象的属性名是和传入对象一样,值则在函数中修改
121 0
CTreeView不是类名或结构名
CTreeView不是类名或结构名
208 0
|
Java
【Groovy】Groovy 方法调用 ( 使用 对象名.成员名 访问 Groovy 类的成员 | 使用 对象名.‘成员名‘ 访问类的成员 | 使用 对象名[‘成员名‘] 访问类成员 )
【Groovy】Groovy 方法调用 ( 使用 对象名.成员名 访问 Groovy 类的成员 | 使用 对象名.‘成员名‘ 访问类的成员 | 使用 对象名[‘成员名‘] 访问类成员 )
239 0
【Groovy】Groovy 方法调用 ( 使用 对象名.成员名 访问 Groovy 类的成员 | 使用 对象名.‘成员名‘ 访问类的成员 | 使用 对象名[‘成员名‘] 访问类成员 )
【Groovy】Groovy 方法调用 ( 使用 对象名.@成员名 访问 Groovy 对象成员 )
【Groovy】Groovy 方法调用 ( 使用 对象名.@成员名 访问 Groovy 对象成员 )
162 0
【Groovy】Groovy 方法调用 ( 使用 对象名.@成员名 访问 Groovy 对象成员 )