[Kotlin]函数

一、 概述

函数:也就是子程序。
高阶函数:在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:接受一个或多个函数作为输入,输出一个函数
在Java中,不支持高阶函数。也就是说函数不能作为参数,也不能作为返回值。
站在Java的基础上,Kotlin开始支持高阶函数。也就是说,函数在Kotlin中成为了一级公民。

二、 函数定义

2.1函数声明

  • Kotlin 中的函数使用 fun 关键字声明。
    函数的基本组成部分包括:名称、参数、返回值和函数体,定义形式为:
fun methodName(param: ParamType): ReturnType {
  ...
}

  • 函数名参数列表作为函数的唯一的标识符

2.2、参数

  • 函数参数使用 Pascal 表示法定义,即 name: type。每个参数必须有显式类型,多个参数之间使用逗号分隔.
fun methodName(number: Int, exponent: String) {
  ...
}

2.3默认参数

函数参数可以指定默认值, 当参数省略时, 就会使用默认值. 与其他语言相比, 这种功能使得我们可以减少大 量的重载(overload)函数定义.

//在参数类型之后, 添加 = 和默认值.
fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) {
  ...
}
  • override方法与基类型方法使用相同的默认值。override时必须省略默认参数值:
open class A {
    open fun foo(i: Int = 10) { …… }
}

class B : A() {
    override fun foo(i: Int) { …… }  // 不能有默认值
}
  • 如果一个默认参数在一个无默认值的参数之前,那么该默认值只能通过使用命名参数调用该函数来使用:
fun foo(bar: Int = 0, baz: Int) { /* …… */ }

foo(baz = 1) // 使用默认值 bar = 0

2.4命名参数

调用函数时, 可以通过参数名来指定参数. 当函数参数很多, 或者存在默认参数时, 指定参数名可增加可读性.

  • 通常与默认参数配合使用

例如:

fun login(name: String, no: Int = 1001, sex: Int = 0) {
    println("name: $name, no:$no, sex: $sex")
}

当采用默认方式时,我们可以这样调用(无默认值参数位于参数列表的第一个时)

login("Aaron")

其实际上相当于

login("Aaron", 101, 0)

人如果我们不需要指定所有的参数, 只是修改部分默认的参数值,我们可以这样:

register(name = "wang", no = 1003)

2.5不定数量参数

如果在函数被调用以前,函数的参数(通常是参数中的最后一个)个数不能够确定,可以采用不定量参数方式:用 vararg 修饰符标记参数。
比如在创建List时,创建前并不知道预添加至List中多少数据。

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

调用时, 可以向这个函数传递不定数量的参数:

val list = asList(1, 2, 3)

  • 只有一个参数可以标注为 vararg

与Java不同的是,在Kotlin中标记为vararg的参数不一定是最后一个。如果标记为vararg的参数不是最后一个,那么vararg参数之后的其他参数, 可以使用命名参数来传递参数值, 或者, 如果参数类型是函数, 可以在括号之外传递一个 Lambda表达式.

fun main(args: Array<String>) {

    fruit("apple", "banana", address = "Minhang")

}

fun fruit(vararg fruits: String, address: String) {
    for (fruit in fruits) {
        println("fruit:$fruit, from address: $addr")
    }
}

// Log
fruit:apple, from address: Minhang
fruit:banana, from address: Minhang

  • 我们调用vararg函数时,我们可以一个接一个地传参,例如 asList(1, 2, 3),或者,如果我们已经有一个数组并希望将其内容传给该函数,我们使用伸展spread操作符(在数组前面加 *):
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

2.6 返回值Unit

如果一个函数不返回任何有意义的结果值,那么它的返回类型为Unit .Unit 类型只有唯一的一个值Unit,在函数中,不需要明确地返回这个值。

  • 返回值为Unit的函数,Unit可以省略。
fun login(name: String, no: Int = 1001, sex: Int = 0): Unit {
    println("name: $name, no:$no, sex: $sex")
}

上例中的代码等价于:

fun login(name: String, no: Int = 1001, sex: Int = 0) {
    println("name: $name, no:$no, sex: $sex")
}

2.7 明确指定返回值类型

如果一个函数体由多行语句组成的代码段,那么必须明确指定返回值类型,除非函数的的返回值为Unit。

2.8 单表达式函数

如果一个函数的函数体只有一个表达式,函数体可以直接写在 “=”之后,也就是这样:

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

如果编译器可以推断出函数的返回值类型, 那么返回值的类型定义是可省略:

fun double(x: Int) = x * 2

2.9 返回值和跳转

Kotlin有三种结构型的跳转表达式:

  • return.默认返回最近的闭包函数和匿名函数
  • break.终结最近的闭包循环
  • continue.进行下一步最近的闭包循环

Break 与 Continue 标签
在 Kotlin 中任何表达式都可以用标签(label)来标记。 标签的格式为标识符后跟 @ 符号,例如:abc@fooBar@都是有效的标签。要为一个表达式加标签,我们只要在其前加标签即可。

loop@ for (i in 1..100) {
    // ……
}

现在,我们可以用标签限制 break 或者continue

loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (……) break@loop
    }
}

标签限制的 break 会跳转到‘该标签指定的循环’后面的执行点(也就是最外层for循环)。

标签处返回
Kotlin 的函数可以被嵌套。 标签限制的 return 允许我们从外层函数返回。 最重要的用例是返回一个lambda表达式。
例如:

fun foo() {
    ints.forEach {
        if (it == 0) return
        print(it)
    }
}

return-expression从他最近的闭包函数当中返回,也就是foo.如果我们需要返回到ints.forEach的lambda表达式,我们必须给它做出标记

 fun foo(ints:List<Int>) {
        ints.forEach lit@ {
            if (it == 0) return@lit
            print(it)
        }
    }

现在,它仅仅从ints.forEach的lambda表达式处返回。

  • 通常情况下,使用隐喻标签更方便(标签与被传入的lambda表达式的函数具有相同的名称)。
fun foo() {
    ints.forEach {
        if (it == 0) return@forEach
        print(it)
    }
}

匿名函数与lambda同理,不再赘述。

三、函数的调用

3.1 传统用法

函数的调用使用传统的方式:

val result = double(2)

调用类的成员函数时, 使用点号标记法(dot notation):

Sample().foo() // 创建一个 Sample 类的实例, 然后调用这个实例的 foo 函数

3.2 中缀标记法(Infix notation)

使用中缀标记法(infix notation)来调用函数, 但函数需要满足以下条件:

① 是成员函数, 或者是扩展函数
② 只有单个参数
③ 使用 infix 关键字标记

class Person(var name: String, var age: Int) {
   // 使用infix 关键字标记,该函数可被中缀标记法法调用
     infix fun printName(addr: String) {
         println("addr: $addr, name: $name")
     }
}

fun main(args: Array) { 
   val person: Person = Person(“Jone”, 20)
   // 使用中缀标记法调用扩展函数
   person printName("AA-BB") // Log: addr: AA-BB, name: Jone

   // 上面的语句等价于
   person.printName("AA-BB") 
}

四、函数的范围

在Kotlin中,函数不仅仅能够被定义为top_level,即包下的函数,还可以被定义为局部函数、成员函数以及扩展函数。函数的定义方式不同,其作用域也不尽相同。当然了,函数的作用域还与修饰符相关,具体可参考 Kotlin-可见性修饰符.

4.1 局部函数

所谓的局部函数,就是定义在函数体的函数。

fun function(input: String) {
    val param:Int = 101
    fun finctionInternal(Inputinternal:Int) {
        ...
    } 
...
    finctionInternal(param)
}

  • 局部函数可以访问外部函数中的局部变量(也就是, 闭包),具体见上例。

4.2 成员函数

成员函数是指定义在类或对象之内的函数。

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

对成员函数的调用使用点号标记法。

Sample().foo() // 创建 Sample 类的实例, 并调用 foo 函数

4.3 扩展函数

扩展函数数是指在一个类上增加一种新的行为,甚至我们没有这个类代码的访问权限。

  • 声明一个扩展函数,我们需要用一个接收者类型也就是被扩展的类型来作为他的前缀。

/***
 * 点击事件的View扩展
 * @param block: (T) -> Unit 函数
 * @return Unit
 */
fun <T : View> T.click(block: (T) -> Unit) = setOnClickListener {

    if (clickEnable()) {
        block(it as T)
    }
}
  • 扩展是静态解析的
  • 如果一个类定义有一个成员函数和一个扩展函数,而这两个函数又有相同的接收者类型、相同的名字并且都适用给定的参数,这种情况总是取成员函数。
class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

如果我们调用 C 类型 c的 c.foo(),它将输出“member”,而不是“extension”。

可空接收者
可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为 null,并且可以在函数体内检测 this == null,这能让你在没有检测 null 的时候调用 Kotlin 中的toString():检测发生在扩展函数的内部。

fun Any?.toString(): String {
    if (this == null) return "null"
    // 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
    // 解析为 Any 类的成员函数
    return toString()
}

具体例子可参考 :
1.[Kotlin]利用扩展函数优雅的实现“防止重复点击”.
2.使用Kotlin高效地开发Android App(一).

4.4 尾递归函数

Kotlin支持一种称为尾递归(tail recursion)的函数式编程方式.这种方式是基于函数表达式和递归函数,来实现某些基本循环的的算法,采用这种方式可以有效的避免栈溢出的危险。

当函数被关键字tailrec修饰,同时满足尾递归(tail recursion)的函数式编程方式的形式时,编译器就会对代码进行优化, 消除函数的递归调用, 产生一段基于循环实现的, 快速而且高效的代码。

tailrec fun plus(start: Int, end: Int, result: Int): Int = if (start >= end) result else plus(start+1, end, start + result) 

// Test
fun main(args: Array<String>) {

    println(plus(0, 10, 0)) // 打印结果 45
}

上面的代码计算了从start到end之间的所有数的和,并将和与初始值相加后返回。编译器优化产生的代码等价于下面这种传统方式编写的代码:

fun plus(start: Int, end: Int, result: Int): Int {
    var res = result
    var sta = start

    while (sta < end) {
        res += sta
        sta++
    }

    return res
}

注:

  1. 要符合 tailrec 修饰符的要求, 函数必须在它执行的所有操作的最后一步, 递归调用它自身
  2. 如果递归调用后还有其他逻辑代码,不能使用尾递归
  3. 尾递归不能用在try/catch/finally 结构内
  4. 尾递归目前只能用在JVM环境内

五、高阶函数

所谓的高阶函数,是一种特殊的函数, 它接受函数作为参数, 或者返回一个函数.

fun test(a: Int, b: Int, sumSom: (Int, Int, Int) -> Int): Int {
    if (a > b) {
        return sumSom(0, a, 0)
    } else {
        return sumSom(0, b, 0)
    }
}

tailrec fun sumSom(start: Int, end: Int, result: Int): Int {
    var res = result
    var sta = start

    while (sta <= end) {
        res += sta
        sta++
    }

    return res
}

// 测试类
fun main(args: Array<String>) {
    println(test(10, 9, ::sumSom)) // Log:55
}

从上述代码,在函数test中,sumSom参数是一个函数类型:(Int, Int, Int) -> Int,其是一个函数,接受3个Int参数,返回值是一个Int类型的值。在test中,对传入的参数a,b进行判断,然后执行sumSom()函数并将执行结果返回。

5.1 函数类型(Function Type)

对于接受另一个函数作为自己参数的函数, 我们必须针对这个参数指定一个函数类型. 比如, 前面提到的test函数, 它的定义如下:

fun test(a: Int, b: Int, sumSom: (Int, Int, Int) -> Int): Int {
    if (a > b) {
        return sumSom(0, a, 0)
    } else {
        return sumSom(0, b, 0)
    }
}

参数sumSom的类型是(Int, Int, Int) -> Int,也就是说,它是一个函数,接受三个Int类型参数,并且返回一个Int。

5.2 Lambda表达式与匿名函数

Lambda 表达式, 或者匿名函数, 是一种”函数字面值(function literal)”, 也就是, 一个没有声明的函数, 但是立即作为表达式传递出去.

max(strings, { a, b -> a.length() < b.length() })

函数 max 是一个高阶函数, 也就是说, 它接受一个函数值作为第二个参数. 第二个参数是一个表达式, 本身又是另一个函数, 也就是说, 它是一个函数字面量. 作为函数, 它等价于:

fun compare(a: String, b: String): Boolean = a.length() < b.length() 

Lambda表达式

Lambda 表达式的完整语法形式, 也就是, 函数类型的字面值, 如下:

val sum = { x: Int, y: Int -> x + y }

① Lambda 表达式用大括号括起,
② 它的参数(如果存在的话)定义在 -> 之前 (参数类型可以省略),
③ 函数体定义在 -> 之后 (如果存在 -> 的话).

  • 如果Lambda 表达式只有唯一一个参数,在Kolin中可以自行判断出Lambda表达式的参数定义,此时允许我们省略唯一一个参数的定义, 并且会为我们隐含地定义这个参数, 使用的参数名为 it:
ints.filter { it > 0 } // 这个函数字面值的类型是 '(it: Int) -> Boolean'

  • 如果一个函数接受另一个函数作为它的最后一个参数, 那么Lambda表达式作为参数时, 可以写在圆括号之外.

可参考View.OnClickListener在Kotlin中的进化

匿名函数

匿名函数看起来与通常的函数声明很类似, 区别在于省略了函数名,函数体可以是一个表达式(如上例), 也可以是多条语句组成的代码段:

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

参数和返回值类型的声明与通常的函数一样, 但如果参数类型可以通过上下文推断得到, 那么类型声明可以省略:

ints.filter(fun(item) = item > 0)

对于匿名函数, 返回值类型的自动推断方式与通常的函数一样: 如果函数体是一个表达式, 那么返回值类型
可以自动推断得到, 如果函数体是多条语句组成的代码段, 则返回值类型必须明确指定(否则被认为是
Unit ).

5.3 内联函数

高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包( 即那些在函数体内会访问到的变量)。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。
在许多情况下通过内联化 lambda 表达式可以消除这类的开销。
lock() 函数为例:

lock(l) { foo() }

编译器没有为参数创建一个函数对象并生成一个调用。取而代之,编译器可以生成以下代码:

l.lock()
try {
    foo()
}
finally {
    l.unlock()
}

这个才是我们想要的。为了让编译器这么做,我们需要使用inline修饰符标记 lock() 函数:

inline fun <T> lock(lock: Lock, body: () -> T): T {
    // ……
}

  • 内联可能导致生成的代码增加;不过如果我们使用得当(即避免内联过大函数),性能上会有所提升,尤其是在循环中的“超多态(megamorphic)”调用处。

  • 通过noinline禁用内联

如果你只想被(作为参数)传给一个内联函数的 lamda 表达式中只有一些被内联,你可以用 noinline 修饰符标记一些函数参数:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    // ……
}

参考:
https://hltj.gitbooks.io/kotlin-reference-chinese/content/txt/functions-and-lambdas.html
https://blog.csdn.net/io_field/article/details/53365834

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

推荐阅读更多精彩内容