Android开发者快速上手Kotlin(三) 之 高阶函数和SAM转换

《Android开发者快速上手Kotlin(二) 之 面向对象编程》文章继续。

6 高阶函数

Kotlin中的高阶函数其实就是跟高等数学中的高阶函数一个概念,就是函数套函数,即f(g(x))。什么意思呢?其实很好理解,就是将函数本身看作一种类型,作为别的函数的参数传入或者作为返回值返回。我们在前面其实就已经接触过高阶函数,因为:arg.forEach(::println)中,forEach本身就是一个高阶函数,它接收的参数是:action: (T) -> Unit。来通过自定义是如何实现:

// 该函数返回一个函数
fun a(): (y: Int, z: Int) -> Int {
    return { a: Int, b: Int -> a + b }
}

// 该函数需要传入第二个参数是一个函数
fun b(int: Int, function: (string:String) -> Unit) {
    function(int.toString())
}
// 调用
var aFun = a()                     // 返回一个函数
var result = aFun(2, 3)           // 执行了返回的函数
b(result, ::println)              // 输入println函数作为参数

说明

  1. a函数不接收参数,但返回了一个Lambda表达式: (y: Int, z: Int) -> Int。
  2. b函数接收一个Int和一个Lambda表达式:(string:String) -> Unit。
  3. 函数的引用使用两个冒号::,如果是类对象方法,就是obj::fun()。

6.1 内联函数 inline

当我们使用高阶函数时,传入或返回的Lambda表达式实际上它会被编译成匿名类,那么也意味着每调用都会创建一个新的对象,这会造成明显的性能开销。所以我们在使用高阶函数时一般都会在前面使用关键字inline来进行修饰,这代码是一个内联函数,也就是说编译器会在编译时把我们实现的真实代码替换到每一个函数调用中,而不是使用匿名类的方式。当然如果存在特殊情况,需要不内联,也可以使用oninline关键字。如:

inline fun a(noinline function1: () -> Unit, function2: () -> Unit) {
    function1()
    function2()
}

6.2 常用高阶函数

6.2.1 let、run、with、also和apply

let、run、with、also和apply这5个高阶函数作用起基本差不多,只是在使用上有一点点区别。它们都是作用域函数,当你需要去定义一个变量在一个特定的作用域范围内,这些函数的是一个不错的选择;而且它们另一个作用就是可以避免写一些判断null的操作。

class Persion(var name: String, var age: Int) {
    override fun toString(): String {
        return "{$name, $age}"
    }
}
// 调用
var persion1 = Persion("子云心", 30)
persion1.let {				// 返回表达式结果
    it.age = 15
    println("{${it.name}, ${it.age}}")  // 结果:{子云心, 15}
}
persion1.run {				// 直接能访问到类对象的let版本
    println("{${name}, ${age}}")        // 结果:{子云心, 15}
}
with(persion1) {			// 非扩展函数的run版本
    println("{${name}, ${age}}")        // 结果:{子云心, 15}
}

var persion2 = persion1.also {		// 同时返回新的对象的let版本
    it.age = 18
}
println(persion2)                       // 结果:{子云心, 18}

var persion3 = persion1.apply {	// 同时返回新的对象的run版本
    age = 22
}
println(persion1)                       // 结果:{子云心, 22}
println(persion2)                       // 结果:{子云心, 22}
println(persion3)                       // 结果:{子云心, 22}

6.2.2 use自动关闭资源

user高阶函数内部做了很多异常的处理和最后自动close释放资源,所以我们在使用上不需要自己去实现异常捕获和手动close,直接在大括号里使用最后执行的代码逻辑就可以了,也不怕内存泄漏。使用如:

File("build.gradle").inputStream().reader().buffered().use {
    println(it.readLines())
}

6.2.3 集合映射函数:filter、map、flatMap以及 asSequence

先来看看这三个函数的作用:

filter:     保留满足条件的元素

map:         集合中的所有元素一一映射到其他元素构成新集合

flatMap:  集合中的所有元素一一映身到新集合并合并这些集合得到新集合

它们的使用如:

val list1: List<Int> = listOf(1, 2, 3, 4)

val list2 = list1.filter { it % 2 == 0 }
println(list2)                          // 输出结果:[2,4]

val list3 = list1.map { it * 2 }
println(list3)                          // 输出结果:[2, 4, 6, 8]

val list4 = list1.flatMap { 0 until it }
println(list4)                          // 输出结果:[0, 0, 1, 0, 1, 2, 0, 1, 2, 3]

asSequence:转换为懒序列

在集合后加上asSequence后,集合变成为懒序列,只有等到真正需要(调用forEach)时才会被执行,否则它就是一个公式不会被执行。下面来对比一下加了asSequence和没有加asSequence集合的输出结果:

val list: List<Int> = listOf(1, 2, 3, 4)

list.asSequence()
    .filter {
        print("filter:$it,")
        it % 2 == 0
    }.map {
        print("map:$it,")
        it * 2
    }.forEach {
        print("forEach:$it,")
    }
// 输出结果:filter:1,filter:2,map:2,forEach:4,filter:3,filter:4,map:4,forEach:8,

list.filter {
        print("filter:$it,")
        it % 2 == 0
    }.map {
        print("map:$it,")
        it * 2
    }.forEach {
        print("forEach:$it,")
    }
// 输出结果:filter:1,filter:2,filter:3,filter:4,map:2,map:4,forEach:4,forEach:8,

6.2.4 集合的聚合函数:sum、reduce和fold

先来看看这三个函数的作用:

sum:      所有元素求和

reduce:    将元素依次按规则聚合,结果与元素类型一致

fold:          给定初始化值版本的reduce

它们的使用如:

val list1: List<Int> = listOf(1, 2, 3, 4)

val list2 = list1.sum()
println(list2)              // 输出结果:10

val list3 = list1.reduce { acc, i -> acc + i }
println(list3)              // 输出结果:10

val list4 = list1.fold("Hello") { acc, i -> acc + i }
println(list4)              // 输出结果:Hello1234

7 SAM转换

SAM全称是Single Abstract Method,意思是单一抽象方法。Java8中开始对Lambda和SAM转换支持。在使用上为:一个参数类型为只有一个方法的接口的方法时,调用时可用Lambda表达式做转换作为参数。而Kotlin中的SAM转换其实跟Java中的使用差不多,但是得加多一个限制条件,那就是只支持Java接口的Java方法。什么意思?其实就是Kotlin只对调用Java代码支持SAM转换,KotlinKotlin接口方法时是不支持SAM转换的。这是因为语言设计者认为,Kotlin本来就原生支持函数类型,根本没有必要再进行单一接口方法的定义。看看看如何使用:

// Java代码
public interface OnClickListener {
    void onClick();
}
// Kotlin调用代码
var onClickListener1 = object : OnClickListener {       // 匿名类的调用方式
    override fun onClick() {
        println("Hello World!")
    }
}
var onClickListener2 = OnClickListener {                // SAM转换调用方式
    println("Hello World!")
}

上述代码,如果要将onClickListener1和onClickListener2两个变量都传递给一个Java方法,可以这样: 

// Java代码
public class ListenerManager {
    List<OnClickListener> listenerList = new ArrayList();

    public void addListener(OnClickListener listener) {
        listenerList.add(listener);
    }

    public void removeListener(OnClickListener listener) {
        listenerList.remove(listener);
    }
}
// Kotlin调用代码
val listenerManager = ListenerManager()
listenerManager.addListener(onClickListener1)
listenerManager.addListener(onClickListener2)

补充: 

ListenerManager的addListener方法因为是Java代码,其实我们在Kotlin中调用时,也可以使用函数类型作为参数传入,如:

var onClickListener3 = {                       // Kotlin中的函数类型
    println("Hello World!")
}
listenerManager.addListener(onClickListener3)

注意:

如果只是一次性调用,上述代码是没有问题的。但是像我们在Android开发中常用的addListener和removeListener中,这样的调用可能就会踩到坑了。因为函数类型在编译时针对每次调用的地方都会new出一个新的匿名类,所以添加一时爽,移除真踩坑:

val listenerManager = ListenerManager()
listenerManager.addListener(onClickListener1)
listenerManager.addListener(onClickListener2)
listenerManager.addListener(onClickListener3)

listenerManager.removeListener(onClickListener1)        // 可以移除
listenerManager.removeListener(onClickListener2)        // 可以移除
listenerManager.removeListener(onClickListener3)        // 不可以移除,因为跟add时不是同一个对象

 

未完,请关注后面文章更新

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值