LJ的Blog

学海无涯苦做舟

0%

Kotlin委托 & 扩展 & 高阶函数

写在前面

Kotlin现在已经是Android官方的一级开发语言了,以前就有大佬给我安利,最近刚好看open cv的c++和ndk看的头昏脑涨,反正最近也用不到,只是出于兴趣,不如换个脑子看看最近势头比较盛的Kotlin好了。在这里感谢一下猫哥对我的耐心指导,让我对Kotlin的认识更进了一步。

委托

委托有委托类和委托属性。

委托类

我在看文档的时候就感觉跟Java里的某个操作非常像……于是非常恶趣味的将代码写成了如下模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface OnClickListener {
fun click()
}

class MyListener : OnClickListener {
override fun click() {
println("do something about click event")
}
}

class MainActivity(b: OnClickListener) : OnClickListener by b {
init {
println("we can do something about activity here")
}
}

fun main(args: Array<String>) {
val listener = MyListener()
val activity = MainActivity(listener)
activity.click()
}

// 输出:
// we can do something about activity here
// do something about click event

是的,我觉得这个类委托跟Java里设置接口回调这个操作很像。只不过一般情况下,我们会使用匿名类来实现这个接口,并在实现中写上我们的代码。而委托类在文档中的描述是:The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin supports it natively requiring zero boilerplate code.

又到了展示我辣鸡英语水平的时候了:委托模式是一种实现继承的良好方式,Kotlin原生支持不需要额外的样板代码。(意译,不完全符合字面意思)

也就是说跟我想的差不多,Kotlin语言本身提供了委托,可以让我们省了setxxxListener这种重复劳动。

委托属性

委托属性官方举了三个应用场景:

  • 延迟属性:只在第一次访问的时候初始化(计算)

  • 可观察属性:监听器得到关于这个特性变化的通知

  • 把所有属性存储在一个map中,而不是在单独的字段里

假如现在在项目里需要一个地点名称的属性,这个地点并不是每一次都需要用到,我们只在有需要的时候获取这个属性,我们就可以使用Kotlin提供的lazy来实现:

1
2
3
4
5
   val address: String by lazy {
getAdd()
}

fun getAdd(): String = "北京天安门"

下面是可观察属性,Kotlin中提供了Observable委托,来看看是怎么用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PropertyDelegate {

var status: Int by Delegates.observable(0) {
d, old, new ->
println("属性名称:$d,以前是:$old,现在是:$new")
}
}

fun main(args: Array<String>) {
var p = PropertyDelegate()

println(p.status)
p.status = 2
}
// 输出:
// 0
// 属性名称:var PropertyDelegate.status: kotlin.Int,以前是:0,现在是:2

的确观察到了属性值的变化,上面observable(0)中的0作为status的初始化值,后面的那种写法暂时先放着,是将函数作为参数的一种使用方式。
接下来看看将属性存储在Map中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestUser(val map: Map<String, Any?>) {

val name: String by map
val age: Int by map
}

fun main(args: Array<String>) {
var user: TestUser = TestUser(mapOf(
"name" to "test",
"age" to 16
))

println("username:" + user.name + ",age:" + user.age)
}
// 输出:
// username:test,age:16

上面三个都是Kotlin提供的委托,我们也可以自己实现属性委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Http {

var name: String? = null

fun getNameInfoFromNetWork(): String {
return name ?: "假装我是从网络获取的名字"
}

operator fun getValue(any: Any, property: KProperty<*>): String {
println("打印观察:any=$any property=$property")
return getNameInfoFromNetWork()
}

operator fun setValue(any: Any, property: KProperty<*>, s: String) {
this.name = s
}
}

class PropertyDelegate {
var name: String by Http()

val address: String by lazy {
getAdd()
}

fun getAdd(): String = "北京天安门"

var status: Int by Delegates.observable(0) {
d, old, new ->
println("属性名称:$d,以前是:$old,现在是:$new")
}


}

fun main(args: Array<String>) {
var p = PropertyDelegate()
println(p.name)
p.name = "hh"
println(p.name)
}
// 输出:
// 打印观察:any=PropertyDelegate@2e5d6d97 property=var PropertyDelegate.name: kotlin.String
// 假装我是从网络获取的名字
// 打印观察:any=PropertyDelegate@2e5d6d97 property=var PropertyDelegate.name: kotlin.String
// hh

var声明的是可变量,需要实现set和get,而val是不可变量,只需要实现get。

简单的介绍了一下官方列举的这三个应用场景,其实感觉这三个例子并不能戳中我的痛点,因为现有的Java虽然实现起来啰嗦了点,但也并不是非常麻烦。比如可观察的属性,我只要在将属性声明为private,然后在set方法里设置一个接口回调就可以了,接口再弄个泛型,得了,所有可观察属性都能用这个接口了。至于后面两个也是同样的,但是这是语言层面的直接支持,喂你糖吃总是得心怀感激的,总好过吃翔不是。接下来就来了解一下Kotlin比较牛x的两个东西:扩展和高阶函数。

扩展 & 高阶函数

扩展是Kotlin中比较能打动我的一个特性,比如原来的Android中的Toast需要这么写:

1
Toast.makeText(mContext,"toast",Toast.LENGTH_SHORT).show();

说实话,刚开始还觉得敲着挺带感,慢慢的就烦了,自己简单的封装一下是这样:

1
2
3
4
5
class ToastUtil{
public static void show(Context context,String text){
Toast.makeText(context,text,Toast.LENGTH_SHORT).show();
}
}

后来传context也传烦了,进一步封装,在Application里弄一个静态方法获取context,然后:

1
2
3
4
5
6
class ToastUtil{

public static void show(String text){
Toast.makeText(Application.getContext(),text,Toast.LENGTH_SHORT).show();
}
}

那么利用Kotlin的扩展,可以怎么做呢?

1
2
3
fun Context.toast(text: String){
Toast.makeText(this,text,Toast.LENGTH_SHORT).show()
}

这为Context类扩展了一个toast方法,这样以后你可以这样使用:

1
context.toast("hello")

如果你已经在上下文中:

1
toast("hello")

这可以说是十分的好用了,有了扩展,还要什么工(zi)具(xing)类(che)。扩展的语法比较简单,不多赘述。另外值得注意的是扩展并非继承,没办法复写类中的函数,用以下例子来说明一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Extend {
fun hello() {
println("hello")
}
}

fun Extend.hello(s: String) {
println("$s hello")
}

fun Extend.hello() {
println("无参 扩展 hello")
}

fun main(args: Array<String>) {
val e = Extend()
e.hello()
e.hello("luo")
}
// 输出:
// hello
// luo hello

可以看到当参数和方法名一样时,调用的是类内部的方法并非扩展方法,当参数不一致时调用对应的方法。

高阶函数

高阶,这俩字一看就高端大气上档次,看一下文档的描述:A higher-order function is a function that takes functions as parameters, or returns a function。高阶函数一种能把函数作为参数,或者将函数作为返回值的函数。将函数作为参数,桥豆麻袋,这……这不是跟传递点击事件那玩意很像么!Java里写的是接口回调,Java8 可以用lambda,先不管Java内部是怎么实现lambda的,至少从形式上看是很像直接传了个函数进去的。那么,继续用这个点击作为实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SecondActivity {
fun clickEvent(click: () -> Unit){
println("click event start")
click()
}
}

fun main(args: Array<String>) {
val secondActivity = SecondActivity()
secondActivity.clickEvent({
println("do something about click event")
})
}
// 输出:
// click event start
// do something about click event

首先解释一下clickEvent函数,click: () 表示函数类型, -> Unit表示返回值为空,也就是clickEvent接收一个返回值为空的click()方法。在clickEvent方法内部调用了这个传入的方法,达到了传递事件的目的。当然,如果函数是作为最后一个参数,是可以写成这样的:

1
2
3
secondActivity.clickEvent() {
println("do something about click event")
}

如果没有其他参数,那么还能写成这样:

1
2
3
secondActivity.clickEvent {
println("do something about click event")
}

相比Java,黑科技的味道慢慢的就出来了。光看这些可能你会觉得没啥,那么接下来的骚操作一定会让你对Kotlin的感觉提升一截。我也是在猫哥给我指导了一番之后,十分惊讶,还有这种操作?

令人窒息的操作

利用高阶函数可以接受函数参数这一点,能玩的东西可就多了去了,接下来就写几个好玩的:

debug模式配置

1
2
3
4
5
6
7
8
9
inline fun debugConf(code: () -> Unit) {
if (BuildConfig.DEBUG) {
code()
}
}

debugConf {
// init
}

这只是一个比较简单的应用场景,类似的还有Android中支持版本代码的编写,每一次都需要判断版本然后调用api,无疑是十分麻烦的,我们也可以利用高阶函数的特性来搞定。具体的代码就不再编写了。接下来举一个利用扩展和高阶函数特性编写的一个判断集合元素是否满足条件的函数。我们利用扩展给List扩展一个叫做myAll的方法,List调用此方法判断集合中的所有元素是否满足条件,如果所有元素都满足,返回true,反之false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline fun <T> List<T>.myAll(getItem: (T) -> Boolean): Boolean {
for (element in this) {
if (!getItem(element)) {
return false
}
}
return true
}

fun main(args: Array<String>) {
val list = listOf(1, 2, 3, 4, 5, 6)
println("myAll:" + list.myAll { it % 1 == 0 })
println("myAll: " + list.myAll { it % 2 == 0 })

}
// 输出:
// myAll:true
// myAll: false

简单的解释一下,inline是内联函数,在编译时编译器将函数体嵌入在每一个调用处。先看最外层,一个泛型T,用来表示集合内元素类型。myAll方法返回Boolean类型,用来表示所有元素是否符合条件。而getItem方法接收类型为T的参数(List元素类型),返回一个Boolean值表示该元素是否符合条件。判断也是简单粗暴的,遍历List如果有getItem()的返回值是false,那么就返回false。

小结

我看Kotlin也就是断断续续的看了一些语法,总体看下来感觉Java开发者学习Kotlin的成本不是非常高,而且最重要的是,Kotlin兼容Java,迁移成本也非常的低。Kotlin中的很多特性,只是提供了一种语言层面上的支持,Java也不是不能实现,总得拐弯抹角,麻烦的一批。Java语法啰嗦受人诟病也不是一天两天了,不过Java发展到现在更像是一个平台而不是语言了,Java能做的事太多,语法已经不是最值得关注的事情了。不过如果有一个兼容Java而且还喂你各种语法糖的语言出现,为什么不尝试一下呢?如果我说成这样你还不尝试一下,那我只能……

强行给你安利一波了,大爷,试一下Kotlin呗~

参考资料