Kotlin高阶函数

100 阅读7分钟

高阶函数分两个部分实现

1、用函数类型替代接口定义;

2、用 Lambda 表达式作为函数参数。

举例,view的点击事件

// View.java

// 成员变量
private OnClickListener mOnClickListener;
private OnContextClickListener mOnContextClickListener;

// 监听手指点击事件
public void setOnClickListener(OnClickListener l) {
    mOnClickListener = l;
}

// 为传递这个点击事件,专门定义了一个接口
public interface OnClickListener {
    void onClick(View v);
}

// 监听鼠标点击事件
public void setOnContextClickListener(OnContextClickListener l) {
    getListenerInfo().mOnContextClickListener = l;
}

// 为传递这个鼠标点击事件,专门定义了一个接口
public interface OnContextClickListener {
    boolean onContextClick(View v);
}
//View.kt
//                     (View) -> Unit 就是「函数类型 」
//                       ↑        ↑ 
var mOnClickListener: ((View) -> Unit)? = null
var mOnContextClickListener: ((View) -> Unit)? = null

// 高阶函数
fun setOnClickListener(l: (View) -> Unit) {
    mOnClickListener = l;
}

// 高阶函数
fun setOnContextClickListener(l: (View) -> Unit) {
    mOnContextClickListener = l;
}

View.java & View.kt实现了等价的功能; 函数类型替代了原来的接口定义

调用方的差距

java

// 设置手指点击事件
image.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        gotoPreview();
    }
});

// 设置鼠标点击事件
image.setOnContextClickListener(new View.OnContextClickListener() {
    @Override
    public void onContextClick(View v) {
        gotoPreview();
    }
});

kt

//                      { gotoPreview() } 就是 Lambda
//                             ↑
image.setOnClickListener({ gotoPreview() })
image.setOnContextClickListener({ gotoPreview() })

Kotlin 中引入高阶函数会带来几个好处:一个是针对定义方,代码中减少了两个接口类的定义;另一个是对于调用方来说,代码也会更加简洁。

函数类型:

字面意思,函数类型(Function Type)就是函数的类型。

//         (Int,  Int) ->Float 这就是 add 函数的类型
//           ↑     ↑      ↑
fun add(a: Int, b: Int): Float { return (a+b).toFloat() }

注释里面的“↑”代表的是一种映射关系。其实,将第三行代码里的“Int Int Float”抽出来,就代表了该函数的类型。

将函数的“参数类型”和“返回值类型”抽象出来后,就得到了“函数类型”。(Int, Int) ->Float 就代表了参数类型是两个 Int,返回值类型为 Float 的函数类型。

函数的引用:

普通的变量也有引用的概念,我们可以将一个变量赋值给另一个变量。而这一点,在函数上也是同样适用的,函数也有引用,并且也可以赋值给变量。

// 函数赋值给变量                    函数引用
//    ↑                              ↑
val function: (Int, Int) -> Float = ::add

高阶函数:

标准定义:高阶函数是将函数用作参数或返回值的函数。

换句话说,一个函数的参数或是返回值,它们当中有一个是函数的情况下,这个函数就是高阶函数。

//                      函数作为参数的高阶函数
//                              ↓
fun setOnClickListener(l: (View) -> Unit) { ... }

Lambda

Kotlin 语言的设计者是用 Lambda 表达式作为函数参数的,那么这里的 Lambda,就可以理解为是函数的简写:

SAM 转换

SAM 是 Single Abstract Method 的缩写,意思就是只有一个抽象方法的类或者接口。但在 Kotlin 和 Java 8 里,SAM 代表着只有一个抽象方法的接口。只要是符合 SAM 要求的接口,编译器就能进行 SAM 转换,也就是我们可以使用 Lambda 表达式,来简写接口类的参数。

注意,Java 8 中的 SAM 有明确的名称,叫做函数式接口(FunctionalInterface)。FunctionalInterface 的限制如下,缺一不可:

必须是接口,抽象类不行;

该接口有且仅有一个抽象的方法,抽象方法个数必须是 1,默认实现的方法可以有多个。

也就是说,对于 View.java 来说,它虽然是 Java 代码,但 Kotlin 编译器知道它的参数 符合 SAM 转换的条件,所以会自动做转换。

Lambda 表达式引发的 8 种写法

当一个函数的参数是 SAM 的情况下,我们同样也可以使用 Lambda 作为参数。

所以,我们既可以用匿名内部类的方式传参,也可以使用 Lambda 的方式传参。

在以上两种写法的中间,还有 6 种“过渡状态”的写法。同样的代码,能有 8 种不同的写法。

第 1 种写法

这是原始代码,它的本质是用 object 关键字定义了一个匿名内部类:

image.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View?) {
        gotoPreview(v)
    }
})

第 2 种写法

在这种情况下,object 关键字可以被省略。这时候它在语法层面就不再是匿名内部类了,它更像是 Lambda 表达式了,因此它里面 override 的方法也要跟着删掉:

image.setOnClickListener(View.OnClickListener { v: View? ->
    gotoPreview(v)
})

第 3 种写法

由于 Kotlin 的 Lambda 表达式是不需要 SAM Constructor 的,所以它也可以被删掉:

image.setOnClickListener({ v: View? ->
    gotoPreview(v)
})

第 4 种写法

由于 Kotlin 支持类型推导,所以 View 可以被删掉:

image.setOnClickListener({ v ->
    gotoPreview(v)
})

第 5 种写法

当 Kotlin Lambda 表达式只有一个参数的时候,它可以被写成 it:

image.setOnClickListener({ it ->
    gotoPreview(it)
})

第 6 种写法

Kotlin Lambda 的 it 是可以被省略的:

image.setOnClickListener({
    gotoPreview(it)
})

第 7 种写法

当 Kotlin Lambda 作为函数的最后一个参数时,Lambda 可以被挪到外面:

image.setOnClickListener() {
    gotoPreview(it)
}

第 8 种写法

当 Kotlin 只有一个 Lambda 作为函数参数时,() 可以被省略:

image.setOnClickListener {
    gotoPreview(it)
}

小结:

将函数的参数类型和返回值类型抽象出来后,我们就得到了函数类型。比如(View) -> Unit 就代表了参数类型是 View,返回值类型为 Unit 的函数类型。

如果一个函数的“参数”或者“返回值”的类型是函数类型,那这个函数就是高阶函数。很明显,我们刚刚就写了一个高阶函数,只是它比较简单而已。

Lambda 就是函数的一种简写。

回顾下函数类型、高阶函数以及 Lambda 表达式三者之间的关系:

ecf85d05f6fdc5ea2d015a61d7e64771.webp

难点:带接收者的函数类型

举例,定一个apply()方法,带接收者的函数类型

//              带接收者的函数类型
//                     ↓  
fun User.apply(block: User.() -> Unit): User{
//  不用再传this
//       ↓ 
    block()
    return this
}

user?.apply { this: User ->
//               this 可以省略
//                   ↓
    username.text = this.name
    website.text = this.blog
    image.setOnClickListener { gotoImagePreviewActivity(this) }
}

标准函数 apply 的使用场景

不用 apply:

if (user != null) {
    ...
    username.text = user.name
    website.text = user.blog
    image.setOnClickListener { gotoImagePreviewActivity(user) }
}

使用 apply:

user?.apply {
    ...
    username.text = name
    website.text = blog
    image.setOnClickListener { gotoImagePreviewActivity(this) }
}

如果不引入带接收者的函数类型,长这样:

//           改为this             改为this
//               ↓                    ↓ 
fun User.apply(this: User, block: (this: User) -> Unit): User{
//    这里还要传参数
//         ↓ 
    block(this)
    return this
}

user?.apply(this = user) { this: User ->
    ...
//               this 可以省略
//                   ↓ 
    username.text = this.name
    website.text = blog
    image.setOnClickListener { gotoImagePreviewActivity(this) }
}

apply 实现会比较繁琐:

需要我们传入 this:user?.apply(this = user)。

需要我们自己调用:block(this)。

因此,Kotlin 就引入了带接收者的函数类型,可以简化 apply 的定义:

//              带接收者的函数类型
//                     ↓  
fun User.apply(block: User.() -> Unit): User{
//  不用再传this
//       ↓ 
    block()
    return this
}

user?.apply { this: User ->
//               this 可以省略
//                   ↓
    username.text = this.name
    website.text = this.blog
    image.setOnClickListener { gotoImagePreviewActivity(this) }
}

带接收者的函数类型,就等价于成员方法。

class User() {
    val name: String = ""
    val blog: String = ""

    fun apply() {
        // 成员方法可以通过 this 访问成员变量
        username.text = this.name
        website.text = this.blog
        image.setOnClickListener { gotoImagePreviewActivity(this) }
    }
}

但从本质上讲,它仍是通过编译器注入 this 来实现的。

图解如下:

image.png

带接收者的函数类型,也能代表扩展函数呢。从语法层面讲,扩展函数就相当于成员函数。

改进 单例的抽象类模板

改进前BaseSingleton 的代码:

                     
abstract class BaseSingleton<in P, out T> {
    @Volatile
    private var instance: T? = null
    //                       ①
    //                       ↓
    protected abstract fun creator(param: P): T

    fun getInstance(param: P): T =
        instance ?: synchronized(this) {
            instance ?: creator(param).also { instance = it }
    }
}

class PersonManager private constructor(name: String) {
    companion object : BaseSingleton<String, PersonManager>() {
        override fun creator(param: String): PersonManager = PersonManager(param)
    }
}

BaseSingleton 是单例抽象模板,而 PersonManager 则是实际的单例类。

改进点:抽象方法creator;creator应该定义成属性。

途径:通过 高阶函数 将一个方法改成属性

将 creator 改成一个类型为:(P)->T的属性

改进后BaseSingleton 的代码:

abstract class BaseSingleton<in P, out T> {
    @Volatile
    private var instance: T? = null
    //               变化在这里,函数类型的属性
    //                  ↓              ↓
    protected abstract val creator: (P)-> T

    fun getInstance(param: P): T =
        instance ?: synchronized(this) {
            instance ?: creator(param).also { instance = it }
    }
}

将 creator 改成了一个抽象的属性,如果其他的单例类继承了 BaseSingleton 这个类,就必须实现这个 creator 属性。不过,PersonManager 该怎么写呢?

通过 函数引用 正确实现 creator 这个函数类型的属性

class PersonManager private constructor(name: String) {
    companion object : BaseSingleton<String, PersonManager>() {
    //                             函数引用
    //                                ↓ 
        override val creator = ::PersonManager
    }
}

直接将 PersonManager 的构造函数,以函数引用的方式传给了 creator 这个属性,这样就成功地实现了这个函数类型的属性。

前后对比,代码中的关键元素只是换了一种语法排列规则:从函数的语法变成了属性的语法,语法从复杂变得简洁,其中的关键元素并未丢失。两种代码是完全等价的,但后者更加简洁易懂