《Kotin 极简教程》第8章 函数式编程(FP)(2)


《Kotlin极简教程》正式上架:

点击这里 > 去京东商城购买阅读

点击这里 > 去天猫商城购买阅读

非常感谢您亲爱的读者,大家请多支持!!!有任何问题,欢迎随时与我交流~


8.2 在Kotlin中使用函数式编程

好了亲,前文中我们在函数式编程的世界里遨游了一番,现在我们把思绪收回来,放到在Kotlin中的函数式编程中来。

严格的面向对象的观点,使得很多问题的解决方案变得较为笨拙。为了将一行有用的代码包装到Runnable或者Callable 这两个Java中最流行的函数式示例中,我们不得不去写五六行模板范例代码。为了让事情简单化(在Java 8中,增加Lambda表达式的支持),我们在Kotlin中使用普通的函数来替代函数式接口。事实上,函数式编程中的函数,比C语言中的函数或者Java中的方法都要强大的多。

在Kotlin中,支持函数作为一等公民。它支持高阶函数、Lambda表达式等。我们不仅可以把函数当做普通变量一样传递、返回,还可以把它分配给变量、放进数据结构或者进行一般性的操作。它们可以是未经命名的,也就是匿名函数。我们也可以直接把一段代码丢到 {}中,这就是闭包。

在前面的章节中,其实我们已经涉及到一些关于函数的地方,我们将在这里系统地学习一下Kotlin的函数式编程。

8.2.1 Kotlin中的函数

首先,我们来看下Kotlin中函数的概念。

函数声明

Kotlin 中的函数使用 fun 关键字声明

fun double(x: Int): Int {
    return 2*x
}

函数用法

调用函数使用传统的方法

fun test() {
        val doubleTwo = double(2)
        println("double(2) = $doubleTwo")
    }

输出:double(2) = 4

调用成员函数使用点表示法

object FPBasics {

    fun double(x: Int): Int {
        return 2 * x
    }

    fun test() {
        val doubleTwo = double(2)
        println("double(2) = $doubleTwo")
    }
}

fun main(args: Array<String>) {
    FPBasics.test()
}

我们这里直接用object对象FPBasics来演示。

8.2.2 扩展函数

通过 扩展 声明完成一个类的新功能 扩展 ,而无需继承该类或使用设计模式(例如,装饰者模式)。

一个扩展String类的swap函数的例子:

    fun String.swap(index1: Int, index2: Int): String {
        val charArray = this.toCharArray()
        val tmp = charArray[index1]
        charArray[index1] = charArray[index2]
        charArray[index2] = tmp

        return charArrayToString(charArray)
    }

    fun charArrayToString(charArray: CharArray): String {
        var result = ""
        charArray.forEach { it -> result = result + it }
        return result
    }

这个 this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象)。 现在,我们对任意 String 调用该函数了:

        val str = "abcd"
        val swapStr = str.swap(0, str.lastIndex)
        println("str.swap(0, str.lastIndex) = $swapStr")

输出: str.swap(0, str.lastIndex) = dbca

8.2.3 中缀函数

在以下场景中,函数还可以用中缀表示法调用:

  • 成员函数或扩展函数
  • 只有一个参数
  • infix 关键字标注

例如,给 Int 定义扩展

infix fun Int.shl(x: Int): Int {
 ...
}

用中缀表示法调用扩展函数:

1 shl 2

等同于这样

1.shl(2)

8.2.4 函数参数

函数参数使用 Pascal 表示法定义,即 name: type。参数用逗号隔开。每个参数必须显式指定其类型。

    fun powerOf(number: Int, exponent: Int): Int {
        return Math.pow(number.toDouble(), exponent.toDouble()).toInt()
    }

测试代码:

        val eight = powerOf(2, 3)
        println("powerOf(2,3) = $eight")

输出:powerOf(2,3) = 8

默认参数

函数参数可以有默认值,当省略相应的参数时使用默认值。这可以减少重载数量。

    fun add(x: Int = 0, y: Int = 0): Int {
        return x + y
    }

默认值通过类型后面的 = 及给出的值来定义。

测试代码:

        val zero = add()
        val one = add(1)
        val two = add(1, 1)
        println("add() = $zero")
        println("add(1) = $one")
        println("add(1, 1) = $two")

输出:

add() = 0
add(1) = 1
add(1, 1) = 2

另外,覆盖带默认参数的函数时,总是使用与基类型方法相同的默认参数值。
当覆盖一个带有默认参数值的方法时,签名中不带默认参数值:

open class DefaultParamBase {
    open fun add(x: Int = 0, y: Int = 0): Int {
        return x + y
    }
}

class DefaultParam : DefaultParamBase() {
    override fun add(x: Int, y: Int): Int { // 不能有默认值
        return super.add(x, y)
    }
}

命名参数

可以在调用函数时使用命名的函数参数。当一个函数有大量的参数或默认参数时这会非常方便。

给定以下函数

    fun reformat(str: String,
                 normalizeCase: Boolean = true,
                 upperCaseFirstLetter: Boolean = true,
                 divideByCamelHumps: Boolean = false,
                 wordSeparator: Char = ' ') {

    }

我们可以使用默认参数来调用它

reformat(str)

然而,当使用非默认参数调用它时,该调用看起来就像

reformat(str, true, true, false, '_')

使用命名参数我们可以使代码更具有可读性

reformat(str,
    normalizeCase = true,
    upperCaseFirstLetter = true,
    divideByCamelHumps = false,
    wordSeparator = '_'
)

并且如果我们不需要所有的参数

reformat(str, wordSeparator = '_')

可变数量的参数(Varargs)

函数的参数(通常是最后一个)可以用 vararg 修饰符标记:

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts is an Array
        result.add(t)
    return result
}

允许将可变数量的参数传递给函数:

val list = asList(1, 2, 3)

8.2.5 函数返回类型

函数返回类型需要显式声明

具有块代码体的函数必须始终显式指定返回类型,除非他们旨在返回 Unit

Kotlin 不推断具有块代码体的函数的返回类型,因为这样的函数在代码体中可能有复杂的控制流,并且返回类型对于读者(有时对于编译器)也是不明显的。

返回 Unit 的函数

如果一个函数不返回任何有用的值,它的返回类型是 UnitUnit 是一种只有一个Unit 值的类型。这个值不需要显式返回:

    fun printHello(name: String?): Unit {
        if (name != null)
            println("Hello ${name}")
        else
            println("Hi there!")
        // `return Unit` 或者 `return` 是可选的
    }

Unit 返回类型声明也是可选的。上面的代码等同于

fun printHello(name: String?) {
    .....
}

8.2.6 单表达式函数

当函数返回单个表达式时,可以省略花括号并且在 = 符号之后指定代码体即可

fun double(x: Int): Int = x * 2

当返回值类型可由编译器推断时,显式声明返回类型是可选的:

fun double(x: Int) = x * 2

8.2.7 函数作用域

在 Kotlin 中函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java、C# 或 Scala 那样创建一个类来保存一个函数。此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数以及扩展函数。

局部函数(嵌套函数)

Kotlin 支持局部函数,即一个函数在另一个函数内部

     fun sum(x: Int, y: Int, z: Int): Int {
        val delta = 0;
        fun add(a: Int, b: Int): Int {
            return a + b + delta
        }
        return add(x + add(y, z))
    }

局部函数可以访问外部函数(即闭包)中的局部变量delta。

println("sum(1,2,3) = ${sum(0, 1, 2, 3)}")

输出:

sum(1,2,3) = 6

成员函数

成员函数是在类或对象内部定义的函数

class Sample() {
    fun foo() { print("Foo") }
}

成员函数以点表示法调用

Sample().foo() // 创建类 Sample 实例并调用 foo

8.2.8 泛型函数

函数可以有泛型参数,通过在函数名前使用尖括号指定。

例如Iterable的map函数:

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

8.2.9 高阶函数

高阶函数是将函数用作参数或返回值的函数。例如,Iterable的filter函数:

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

它的输入参数predicate: (T) -> Boolean就是一个函数。其中,函数类型声明的语法是:

(X)->Y

表示这个函数是从类型X到类型Y的映射。即这个函数输入X类型,输出Y类型。

这个函数我们这样调用:

    fun isOdd(x: Int): Boolean {
        return x % 2 == 1
    }

   val list = listOf(1, 2, 3, 4, 5)
   list.filter(::isOdd)

其中,::用来引用一个函数。

8.2.10 匿名函数

我们也可以使用匿名函数来实现这个predicate函数:

        list.filter((fun(x: Int): Boolean {
            return x % 2 == 1
        }))

8.2.11 Lambda 表达式

我们也可以直接使用更简单的Lambda表达式来实现一个predicate函数:

        list.filter {
            it % 2 == 1
        }
  • lambda 表达式总是被大括号 {} 括着
  • 其参数(如果有的话)在 -> 之前声明(参数类型可以省略)
  • 函数体(如果存在的话)在 -> 后面

上面的写法跟:

        list.filter({
            it % 2 == 1
        })

等价,如果 lambda 是该调用的唯一参数,则调用中的圆括号可以省略。

使用Lambda表达式定义一个函数字面值:

>>> val sum = { x: Int, y: Int -> x + y }
>>> sum(1,1)
2

我们在使用嵌套的Lambda表达式来定义一个柯里化的sum函数:

>>> val sum = {x:Int ->  {y:Int -> x+y }}
>>> sum
(kotlin.Int) -> (kotlin.Int) -> kotlin.Int
>>> sum(1)(1)
2

8.2.11 it:单个参数的隐式名称

Kotlin中另一个有用的约定是,如果函数字面值只有一个参数,
那么它的声明可以省略(连同 ->),其名称是 it

代码示例:

>>> val list = listOf(1,2,3,4,5)
>>> list.map { it * 2 }
[2, 4, 6, 8, 10]

8.2.12 闭包(Closure)

Lambda 表达式或者匿名函数,以及局部函数和对象表达式(object declarations)可以访问其 闭包 ,即在外部作用域中声明的变量。 与 Java 不同的是可以修改闭包中捕获的变量:

    fun sumGTZero(c: Iterable<Int>): Int {
        var sum = 0
        c.filter { it > 0 }.forEach {
            sum += it
        }
        return sum
    }

val list = listOf(1, 2, 3, 4, 5)
sumGTZero(list) // 输出 15

我们再使用闭包来写一个使用Java中的Thread接口的例子:

    fun closureDemo() {
        Thread({
            for (i in 1..10) {
                println("I = $i")
                Thread.sleep(1000)
            }

        }).start()

        Thread({
            for (j in 10..20) {
                println("J = $j")
                Thread.sleep(2000)
            }
            Thread.sleep(1000)
        }).start()
    }

一个输出:

I = 1
J = 10
I = 2
I = 3
...
J = 20

8.2.13 带接收者的函数字面值

Kotlin 提供了使用指定的 接收者对象 调用函数字面值的功能。

使用匿名函数的语法,我们可以直接指定函数字面值的接收者类型。

下面我们使用带接收者的函数类型声明一个变量,并在之后使用它。代码示例:

>>> val sum = fun Int.(other: Int): Int = this + other
>>> 1.sum(1)
2

当接收者类型可以从上下文推断时,lambda 表达式可以用作带接收者的函数字面值。

class HTML {
    fun body() {
        println("HTML BODY")
    }
}

fun html(init: HTML.() -> Unit): HTML { // HTML.()中的HTML是接受者类型
    val html = HTML()  // 创建接收者对象
    html.init()        // 将该接收者对象传给该 lambda
    return html
}

测试代码:

    html {
        body()
    }

输出:HTML BODY

使用这个特性,我们可以构建一个HTML的DSL语言。

8.2.14 具体化的类型参数

有时候我们需要访问一个参数类型:

fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T?
}

在这里我们向上遍历一棵树并且检查每个节点是不是特定的类型。
这都没有问题,但是调用处不是很优雅:

treeNode.findParentOfType(MyTreeNode::class.java)

我们真正想要的只是传一个类型给该函数,即像这样调用它:

treeNode.findParentOfType<MyTreeNode>()

为能够这么做,内联函数支持具体化的类型参数,于是我们可以这样写:

inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

我们使用 reified 修饰符来限定类型参数,现在可以在函数内部访问它了,
几乎就像是一个普通的类一样。由于函数是内联的,不需要反射,正常的操作符如 !isas 现在都能用了。

虽然在许多情况下可能不需要反射,但我们仍然可以对一个具体化的类型参数使用它:

inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}

普通的函数(未标记为内联函数的)没有具体化参数。

8.2.10 尾递归tailrec

Kotlin 支持一种称为尾递归的函数式编程风格。 这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。 当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,生成一个快速而高效的基于循环的版本。

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) // 函数必须将其自身调用作为它执行的最后一个操作

这段代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos,直到结果不再改变,产生0.7390851332151607的结果。最终代码相当于这种更传统风格的代码:

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (x == y) return y
        x = y
    }
}

要符合 tailrec 修饰符的条件的话,函数必须将其自身调用作为它执行的最后一个操作。在递归调用后有更多代码时,不能使用尾递归,并且不能用在 try/catch/finally 块中。尾部递归在 JVM 后端中支持。

Kotlin 还为集合类引入了许多扩展函数。例如,使用 map() 和 filter() 函数可以流畅地操纵数据,具体的函数的使用以及示例我们已经在 集合类 章节中介绍。

本章小结

本章我们一起学习了函数式编程的简史、Lambda演算、Y组合子与递归等核心函数式的编程思想等相关内容。然后重点介绍了在Kotlin中如何使用函数式风格编程,其中重点介绍了Kotlin中函数的相关知识,以及高阶函数、Lambda表达式、闭包等核心语法,并给出相应的实例说明。

我们将在下一章 中介绍Kotlin的 轻量级线程:协程(Coroutines)的相关知识,我们将看到在Kotlin中,程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。

本章示例代码工程: https://github.com/EasyKotlin/chapter8_fp


Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

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

推荐阅读更多精彩内容