Kotlin 知识梳理(10) - 高阶函数:Lambda 作为形参或返回值

Kotlin 知识梳理系列文章

Kotlin 知识梳理(1) - Kotlin 基础
Kotlin 知识梳理(2) - 函数的定义与调用
Kotlin 知识梳理(3) - 类、对象和接口
Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字
Kotlin 知识梳理(5) - lambda 表达式和成员引用
Kotlin 知识梳理(6) - Kotlin 的可空性
Kotlin 知识梳理(7) - Kotlin 的类型系统
Kotlin 知识梳理(8) - 运算符重载及其他约定
Kotlin 知识梳理(9) - 委托属性
Kotlin 知识梳理(10) - 高阶函数:Lambda 作为形参或返回值
Kotlin 知识梳理(11) - 内联函数
Kotlin 知识梳理(12) - 泛型类型参数


一、本文概要

本文是对<<Kotlin in Action>>的学习笔记,如果需要运行相应的代码可以访问在线环境 try.kotlinlang.org,这部分的思维导图为:


Kotlin 知识梳理(5) - lambda 表达式和成员引用 中我们初步认识了lambda,这一章我们将学到如何创建 高阶函数:使用lambda作为 参数或者返回值 的函数。高阶函数有助于简化代码,去除代码重复,以及构建漂亮的抽象概念。

二、声明高阶函数

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

list.filter { x > 0 }

2.1 函数类型

为了声明一个以lambda作为实参的函数,你需要知道如何声明 对应形参的类型。下面我们先看一个简单的例子,把lambda表达式保存在局部变量当中:

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

在上面的例子中,我们省去了类型的声明。但是编译器可以推导出sumaction这两个 变量具有函数类型,这些变量的显示声明为:

//有两个 Int 型参数和 Int 型返回值的函数
val sum : (Int, Int) -> Int = {x, y -> x + y}
//没有参数和返回值的函数
val action : () -> Unit = { println(42) }

声明函数类型,需要 将函数参数类型放在括号中,紧接着是一个箭头和函数的返回类型

(Int, String) -> Unit

Unit类型用于表示函数不返回任何有用的值,在声明一个普通的函数时,Unit类型的返回值是可以忽略的,但是一个 函数类型声明总是需要一个显示的返回类型,所以在这种场景下Unit是不能省略的。

{x, y -> x + y}中,因为它们的类型已经在函数类型的变量声明部分指定了,不需要在lambda当中重复声明。

就像其它方法一样,函数类型的返回值也可以标记为可空类型:

var canReturnNull : (Int, Int) -> Int? = { null }

也可以定义一个 函数类型的可空变量,为了明确表示 变量本身可空,而不是函数类型的返回类型可空,你需要 将整个函数类型的定义包含在括号内并在括号后添加一个问号

var funOrNull : ((Int, Int) -> Int)? = null

函数类型的参数名

可以为函数类型声明中的参数指定名字:

//函数类型的参数现在有了名字...
fun performRequest(url : String, callback : (code : Int, content : String) -> Unit) {
    //....
}

调用方法为:

>> val url = "http://kotl.in"
//可以使用 API 中提供的参数名字作为 lambda 参数的名字....
>> performRequest(url) { code, content -> / *...* / }
>> performRequest(url) { code, page -> / *...* / }

参数名称不会影响类型的匹配,当你声明一个lambda时,不必使用和函数类型声明中一模一样的参数名称,但命名会提升代码的可读性并且能用于IDE的代码补全。

2.2 调用作为参数的函数

下面我们讨论如何实现一个高阶函数,这个例子会尽量简单并且使用之前的lambda sum同样的声明,这个函数实现对于两个整数的任意操作,然后打印出结果:

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

fun main(args: Array<String>) {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}

运行结果为:

>> The result is 5
>> The result is 6

调用作为参数的函数operation和调用普通函数的语法是一样:把括号放在函数名后,并把参数放在括号内。下面,让我们实现一个标准的库函数:filter函数。它会过滤掉字符串中不属于a..z范围内的字母。

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}

filter函数以一个判断式作为参数,判断式的类型是一个函数,以字符串作为参数并返回boolean类型的值。

fun main(args: Array<String>) {
    println("ab1c".filter { it in 'a'..'z' })
}

运行结果:

>> abc

2.3 在 Java 中使用函数

背后的原理

背后的原理是:

  • 函数类型被声明为普通的接口:一个函数类型的变量是FunctionN接口的一个实现。Kotlin标准库定义了一系列的接口:Function0<R>表示没有参数的函数,Function1<P1, R>表示一个参数的函数。
  • 一个函数类型的变量就是实现了对应的Function接口的实现类的实例,每个接口定义了一个invoke方法,实现类的invoke方法包含了lambda函数体,调用这个方法就会执行函数。

Java中可以很简单地调用使用了函数类型的Kotlin函数,Java 8lambda会被自动转换为函数类型的值:

//Kotlin 声明
fun processTheAnswer(f : (Int) -> Int) {
    println(f(42))
}
//Java
processTheAnswer(number -> number + 1)

在旧版的Java中,可以传递一个实现了函数接口中的invoke方法的匿名内部类的实例:

>> processTheAnswer(
    new Function1<Integer, Integer>() {
        @override
        public Integer invoke(Integer number) {
            System.out.println(number);
            return number + 1;
        }
    }
)

Java中可以很容易地使用Kotlin标准库中以lambda作为参数的扩展函数,但是必须要 显示地传递一个接收者作为第一个参数

List<String> strings = new ArrayList();
strings.add("42");
CollectionsKt.forEach(strings, s -> {
    System.out.println(s);
    retrun Unit.INSTANCE;
});

Java中,函数或者lambda可以返回Unit。但因为在KotlinUnit类型是有一个值的,所以需要显示地返回它。一个返回voidlambda不能作为返回Unit的函数类型的实参,就像之前的例子中的(String) -> Unit

2.4 函数类型的参数默认值和 null 值

2.4.1 函数类型的参数默认值

joinToString函数为例,我们除了可以定义前缀、后缀和分隔符以外,还可以通过最后一个 函数类型的参数 指定如何将集合当中的每个元素转换成为String,这是一个泛型函数:它有一个类型参数T表示集合中的元素的类型,Lambda transform将接收这个类型的参数,下面我们来看一下如何为它指定一个lambda作为默认值:

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        //为函数类型的参数提供默认值。
        transform: (T) -> String = { it.toString() }
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        //调用传入的函数。
        result.append(transform(element))
    }

    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ",
           transform = { it.toUpperCase() }))
}

运行结果为:

>> Alpha, Beta
>> alpha, beta
>> ALPHA! BETA! 

2.4.2 声明一个参数可为空的函数类型

当声明一个参数为可空的函数类型时,不能直接调用作为参数传递进来的函数:Kotlin会因为检测到潜在的空指针而导致编译失败,在这种情况下有两种处理方式:

  • 显示地检查 null
    显示地检查null是一种比较容易理解的方法:
fun foo(callback : (() _ Unit)?) {
    if (callback != null) {
        callback()
    }
}
  • 通过安全调用语法调用
    除此之外,因为函数类型是一个包含invoke方法的接口的具体实现,作为一个普通方法,invoke可以通过安全调用语法调用:
callback?.invoke() ?: /* 默认实现 */

2.5 返回函数的函数

从函数中返回另一个函数适用于下面的场景:程序中的一段逻辑可能会因为程序的状态或者其他条件而产生变化,比如说下面的例子,运输费用的计算依赖于选择的运输方式:

//声明一个枚举类型。
enum class Delivery { STANDARD, EXPIRED }

class Order(val itemCount : Int)

//返回的函数类型为:形参为 Order 类,返回类型为 Double。
fun getShippingCalculator(delivery : Delivery) : (Order) -> Double {
    if (delivery == Delivery.EXPIRED) {
        return { order -> 6 + 2.1 * order.itemCount }
    }
    return { order -> 1.2 * order.itemCount }
}

fun main(args: Array<String>) {
    val calculator = getShippingCalculator(Delivery.EXPIRED)
    println("cost ${calculator(Order(3))}")
}

在上面的例子中,getShippingCalculator返回了一个函数,这个函数以Order作为参数并返回一个Double类型的值,要返回一个函数,需要写一个return表达式,跟上一个lambda、一个成员引用,或者其他的函数类型的表达式。

下面,我们来看一个过滤器的例子:

data class Person(val firstName : String, val phoneNumber : String?)

class ContactListFilter {
    var prefix : String = ""
    var onlyWithPhoneNumber : Boolean = false
    
    fun getPredicate() : (Person) -> Boolean {
        val startWithPrefix = { p : Person ->
            p.firstName.startsWith(prefix)
        }
        if (!onlyWithPhoneNumber) {
            return startWithPrefix
        }
        return { startWithPrefix(it) && it.phoneNumber != null }
    }
}

fun main(args: Array<String>) {
    val contacts = listOf(Person("Dmitry", "123-4567"),
                         Person("Svelana", null))
    val contactListFilters = ContactListFilter()
    contactListFilters.prefix = "S"
    contactListFilters.onlyWithPhoneNumber = false
    println(contacts.filter(contactListFilters.getPredicate()))
}

运行结果为:

>> [Person(firstName=Svelana, phoneNumber=null)]

2.6 通过 lambda 去除重复代码

我们来看一个分析网站的例子,SiteVisit类用来保存每次访问的路径、持续时间和用户的操作系统。

data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }

val log2 = listOf(
    SiteVisit("/", 34.0, OS.WINDOWS),
    SiteVisit("/", 22.0, OS.MAC),
    SiteVisit("/login", 12.0, OS.WINDOWS),
    SiteVisit("/signup", 8.0, OS.IOS),
    SiteVisit("/", 16.3, OS.ANDROID)
)

接下来,我们通过扩展函数的方式,定义一个方法用于统计 符合特定条件 的操作系统用户的平均使用时长。

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

运行下面的代码:

fun main(args: Array<String>) {
    println(log2.averageDuration {it.os in setOf(OS.WINDOWS, OS.ANDROID) })
}

对于一些广为人知的设计模式可以使用函数类型和lambda表达式进行简化,比如策略模式。没有lambda表达式的情况下,你需要声明一个接口,并为每一种可能的策略提供实现类。使用函数类型,可以用一个通用的函数类型来描述策略,然后传递不同的lambda表达式作为不同的策略。


更多文章,欢迎访问我的 Android 知识梳理系列:

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

推荐阅读更多精彩内容