[Kotlin]研究 Kotlin 的函式宣告、函式型别、匿名函式、Lambda 与高阶函式
Kotlin 的“函式”是所谓的“一级函式”,支援“高阶函式”的用法,也可宣告“匿名函式”及“巢状函式”,这些都是近几年所兴起的程式语言特色。
(原本这篇文章只是要整理一下 Kotlin 的函式用法而已,没想到愈写愈多!!)
名词定义
先确认是否了解什么是表达式? 什么是叙述式?
Expression (表达式、表示式、运算式)
- 它是一种“值”
- 会传回结果
- 单独存在没有意义
- 可放在“等号”的右边
- 可做为函式的引数 (Argument)
- 可做为函式的传回值
- 例如: 数值、字串、布林值、null、运算后的结果、比较后的结果、匿名函式...
Statement (陈述式、叙述式、语句)
- 由会产生“动作”的程式关键字及语法所组成的程式码
- 不会传回结果
- 例如: 流程控制、循环、宣告、函式、类别...
其它名词定义
- Literal: 字面值,例如: 10、3.14、true、null、'A'、"This is a book"...
- Parameter: 参数,函式“宣告”时所输入的值,例如: fun example(参数) { }
- Argument: 引数,函式“执行”时所引用的值,例如: example(引数)
- Identifier: 识别字,命名变量、函式、类别...时所使用的文字
- Lambda: 一种匿名函式的写法或概念
Kotlin 函式种类
先看这张整理好的表格:
种类 | 语法 | 速记 | 函式主体 | 本质 | 传回值 |
---|---|---|---|---|---|
具名函式 | A.叙述式 | fun f():{} | 可多行 | 函式 | return |
B.单一表达式 | fun f()= | 一行 | 函式 | 隐性传回 | |
匿名函式 | A.仿叙述式 | fun():{} | 可多行 | 物件 | return |
B.单一表达式 | fun()= | 一行 | 物件 | 隐性传回 | |
C.Lambda | {->} | 可多行 | 物件 | 隐性传回 | |
函式物件 | f:()->=匿名函式 |
~ | 物件 | ~ |
以下以简单的 sum() 函式做为范例来看 Kotlin 各种函式的写法。
- 标记红色字为函式原本就会使用到的“参数及型别”、蓝色字为“函式主体”。
具名函式
A. 叙述式:
- 重点: 以 fun 开头、函式主体有多行、写 return 传回
fun sum (m:Int, n:Int): String { return "Result: ${m + n}" }
B. 单一表达式:
- 重点: 以 fun 开头、函式主体只能有一行、隐性传回最后的值
fun sum (m:Int, n:Int): String = "Result: ${m + n}"
呼叫函式与传递引数:
println(sum(1, 2)) // Result: 3
匿名函式 (物件)
- 把具名函式里的函式名称拿掉
- 好比是“工具人”,把事情做完就可以走了
- 所有的匿名函式都是“表达式”
- 匿名函式的本质是“物件”
A. 仿叙述式:
- 重点: 函式主体有多行、写 return 传回
fun(m:Int, n:Int): String { return "Result: ${m + n}" }
B. 单一表达式:
- 重点: 函式主体只能有一行、隐性传回最后的值
fun(m:Int, n:Int): String = "Result: ${m + n}"
C. Lambda:
- 重点: 仅用大括号来表现函式、函式主体有多行、隐性传回最后的值、可用 it 取代唯一参数
{ m:Int, n:Int -> "Result: ${m + n}" }
可以看到 Lambda 具有所有匿名函式的优点。但是这些匿名函式有什么屁用呢? 请接着看下去。
函式物件
在“一级函式”中,函式可以被当作“值”指派给另一个“变量”。Kotlin 的“函式物件”是由“匿名函式”转化而来的,是将“匿名函式”指派给另一个变量,成为“函式物件”。
函式型别:
Kotlin 是一种“强型别”语言,每一种值都有它的“资料型别”,因此函式物件也有属于自己的型别,称之为“函式型别”。
例如,用“资料型别”来宣告一般变量的语法:
var 变量名称: 资料型别 var 变量名称: 资料型别 = 值
用“函式型别”来宣告函式物件:
var 函式名称: 函式型别 var 函式名称: 函式型别 = 匿名函式
“函式型别”的宣告比较复杂! 因为要识别整个匿名函式的“输入型别”及“输出型别”,所以写做 (输入型别)->输出型别。若该匿名函式没有输入也没有输出则写做 ()->Unit。
并且,跟原本的变量宣告一样,只要可以从“等号”的右边推论出型别,那左边的函式型别也可以省略不写 (这里的“省略不写”只是程式设计师不用打字上去而已,实际上,编译器会偷偷帮你把型别补上去)。
以下是将 sum() 范例用“函式型别”改写成函式物件。
- 标记紫色字为“函式型别”
A. 仿叙述式:
var sum: (Int, Int) -> String = fun(m:Int, n:Int): String { return "Result: ${m + n}" } // 或是省略函式型别: var sum = fun(m:Int, n:Int): String { return "Result: ${m + n}" }
B. 单一表达式:
var sum: (Int, Int) -> String = fun(m:Int, n:Int): String = "Result: ${m + n}" // 或是省略函式型别: var sum = fun(m:Int, n:Int): String = "Result: ${m + n}"
C. Lambda:
var sum: (Int, Int) -> String = { m: Int, n: Int -> "Result: ${m + n}" } // 或是省略函式型别: var sum = { m: Int, n: Int -> "Result: ${m + n}" }
呼叫函式与传递引数:
原本函式物件要传入引数必须用 invoke() 方法,例如:
println(sum.invoke(1, 2)) // Result: 3
但 Kotlin 用语法糖包装函式物件,让你用 sum(1, 2) 就可以直接传入引数:
println(sum(1, 2)) // Result: 3
函式名称冲突:
Kotlin 允许相同名称的“具名函式”与“函式物件”同时存在,但具名函式的优先权高于函式物件。执行 functionName() 会呼叫“具名函式”,此时若要呼叫“函式物件”,必须用函式物件原本的 invoke() 方法:
functionName.invoke()
其它用法:
1. 若 Lambda 函式只有一个输入参数,可用 it 取代:
var inc = { n: Int -> "Result: ${n + 1}" } // 简化成: var inc = { "Result: ${it + 1}" } println(inc(2)) // Result: 3
2. 巢状 Lambda:
var sum: (Int, Int) -> (Int, Int) -> String = { m: Int, n: Int -> { x: Int, y: Int -> "Result: ${m + n + x + y}" } } // 或是省略函式型别: var sum = { m: Int, n: Int -> { x: Int, y: Int -> "Result: ${m + n + x + y}" } } println(sum(1, 2)(3, 4)) // Result: 10
3. 可用 :: 将“具名函式”转成“函式物件”:
// 宣告具名函式 oldSum(): fun oldSum(m:Int, n:Int): String { return "Result: ${m + n}" } println((::oldSum)(1, 2)) // Result: 3 // 将 oldSum() 转成函式物件: var sum = ::oldSum println(sum(1, 2)) // Result: 3
高阶函式
Kotlin 支援所谓的“高阶函式”,意思是函式可以作为“参数”传入另一个函式,也可以被函式作为“传回值”,而这些特性通常与“匿名函式”与“函式物件”相关。
范例 1
以下的范例是对 Kotlin 既有的 Int 整数型别“扩充”一个名为 check() 的函式,这个函式的功能是“让你传入另一个函式”去做你想要的检查。这个范例可以让你体会一下“高阶函式”的用途,以及各种匿名函式的写法。
- 说明: 因为以下的范例是为了“扩充”既有的型别,原本的“整数值”即为函式的第一个参数,在函式中是用 this 取代。而后面传入的匿名函式其实是第二个参数。
- 标记红字为“匿名函式”。
// 函式宣告 fun Int.check(fx: (Int) -> Boolean): Boolean { return fx(this) }
不专业的图解:
// 使用匿名函式,判断整数 7 是否大于 9 // 1. 仿陈述式 println(7.check(fun(n: Int): Boolean { return n > 9 })) // false // 2. 使用表达式 println(7.check(fun(n: Int) = n > 9)) // false // 3. 使用 Lambda println(7.check({ n: Int -> n > 9 })) // false
Lambda 简化:
当 Lambda 做为函式的“引数”时,还可以有一些简化:
- 可用 it 取代唯一参数
- 若 Lambda 为最后一个引数,可提取至小括号的后面
- 若 Lamba 为唯一的引数,可省略小括号
因此上面的 Lambda 执行范例可以再简化如下:
println(7.check({ it > 9 })) // false println(7.check() { it > 9 }) // false println(7.check { it > 9 }) // false
高阶函式的好处:
使用高阶函式可以让你任意更换程式逻辑,例如:
println(7.check { it % 2 == 0 }) // 判断整数 7 是否为偶数 println(12.check { it in 10..100 }) // 判断整数 12 是否介于 10 ~ 100
范例 2
以下是带多个参数的范例,最后一个参数为匿名函式。
// 函式宣告 fun Int.demo(text: String, fx: (Int, String) -> Unit): Unit { return fx(this, text) }
不专业的图解:
// 用 Lambda 传入的执行结果 5.demo("cars", { n, text -> println("The number of $text is $n") }) // The number of cars is 5 9.demo("cars") { n, text -> println("The number of $text is $n") } // The number of cars is 9
不专业的结论
- 在写程式时,我们通常会将“具名函式”宣告在程式的独立区块,即 main() 的外面,会优先被加载内存。
- “匿名函式”与“函式物件”是宣告在程式要用到的地方,用到时才会建立物件、加载内存。
- 多用 Lambda 来写函式,因为 Lambda 具有多行、简洁、参数明确的优点。
- 看到“冒号”就会联想到型别,只要“等号”右边的型别有宣告清楚,那“等号”左边的型别可以省略。
No comments yet.