Kotlin学习——函数与Lambda表达式

函数

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

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

//调用函数使用传统的方法
val result = double(2)

//调用成员函数使用点表示法
Stream().read()

参数

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

默认参数:函数参数可以有默认值,当省略相应的参数时使用默认值。与其他语言相比,这可以减少重载数量

fun powerOf(number: Int, exponent: Int){/*...*/}

//默认值通过类型后面的 = 及给出的值来定义
fun read(b: Array, off: Int = 0, len : Int = b.size){ /*....*/ }

覆盖方法总是使用与基类型方法相同的默认参数值。当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值。

如果一个默认参数在一个无默认值的参数之前,那么该默认值只能通过使用命名参数调用该函数来使用

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

//如果在默认参数之后的最后一个参数是lambada表达式,那么它即可以作为命名参数在括号内传入,
//也可以在括号外传入
fun foo(bar: Int = 0, baz: Int = 1, qux: ()-> Unit()){ /*....*/ }

foo(1){ println("hello") }.  //使用默认值 baz = 1
foo(qux = { println("hello") }) //使用两个默认值 bar = 0 和 baz = 1
foo{ println("hello") }      //使用两个默认值 bar = 0 和 baz = 1

命名参数

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

fun reformat(str: String, normalCase: Boolean = true, firstLetter: Boolean = true,
    wordSeparator: Char = ' '){
    /*....*/
}

//我们可以使用默认参数来调用它
reformat(str)

//如果我们不需要所有的参数
reformat(str, wordSeparator = '_')

当一个函数调用混用位置参数与命名参数时,所有位置参数都要放在第一个命名参数之前。例如允许调用 f(1, y=2) 但不允许 f(x =1, 2)

可以通过使用星号操作符将可变数量参数( vararg) 以命名形式传入

fun foo(vararg strings: String) { .... }

foo(strings = *arrayOf("a", "b", "c"))

返回 Unit 的函数

如果一个函数不返回任何有用的值,它的返回类型是 Unit。 Unit 是一种只有一个值——Unit 的类型。这个值不需要现式返回,Unit 返回类型声明也是可选的(相当于java中的 void)

单表达式函数

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

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

//当返回值类型可由编译器推断时,显式声明返回类型是可选的
fun double(x: Int) = x * 2

显式返回类型

具有块代码体的函数必须始终显式指定返回类型,除非他们想要返回 Unit,在这种情况下它是可以省略的

可变数量的参数(Varargs)

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

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

//允许将可变数量的参数传递给函数
val list = asList(1,2,3)

在函数内部,类型T的 vararg 参数的可见方式是作为 T 数组,即上例中的 ts 变量具有类型 Array

只有一个参数可以标注为 vararg。如果 vararg 参数不是列表中的最后一个参数,可以使用命名参数语法传递其后的参数的值,或者,如果参数具有函数类型,则通过在括号外部传一个 lambda

当我们调用 vararg -函数时,我们可以一个接一个的传参,如果我们已经有一个数组并希望将其内容传递给该函数,我们使用伸展(spread)操作符(在数组前面加 *)

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

中缀表示法

标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。中缀函数必须满足以下要求:

  • 它们必须是成员函数或扩展函数
  • 它们必须只有一个参数
  • 其参数不得接受可变数量的参数且不能有默认值

中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符;优先级高于布尔操作符 && 与 || 、is- 与 in- 检测以及其他一些操作符

中缀函数总是要求指定接收者与参数。当使用中缀表示法在当前接收者上调用方法时,需要显式使用 this;不能像常规方法调用那样省略,这是确保非模糊解析所必需的

class MyStringCollection{
    infix fun add(s: String){ /*...*/ }

    fun build(){
        this add "abc"  //正确
        add("abc")      //正确
        // add "abc"    //错误:必须指定接收者
    }
}

尾递归函数

Kotlin支持一种称为尾递归的函数式编程风格。这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。

当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本

val eps = 1E - 10

//这段代码计算余弦的不动点是一个数学常数
tailrec fun findFixPoint(x: Double = 1.0): Double
    = if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

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

高阶函数与 lambda 表达式

高阶函数

是将函数用作参数或返回值的函数

fun  Collection.fold(initial: R,
    combine: (acc: R, nextElement: T) -> R):R{
        var accumulator: R = initial
        for(element: T in this){
            accumulator = combine(accumulator, element)
        }
        return accumulator
}

在上面的代码中,参数 combine 具有函数类型 (R, T) -> R,因此 fold 接受一个函数作为参数,该函数接受类型分别为 R 与 T 的两个参数并返回一个 R 类型的值。在 for-循环内部调用该函数,然后将其返回值赋值给 accumulator

函数类型

Kotlin使用类似 (Int) -> String 的一系列函数类型来处理函数的声明:val onClick: () => Unit = ......

这些类型具有与函数签名相对应的特殊表示法,即它们的参数和返回值:

  • 所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B)-> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。参数类型列表可以为空,如 () -> A 。Unit 返回类型不可省略
  • 函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定: 类型 A.(B) -> C 表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。带有接收者的函数字面值通常与这些类型一起使用
  • 挂起函数属于特殊种类的函数类型,它的表示法中有一个 suspend 修饰符,例如 suspend() -> Unit 或者 suspend A.(B) -> C

函数类型表示法可以选择性的包含函数的参数名: (x: Int, y: Int) -> Point。这些名称可用表明参数的含义。

如需将函数类型指定为可空,可以使用圆括号:((Int, Int) -> int)?

函数类型实例化

有几种方法可以获得函数类型的实例

  • 使用函数字面值的代码块,采用以下形式之一
    • lambda 表达式: { a, b -> a+b}
    • 匿名函数: fun(s: String): Int{ return s.toIntorNull()?: 0}
  • 使用已有声明的可调用引用
    • 顶层、局部、成员、扩展函数: ::isOdd、Sting::toInt
    • 顶层、成员、扩展属性:List::size
    • 构造函数: ::Regex(这包括指向特定实例成员的绑定的可调用引用:foo::toString
  • 使用实现函数类型接口的自定义类的实例
    class IntTransformer: (Int) -> Int{
        override operator fun invoke(x: Int): Int = TODO()
    }
    
    val intFunction: (Int) -> Int = IntTransofrmer()

函数类型实例调用

函数类型的值可以通过其 invoke(....) 操作符调用:f.invoke(x) 或者直接 f(x)

如果该值具有接收者类型,那么应该将接收者对象作为第一个参数传递。调用带有接收者的函数类型值的另一个方式是在其前面加上接收者对象,就好比该值是一个扩展韩式:1.foo(2)

    val stringPlus: (String, String) -> String = String::plus
    val intPlus: Int.(Int) -> Int = Int::plus
    println(stringPlus.invoke("<-","->"))
    println(stringPlus("hello","world!"))

    println(intPlus.invoke(1,1))
    println(intPlus(1,2))
    println(2.intPlus(3)) // 类扩展调用

   /*执行结果
    <-->
    helloworld!
    2
    3
    5 */

Lambda 表达式与匿名函数

lambda 表达式与匿名函数是“函数字面值”,即未声明的函数,但立即做为表达式传递

max(stings, {a, b -> a.length < b.length})
//函数 max 是一个高阶函数,它接受一个函数作为第二个参数。其第二个参数是一个表达式
//它本是是一个韩式,即函数字面值,它等价于下面的命名韩式:
fun compare(a:String, b:String): Boolean = a.length < b.length

Lambda 表达式语法

Lambda 表达式的完整语法形式如下:

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

//如果我们把所有可选标注都留下,看起来如下
val sum = { x, y -> x + y}

Lambda 表达式总是括在花括号中,完整语法形式的参数声明放在花括号内,并有可选的类型标注,函数体跟在一个 -> 符号之后。如果推断出该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值

传递末尾的 lambda 表达式

在 Kotlin 中有一个约定:如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外

val product = items.fold(1) { acc, e -> acc * e}

//如果该lambda表达式是调用时唯一的参数,那么圆括号可以完全省略
run { println("...") }

这种语法也称为拖尾lambda表达式 

it: 单个参数的隐式名称

一个lambda 表达式只有一个参数是很常见的。如果编译器自己可以识别出签名,也可以不用声明唯一的参数并忽略 ->。该参数会隐式声明为 it

ints.filter{ it > 0 } // 这个字面值是“( it: Int) -> Boolean" 类型的

从 lambda 表达式中返回一个值

我们可以使用限定的返回语法从 lambda 显式返回一个值。否则,将隐式返回最后一个表达式的值

ints.filter{
    val shouldFilter = it > 0
    shouldFilter
}

//等价于下面的代码
ints.filter{
    val shouldFilter = it > 0
    return@filter shouldFilter
}

下划线用于未使用的变量(自1.1起)

如果 lambda 表达式的参数未使用,那么可以用下划线取代其名称:

map.forEach{ _, value -> println("$value!")}

匿名函数

上面提供的 lambda 表达式语法缺少的一个东西是指定函数的返回类型的能力,在大多数情况下,这是不必要的。因为返回类型可以自动推断出来。然而,如果确定需要显式指定,可以使用另一种语法:匿名函数

匿名函数看起来非常像一个常规函数声明,除了其名称省略了。其函数体可以是表达式或代码块

//表达式
fun(x: Int, y: Int): Int = x + y 

//代码块
fun(x: Int, y: Int): Int{
    return x + y
}

//参数和返回类型的指定方式与常规函数相同,除了能够从上下文推断出的参数类型可以省略
ints.filter(fun(item) = item > 0)

匿名函数的返回类型推断机制与正常函数一样:对于具有表达式函数体的匿名函数将自动推断返回类型,而具有代码块函数体的返回类型必须显式指定(或者已假定为 Unit)

注意:匿名函数参数总是在括号内传递。允许将函数留在圆括号外的简写语法仅适用于 lambda 表达式

Lambda 表达式与匿名函数之间的另一个区别是非局部返回的行为。一个不带标签的 reutrn 语句总是在用 fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,而匿名函数中的 retun 将从匿名函数自身返回

闭包

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

var sum = 0
ints.filter{ it > 0 }.forEach{
    sum += it
}

print(sum)

带有接收者的函数字面值

带有接收者的函数类型。例如 A.(B) -> C,可以用特殊形式的函数字面值实例化——带有接收者的函数字面值

Kotlin 提供了调用带有接收者(提供接收者对象)的函数类型实例的能力。在这样的函数字面值内部,传给调用的接收者对象称为隐式的 this,以便访问接收者对象的成员而无需任何额外的限定符,亦可以使用 this 表达式访问接收者对象。

内联函数

使用高阶函数会带来一些运行时的效率损失:每个函数都是一个对象,并且会捕获一个闭包:即那些在函数体内会访问到的变量。内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。

但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。我们使用 inline 修饰符标记函数,inline 修饰符影响函数本身和传递给它的lambda表达式:所有这些都将内联到调用处。

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

禁用内联

如果希望只内联一部分传给内联函数的 lambda 表达式参数,那么可以用 noinline 修饰符标记不希望内联的函数参数

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

非局部返回

在Kotlin中,我们可以只使用一个正常的、非限定的 return 来退出一个命名或匿名函数。这意味着要退出一个 lambda 表达式,我们必须使用一个标签,并且在 lambda 表达式内部禁止使用裸 return ,因为 lambda 表达式不能使包含它的函数返回:

fun ordinaryFunction(block: () -> Unit){
    println("hello")
}

fun foo(){
    ordinaryFunction{
        return //错误:不能使 foo 在此处返回
    }
}

//但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联,那么它是允许的
inline fun inlined(block:() -> Unit){ println("word") }

fun foo(){
    inlined{
        return //OK:该 lambda 表达式是内联的
    }
}

位于 lambda 表达式中,但退出包含它的函数称为非局部返回。

一些内联函数可能调用传给它们的不是直接来自函数体、而是来自另一个执行上下文的 lambda 表达式参数,例如来自局部对象或嵌套函数。在这种情况下,该 lambda 表达式中也不允许非局部控制流。为了标识这种情况,该 lambda 表达式参数需要用 crossinline 修饰符标记

inline fun f(crossinline body: () -> Unit){
    val f = object: Runnable{
        override fun run() = body()
    }
}

具体化的类型参数

有时候我们需要访问一个作为参数传给我们的一个类型,我们可以使用 reified 修饰符来限定类型参数,可以在函数内部访问,几乎就像是一个普通的类一样。

内联属性

inline 修饰符可用于没有幕后字段的属性的访问器。可以标注独立的属性访问器(get或set单个方法),也可以标注整个属性,将它的两个访问器都标记为内联。在调用处,内联访问器如同内联函数一样内联

你可能感兴趣的:(Kotlin学习,kotlin,开发语言,android)