本文为稀土技术社区首发签约文章,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 控件做透明度和位移动画,并设置了动画时刻和插值器。
整个代码的表达略显烦琐,主要表现在冗余的目标名:animatorSet
,objectAnimator
,objectAnimator2
。其间榜首个目标或许还有存在的价值,比如在某个时分中止或重播动画都需求它。而别的两个目标就显得很冗余,从它们的命令就能够看出很唐塞,其实我不想给他们取一个姓名,由于它们是暂时的目标,用完就弃。但为了给每个子动画设置特点,在 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() :
-
object.apply() 接纳一个 lambda 作为参数。它的语义是:将lambda应用于object目标,其间的 lambda 是一种特别的 lambda,称为带接纳者的lambda。这是 kotlin 中特有的,java 中没有。
带接纳者的lambda的函数体除了能拜访其所在类的成员外,还能拜访接纳者的一切非私有成员,这个特性是它具有魅力的要害。
上述代码中紧跟在 apply() 后的 lambda 函数体除了拜访其外部的变量 span ,还拜访了 AnimatorSet 的 playTogether() 和 start() 办法,就好像在 AnimatorSet 类内部相同。(也能够在这两个函数前面加上
this
,省掉了更简洁)。 -
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() 不太相同:
- 它接纳一个普通的 lambda 作为参数。
- 它将 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()
的三个用法常规:
-
一般情况下 let() 会和
安全调用运算符?
一起运用,即object?.let()
,它的语义是:假如object不为空则对它做一些操作,这些操作能够是调用它的办法,或许将它作为参数传递给另一个函数和
apply()
对比一下,由于 apply() 一般用于构建新目标( let() 用于既有目标),新建的目标不或许为空,所以不需求?
,而且就运用习惯而言,apply() 后的 lambda 中一般只要调用目标的办法,而不会将目标作为参数传递给另一个函数(虽然也能够这么做,只要传this
就能够) -
let() 也会结合
Elvis运算符?:
完成空值处理,当调用 let() 的目标为空时,其 lambda 中的逻辑不会被履行,假如需求指定此时履行的逻辑,能够运用?:
-
当 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()
比较:
- 就传参而言,apply() 传入的是带接纳者的lambda,而 also() 传入的是普通 lambda。所以在 lambda 函数体中前者经过
this
引证调用者,后者经过it
引证调用者(假如不界说参数姓名,默许为it) - 就运用场景而言,
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)
推荐阅读
事务代码参数透传满天飞?(一)
事务代码参数透传满天飞?(二)
全网最高雅安卓控件可见性检测
全网最高雅安卓列表项可见性检测
页面曝光难点分析及应对方案
你的代码太烦琐了 | 这么多目标名?
你的代码太烦琐了 | 这么多办法调用?