[Kotlin]研究 Kotlin 的函式宣告、函式型别、匿名函式、Lambda 与高阶函式

Kotlin 的“函式”是所谓的“一级函式”,支援“高阶函式”的用法,也可宣告“匿名函式”及“巢状函式”,这些都是近几年所兴起的程式语言特色。

(原本这篇文章只是要整理一下 Kotlin 的函式用法而已,没想到愈写愈多!!)

名词定义

先确认是否了解什么是表达式? 什么是叙述式?

Expression (表达式、表示式、运算式)

  • 它是一种“值”
  • 会传回结果
  • 单独存在没有意义
  • 可放在“等号”的右边
  • 可做为函式的引数 (Argument)
  • 可做为函式的传回值
  • 例如: 数值、字串、布林值、null、运算后的结果、比较后的结果、匿名函式...

Statement (陈述式、叙述式、语句)

  • 由会产生“动作”的程式关键字及语法所组成的程式码
  • 不会传回结果
  • 例如: 流程控制、循环、宣告、函式、类别...

其它名词定义

  • Literal: 字面值,例如: 103.14truenull'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 做为函式的“引数”时,还可以有一些简化:

  1. 可用 it 取代唯一参数
  2. 若 Lambda 为最后一个引数,可提取至小括号的后面
  3. 若 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 具有多行、简洁、参数明确的优点。
  • 看到“冒号”就会联想到型别,只要“等号”右边的型别有宣告清楚,那“等号”左边的型别可以省略。
  1. No comments yet.

  1. No trackbacks yet.

return top

%d 位部落客按了赞: