【Kotlin回顾】5.高阶函数与Lambda

语言: CN / TW / HK

theme: cyanosis


开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情


Kotlin中函数都是头等的,这意味着它可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以向操作任何其他非函数值一样操作函数。

为促成这点,作为一门静态类型编程语言的Kotlin使用一系列函数类型来表示函数并提供了一组专门的语言结构,例如Lambda表达式

这段话来自Kotlin文档。从没有接触过Kotlin的话这段话的意思很难理解,上面有三个关键词【高阶函数】、【函数类型】、【Lambda表达式】先分析这三个关键词然后再对上面这段话进行理解。

1.函数类型

函数类型就是函数的类型, 变量有类型IntString等,那函数的类型到底是指什么?

fun lastElement(str: String): Char { return str[str.length - 1] }

上面的代码是之前用过的,意思是获取字符串的最后一个字符,其中参数类型是String,返回值类型是Char,将其抽出来就是【String -> Char】这就代表了函数的类型,一句话概括就是:将函数的【参数类型】和【返回值类型】抽象出来就得到了函数的【函数类型】。【(String) -> Char】的意思就是参数类型是【String】,返回值类型是【Char】的函数类型。这个比较好理解。类似的还有Kotlin中继承自BaseAdapter的几个方法

``` //函数类型:() -> Int override fun getCount(): Int {

}

//函数类型:(Int) -> Any override fun getItem(position: Int): Any {

}

//函数类型:(Int) -> Long override fun getItemId(position: Int): Long {

} ```

2.高阶函数

理解饿了函数类型再来看下高阶函数。

高阶函数是将函数用作参数或返回值的函数, 这是高阶函数的定义。

  • 函数用作参数的高阶函数写法

``` fun main() { val result = answer(20) { 10 } println("result:$result") //输出结果:result:30 }

/* * 高阶函数 * 函数类型:(Int, add方法) -> Int / fun answer(num: Int, add: () -> Int): Int { return num + add() } ```

上面的代码用的是高阶函数中的函数用作参数的写法,定义一个answer方法,添加一个参数num和函数add,因为add方法返回值是一个Int类型因此可以跟num直接相加并返回结果,代码没有实际意义就是个例子

这里可能会产生一个疑问:为什么result的调用方式是成立的?

将上面的代码转换成Java的写法就清楚了

``` public final class HighFunctionKt { public static final void main() { int result = answer(20, (Function0)null.INSTANCE); String var1 = "result:" + result; boolean var2 = false; System.out.println(var1); }

// $FF: synthetic method public static void main(String[] var0) { main(); }

public static final int answer(int num, @NotNull Function0 add) { Intrinsics.checkNotNullParameter(add, "add"); return num + ((Number)add.invoke()).intValue(); } } ```

可以看到add方法被转换成了Function0,并且num与add方法的返回值进行了相加,那么这里有一个新的疑问invoke是什么?invokeFunction0的一个方法,作用就是调用函数,在Functions.kt类中,Function的数量达到Function21,从Function0Function21的区别就是传参数量的不同。

public interface Function0<out R> : Function<R> { /** Invokes the function. */ public operator fun invoke(): R }

再来看一个稍微复杂的高阶函数例子,answer函数中又添加了一个参数,同时函数参数也支持传参

``` fun main() { val add = answer(10, 20) { num1, num2 -> num1 + num2 } val minus = answer(10, 20) { num1, num2 -> num1 - num2 } println("add:$add") //输出结果:result:30 println("minus:$minus") //输出结果:result:30 }

fun answer(num1: Int, num2: Int, result: (Int, Int) -> Int): Int { return result(num1, num2) } ```

answer方法做了改动,传了两个参数,函数类型的参数也传入了两个参数,这样定义的作用是更灵活

  • 函数用作返回值的高阶函数写法

``` fun main() { println(answer(10).invoke()) //输出结果:输入的数:10 }

fun answer(num: Int): () -> String { return { "输入的数:${num}" } } ```

这里的invoke就是一个调用函数的功能。编译成Java代码如下所示:

``` public final class HighFunctionKt { public static final void main() { Object var0 = answer(10).invoke(); boolean var1 = false; System.out.println(var0); }

// $FF: synthetic method
public static void main(String[] var0) {
    main();
}

@NotNull
public static final Function0 answer(final int num) {
    return (Function0)(new Function0() {
        // $FF: synthetic method
        // $FF: bridge method
        public Object invoke() {
            return this.invoke();
        }

        @NotNull
        public final String invoke() {
            return "输入的数:" + num;
        }
    });
}

} ```

  • 应用场景举例

Android开发过程中RecycleView是常用的一个组件,但是它本身不支持点击事件,现在,假设我们在Adapter中有两个点击事件,添加和删除,常用的写法会先定义一个接口,对该接口定义一个变量,然后定义一个方法,代码如下:

``` private lateinit var mOnItemAddClickListener: OnItemAddClickListener private lateinit var mOnItemDeleteClickListener: OnItemDeleteClickListener

interface OnItemAddClickListener { fun onItemAddClick(position: Int) }

interface OnItemDeleteClickListener { fun onItemDeleteClick(position: Int) }

fun setOnItemAddClickListener(onItemAddClickListener: OnItemAddClickListener) { mOnItemAddClickListener = onItemAddClickListener }

fun setOnItemDeleteClickListener(onItemDeleteClickListener: OnItemDeleteClickListener) { mOnItemDeleteClickListener = onItemDeleteClickListener }

holder.ivAdd.setOnClickListener { mOnItemAddClickListener.onItemAddClick(position) }

holder.ivDelete.setOnClickListener { mOnItemDeleteClickListener.onItemDeleteClick(position) }

adapter.setOnItemAddClickListener(object :DemoAdapter.OnItemAddClickListener{ override fun onItemAddClick(position: Int) { TODO("Not yet implemented") } }) adapter.setOnItemDeleteClickListener(object :DemoAdapter.OnItemDeleteClickListener{ override fun onItemDeleteClick(position: Int) { TODO("Not yet implemented") } }) ```

用高阶函数对其进行优化后的代码如下:

``` private lateinit var mOnItemAddClickListener: (Int) -> Unit private lateinit var mOnItemDeleteClickListener: (Int) -> Unit

fun setOnItemAddClickListener(listener: (Int) -> Unit) { mOnItemAddClickListener = listener }

fun setOnItemDeleteClickListener(listener: (Int) -> Unit) { mOnItemDeleteClickListener = listener }

holder.ivAdd.setOnClickListener { mOnItemAddClickListener.invoke(position) }

holder.ivDelete.setOnClickListener { mOnItemDeleteClickListener.invoke(position) }

adapter.setOnItemAddClickListener {

} adapter.setOnItemDeleteClickListener {

} ```

这两种写法的代码进行对比可以发现高阶函数的实现方式中没有定义接口,同时它代码量显著减少,代码也变得更加简洁。

3.系统标准高阶函数

系统的标准高阶函数来自Standard.kt,里面的方法也是比较常用的

  • run

public inline fun <R> run(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }

block是函数式参数的名称,传入的函数类型是()-> RR是泛型

这段代码的意思就是调用传入的函数并返回结果,return block()就是传入函数的调用。

怎么用?or有什么用?

``` fun main() { run { println(add(1, 2)) } }

fun add(num1: Int, num2: Int): Int { return num1 + num2 } ```

作用就是构建Lambda更方便

  • T.run

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

这个run更上面那个的区别在于它有一个接收者,有了这个接收者就可以使用上下文了,例如:

val num = 10 num.run { println("result:${this + 1}") } //输出结果:result:11

还有一种情况,例如要从某个对象中取出它的一些属性的值也可以通过T.run,同时由于可以将this省略因此代码就可以这么写:

``` class Person(val name:String, var age:Int)

val person = Person("张三", 19) person.run{ println("name:$name") println("age:$age") } ```

再举个例子,TextView利用T.run修改属性并赋值

holder.tvText.run { text = "自定义文本" setTextColor(context.getColor(R.color.color_000000)) textSize = 20F }

  • whith

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

with的使用方式与T.run类似,with在返回值上带了一个接收者,看下面代码

with(holder.tvText) { text = "自定义文本" setTextColor(context.getColor(R.color.color_000000)) textSize = 20F }

  • T.apply

public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }

这个函数的意思就是接收者传入了什么返回值就是什么,使用案例:

``` val person = Person("张三", 19) person.apply { println("name:$name") //输出结果:张三 println("age:$age") //输出结果:19 }.age = 10 //把张三的年龄修改我10岁

println("name:${person.name}") //输出结果:张三 println("age:${person.age}") //输出结果:10 ```

有什么用?

Android中会遇到根据状态修改Button样式然后还要响应点击事件的情况,常用写法就不在这里讲了,这里用T.apply实现:

button.apply { text = "提交" setTextColor(context.getColor(R.color.color_000000)) background = context.getDrawable(R.drawable.shape_solid_4dp_4e6cf5) }.setOnClickListener { //点击事件 }

这行代码是不是很简洁。

还有webview的使用

webView.apply { settings.javaScriptEnabled = true settings.useWideViewPort = true }.loadUrl("https://www.baidu.com/")

T.apply很灵活,具体问题具体分析就好。

  • T.also

public inline fun <T> T.also(block: (T) -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block(this) return this }

T.also的意思就是用传参调用指定函数并返回这个参数

button.also { it.text }

这里的button在调用also之后在它的里面只能用it也必须用it,这个it指的是button本身,而T.apply中是this指的是Button本身,并且这个this是可以被省略的。使用过程中的区别不大。

  • T.let

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

T.let的意思就是使用传参调用指定函数然后返回结果,代码如下

``` val person = Person("张三", 19) val str = "Hello Kotlin"

val result = person.let { it.name == "张三" } println("result:$result") //输出结果:result:true

println("length:${str.let { it.length }}") //输出结果:length:12 ```

在Person类中有一个name = 张三的实例,经过let判断后返回true;获取str字符串的长度得到最终结果12。

  • T.takeIf

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (predicate(this)) this else null }

T.takeIf的意思就是如果符合条件则返回传入的值,否则返回null

``` val person = Person("张三", 19)

val result = person.takeIf { it.name == "李四" } println("result:${result?.name}") //输出结果:result:null

val result = person.takeIf { it.name == "张三" } //条件成立返回person对象 ```

  • T.takeUnless

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? { contract { callsInPlace(predicate, InvocationKind.EXACTLY_ONCE) } return if (!predicate(this)) this else null }

T.takeUnlessT.takeIf正好相反,如果满足条件则返回null,不满足则返回正确的谓词

``` val person = Person("张三", 19)

val result = person.takeUnless { it.name == "李四" } println("result:${result?.name}") //输出结果:result:张三

val result = person.takeUnless { it.name == "张三" } println("result:${result?.name}") //输出结果:result:null ```

  • repeat

``` public inline fun repeat(times: Int, action: (Int) -> Unit) { contract { callsInPlace(action) }

for (index in 0 until times) {
    action(index)
}

} ```

action函数就是一个重复执行的函数,从0开始

``` repeat(2) { println("执行第:${it}次") }

//输出结果: //执行第:0次 //执行第:1次 ```

4.Lambda表达式

Lambda表达式在Java中已经用的比较多了,通过它可以简化代码和提高开发效率,所以我们可以把Lambda表达式理解为函数的简写

例如view的点击事件在Kotlin中调用时就是这样的:

``` fun setOnClickListener(l: ((View!) -> Unit)?){

}

//调用时可以这么写: button.setOnClickListener{

} ```

开头提出了一个结论:在Kotlin中函数是头等的,为什么这么说呢?

  • Kotlin的函数可以独立于类之外,这就是顶层函数
  • Kotlin的函数可以作为参数也可以作为函数,它被称为高阶函数和Lambda
  • Kotlin的函数可以向变量一样,这叫做函数引用