Kotlin学习(8)高阶函数:lambda作为参数和返回值

​ lambda是用来构建抽象的一个强有力的工具,他们并不局限于集合和标准库中的类。我们可以将lambda作为函数参数和返回值来创建高阶函数。高阶函数可以帮助我们简化代码,移除重复代码,以及很好的构建抽象。

8.1 声明高阶函数

高阶函数定义为:一个将另一个函数作为参数或者返回值的函数。在Kotlin中,函数可以用lambda或者函数引用以值的形式来表示。因此,高阶函数就是传递lambda或者函数引用作为参数,或者作为返回值的函数。例如,filter标准库函数是接受一个predicate函数作为参数,它就是一个高阶函数。

val list = listOf(1, 2, 3, 4)
list.filter { it > 2 }
8.1.1 函数类型

​ 为了定义一个接受lambda作为参数的函数,我们需要知道如何声明这个参数的类型。首先看一下,直接把lambda保存在一个变量中的情况。

val sum = { x: Int, y: Int -> x + y }
val action = { println("action performed") }

​ 这种情况下,编译器推断sum变量是有函数类型的。现在看一下如何显式的声明sum变量的类型

val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println("action performed") }

​ 声明函数类型时,需要将函数参数类型放在括号中,将返回值类型放在箭头后面(Int, Int) -> Int

  • 由于函数类型声明中指定了参数类型,所以lambda中的类型可以省略掉
  • 函数类型声明中,函数返回类型是需要显式声明的,所以返回Unit是不可以省略的
  • 函数类型声明中,函数的返回值类型可以是可空的val returnNull: (Int, Int) -> Int? = {_,_ -> null}
  • 当然也可以定义一个可空类型的变量的函数类型val funOrNull:((Int,Int) ->Int)? = null
8.1.2 调用函数作为参数传递

​ 现在已经知道了如何声明一个高阶函数,下面自己实现一个简单的高阶函数。这个函数会对2和3执行一个任意的操作,并打印操作结果:

val sum: (Int, Int) -> Int = { x, y -> x + y }
fun numOperation(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("the result is $result")
}
numOperation(sum)
>>the result is 5

调用作为参数传递函数和调用普通函数的语法相同:函数名称后面加上括号,并将参数传递到括号中。

8.1.3 在Java中使用函数类型

​ Java中使用函数类型,这其中的机制是函数类型被声明为常规的接口。根据参数数量的不同分为:Funtion0<R>(无参函数),Function1<P1,R>(一个参数的函数)等等。

​ Kotlin中,使用函数类型的函数在Java中调用是很容易的。Java8的lambda会自动转变为函数类型的值

//Kotlin中定义高阶函数
fun processAnswer(f: (Int) -> Int) {
    println(f(66))
}
//java中调用
HighFunc2Kt.processAnswer(number -> number + 1);

​ 在老版本的Java中,可以传递实现了对应函数接口invoke方法的匿名类的一个实例

HighFunc2Kt.processAnswer(new Function1<Integer, Integer>() {
    @Override
    public Integer invoke(Integer integer) {
        return integer * 20;
    }
})

​ 在Java中,使用Kotlin标准库中接受lambda作为参数的扩展函数也是很简单的。需要注意的是,你必须显式将接受者传递到第一个参数。

List<String> strings = new ArrayList<>();
strings.add("m1Ku");
CollectionsKt.forEach(strings, s -> {
    System.out.print(s);
    return Unit.INSTANCE; //显式的返回Unit
});
Java中,函数和lambda可以返回Unit,但是Unit类型在Kotlin中是有值的,所有你需要显式的返回它。你不能传递一个返回`void`的lambda的传给一个期望返回`Unit`的函数类型。
8.1.4 函数类型参数的默认值和空值

​ 当声明一个函数类型的参数时,我们也可以指定其默认值。下面来看一下原来定义过的joinToString函数,这里多加了一个参数可以传递一个lambda,用来指定集合中的元素转为String的方式。如果要求调用时都传递lambda是很麻烦的,这里用一个默认的转换就可以满足大部分要求。这里就可以为其指定一个lambda作为默认值。

fun <T> Collection<T>.jointToString(
        prefix: String = "{",
        separator: String = ",",
        postfix: String = "}",
        transform: (T) -> String = { it.toString() } //指定函数类型参数的默认值
): String {
    val stringBuilder = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) {
            stringBuilder.append(separator)
        }
        stringBuilder.append(transform(element))
    }
    stringBuilder.append(postfix)
    return stringBuilder.toString()
}
val strings = listOf("m1Ku", "alpha", "event")
println(strings.jointToString())
println(strings.jointToString { it.toUpperCase() })
>> {m1Ku,alpha,event}
   {M1KU,ALPHA,EVENT}

​ 函数类型的参数可以定义为可空的。但是传递的可空类型函数不能在函数中直接调用,因为这里有潜在的空指针异常,所以Kotlin会报编译错误,这里一种选择是显式的检查null:

fun foo(callBack: (() -> Unit)?) {
    if (callBack!=null){
        callBack()
    }
}

​ 这样写是有些繁琐的,但是想写的简洁一些也是完全可能的,这得益于:一个函数类型的变量是接口FunctionN的一个实现。Kotlin标准库定义了一系列的接口(Function0Function1,etc),代表拥有不同数量参数的函数。每一个人接口定义了一个叫invoke的方法(invoke方法包含lambda体),调用它就会执行函数。作为一个常规的方法,可以通过安全调用操作符调用invoke:callBack?.invoke()

8.1.5 从函数中返回函数

​ 从一个函数中返回另一个函数的需求并不如把函数作为参数传递的需求多,但这个也是很有用的。例如,程序中的一段代码逻辑会跟程序的状态或者其他情况而变,即有多种逻辑。举个栗子,我们可以根据选中的运费计算方法来计算运费。这里定义一个选择适当得计算逻辑的函数,并且将这段逻辑作为函数返回。

enum class Delivery { 
    STANDARD, EXPEDITED //定义枚举的运输类型
}
class Order(val goodCount: Int) //订单类
//传入运输类型,得到运输费的计算函数
fun getCostCaculator(delivery: Delivery): (Order) -> Double { 
    if (delivery == Delivery.STANDARD) {
        return { order -> order.goodCount * 10.4 + 10 }
    }
    return { order -> order.goodCount * 25.5 + 20 }
}

val calculator = getCostCalculator(Delivery.STANDARD)
println(calculator(Order(20)))
>>218.0
8.1.6通过lambdas移除重复代码

​ 函数类型和lambda表达式一起组成了一个很好的创建重用代码的工具。很多的代码重复都可以用lambda表达式来消灭了。看下面这个例子:

//定义操作系统
enum class OS { ANDROID, IOS, WINDOWS, MAC }
//定义浏览网站的人信息的类
data class SiteVisit(
        val path: String,
        val os: OS,
        val duration: Double
)
//定义信息集合类
val log = listOf(
            SiteVisit("/", OS.ANDROID, 20.2),
            SiteVisit("/user", OS.IOS, 35.3),
            SiteVisit("/vim", OS.ANDROID, 100.43)
    )

​ 假设我们要计算使用Android浏览网站的人的平均使用时间,可以这样写:

log.filter { it.os == OS.ANDROID }
        .map(SiteVisit::duration)
        .average()
当然我们可以抽取平台类型作为函数的参数,这样可以适用于不同的平台类型求平均使用时间。
fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }
                .map(SiteVisit::duration)
                .average()

​ 但是当如果我们想求出使用安卓和IOS两个移动平台的人平均使用时间时,这样定义函数就不满足我们的需求了。此时,我们可以使用函数类型将使用条件抽取到一个参数中。

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
        filter(predicate)
                .map(SiteVisit::duration)
                .average()

8.2 内联函数:消除lambda带来的运行时开销

​ 在学习lambda表达式时,我们已经知道lambda正常会被编译成匿名类。这就意味着每次使用lambda表达式时,一个额外的类都会被创建;并且如果lambda捕获了一些变量,每一次调用lambda时都会创建一个新的对象。这就带来了运行时开销,意味着执行相同的代码使用lambda比函数效率更低。

​ 这里贪心的问一句:是否可以让编译器生成和Java一样高效的代码,但同时有可以把重复逻辑抽取到标准库中呢?答案是可以的。将函数以inline关键字修饰,当函数使用时,编译器不会生成函数调用代码,而是以真实的实现函数的代码代替函数调用代码。一脸懵逼,还是试试下面的例子理解吧。

8.2.1 内联函数工作原理

​ 当函数被声明为inline时,其函数体是内联的,换句话说就是,函数体会被直接替换到函数被调用的地方,而不是正常的调用,下面声明一个内联函数:

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

​ 在函数中调用内联函数

fun foo(l: Lock) {
    println("before sync")
    synchronized(l) {
        println("Action")
    }
    println("After sync")
}

​ 上述调用与下面的代码编译成的字节码是相同的

fun _foo(l: Lock) {
    println("before sync")
  //即这里对synchronized调用时,直接用synchronized的函数体做了替换
    try {
        println("Action")
    } finally {
        l.unlock()
    }
    println("After sync")
}
8.2.2 内联集合操作

​ Kotlin标准库中大部分的集合操作函数都带有lambda参数。但是这里不用担心性能问题,由于Kotlin对内联函数的支持,类似filter这种集合操作函数都会标记为是内联的。

persons.filter { it.age > 18 }
        .map(Person::name)

上面这个例子中,filtermap都被声明为inline的,函数体都是内联的,所以不会产生额外的类或者对象。但是会产生一个集合保存列表过滤的结果,当有集合有大量元素时,可以在集合后加上asSequence,用序列代替集合。对于小的集合可以使用普通的集合操作处理。

8.2.3 何时应该声明函数是内联的?

​ 只有那些接受lambda作为参数的函数,使用inline关键字标记为内联函数时才会提升性能。对于常规的函数调用,Java虚拟机已经提供了强大的内联支持。它会分析代码的执行,并将调用内联,这些过程发生在机器码层。在字节码上,每个方法的实现只会重复一次,并不会因为不同地方方法的调用,而多次拷贝代码。另外如果直接调用函数,堆栈信息也会更加清晰。

​ 另一方面,接受lambda参数的内联函数是很有效的。第一,使用内联避免的运行时开销是很显著的,节省的不止是每次调用,而且还有为lambda创建的额外的来,以及lambda实例的对象。第二,JVM虚拟机并不能智能总是执行内联操作。最后,内联可以让你使用普通lambda不能使用的特性,比如non-local返回。

​ 但是决定使用内联操作符时也要注意代码的体积,当想内联的函数的体积很大时,就应该避免将其定义为内联函数。或者是尽可能抽取和lambda参数的无关的代码到一个非内联函数中。

8.2.4 使用内联lambda管理资源

​ Lambda能够去除重复代码的一个常见模式是资源管理:在某个操作前获取资源,并在结束后释放这个资源。这里的资源可以是一个文件,一个锁,一个数据库事务等等。实现这种模式的标准做法就是使用一个try/finally语句,在try代码块之前获取资源,并在finally块中释放资源。

​ 前面我们看到可以将try/finally的逻辑抽取到一个函数中,并将使用资源的代码作为lambda传递给函数。正如前面的我们定义的synchronized(lock: Lock, action: () -> T): T 函数,后面传入的函数类型参数就是需要加锁执行的代码,而这里的锁就是前面所说的资源。Kotlin标准库定义了一个withLock函数,他是Lock接口的扩展函数,它提供了实现相同功能的更符合语言习惯的API。

val l: Lock = ...
l.withLock {
//加锁情况下执行指定的代码
}

​ Kotlin库中withLock函数的定义:

public inline fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

8.3 高阶函数中的控制流

​ 当开始使用lambda代替像循环这种命令式代码结构时,你很快就会遇到return表达式的问题。在循环中使用return表达式是很简单的。但是当把循环替换为使用像filter这样的函数时,return应该怎么用呢?

8.3.1 lambda中的返回语句:从一个封闭的函数中返回

​ 在lambda中使用return语句是可以正常返回的,但是他是从调用lambda的函数中返回,而不是仅仅是从lambda中返回,这叫非局部返回,因为它是从一个比包含return的代码块更大的一个代码块返回了。

fun findPerson(persons: List<Person>) {
    persons.forEach {
        if (it.age > 18){
            println("找到一个年龄大于18的人${it.name}")
            return
        }
    }
    println("没有找到年龄大于18的人")
}
findPerson(persons)
>>找到一个年龄大于18的人m1Ku
8.3.2 从lambda返回:使用标签返回

​ 在lambda中也是可以实现局部返回的,局部返回类似于for循环的break语句。即结束lambda中代码的执行,继续执行调用lambda的函数的函数体中的代码。要区分局部返回和非局部返回,要使用标签。想从一个lambda表达式返回,可以标记它,然后再return关键字后面引用这个标签。

​ 想要标记一个局部返回的lambda,需要在lambda的花括号前写上标签名(可以任意起的)并在标签名后面加@符号,并在返回语句时引用这个标签,例如:

fun findPerson(persons: List<Person>) {
    persons.forEach label@ { //标记一个叫label的标签
        if (it.age > 18) {
            println("找到一个年龄大于18的人${it.name}")
            return@label //引用这个标签并且返回
        }
    }
    println("没有找到年龄大于18的人")
}
//此时,程序始终会输出>>没有找到年龄大于18的人

​ 另一种选择是,将使用lambda作为参数的函数的函数名作为标签。

persons.forEach label@ {
    if (it.age > 18) {
        println("找到一个年龄大于18的人${it.name}")
        return@forEach
    }
}

如果显式的给定了标签的名字,就不能再使用函数的名字作为标签了。另外,lambda表达式中只能有一个标签。

8.3.3 匿名函数:默认局部返回

​ 另一种给函数传递代码片段的方式是使用匿名函数。

​ 这里给forEach传递一个匿名函数代替lambda,匿名函数的函数名和参数类型可以省略。

fun findM1Ku(persons: List<Person>) {
    persons.forEach(fun(person) {
        if (person.name == "m1Ku") return
        println("person ${person.name} is not m1Ku")
    })
}

​ 匿名函数声明返回值类型的规则和常规函数相同,有函数体的函数需要显式的声明返回值的类型。表达式体的函数可以省略掉返回值类型。

persons.filter(fun(person): Boolean {
    return person.age > 18
})  

​ 匿名函数的return是从匿名函数局部返回,而不是包围它的函数。这里的规则很简单:return会从最近的使用fun关键字声明的函数中返回。由于lambda表达式不使用fun关键字,所以lambda中的retun会从外层函数返回。

​ 需要注意的是:尽管匿名函数看起来和普通函数很像,但它只是lambda表达式另一个语法形式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容