Kotlin学习日记-4.函数

211 阅读13分钟

1.函数

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

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

1.1 函数用法

调用函数与调用对象函数:

val result = double(2)

Stream().read() // 创建类 Stream 实例并调用 read()

1.1.1 参数

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

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

1.1.2 默认参数

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

fun read(
    b: ByteArray,
    off: Int = 0,
    len: Int = b.size,
) { /*……*/ }

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

open class A {
    open fun foo(i: Int = 10) { /*……*/ }
}

class B : A() {
    override fun foo(i: Int) { /*……*/ }  // 不能有默认值。
}

如果在默认参数之后的最后一个参数是 lambda 表达式,那么它既可以作为具名参数在括号内传入,也可以在括号外传入:

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

1.1.3 具名参数

当一个函数参数都有默认值的情况下,我们可以指定只传入某个参数,例如:

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

fun main(){
    reformat(
        "String!",
        upperCaseFirstLetter = false //只传入某个参数
    )
}

1.1.4 返回 Unit 的函数

如果一个函数并不返回有用的值,其返回类型是 Unit。Unit 是一种只有一个值——Unit 的类型。 这个值不需要显式返回:

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

//同样 不声明:Unit也可以
fun printHello(name: String?){
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")

}

1.1.5 单表达式函数

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

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

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

fun double(x: Int) = x * 2

1.1.6 显式返回类型

具有块代码体的函数必须始终显式指定返回类型,除非他们返回 Unit, 在这种情况下指定返回值类型是可选的。

1.1.7 可变数量的参数(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)

1.1.8 中缀表示法

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

  • 它们必须是成员函数或扩展函数。
  • 它们必须只有一个参数。
  • 其参数不得接受可变数量的参数且不能有默认值。
infix fun Int.shl(x: Int): Int { …… }

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)

中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符。 以下表达式是等价的:

  • 1 shl 2 + 3 等价于 1 shl (2 + 3)
  • 0 until n * 2 等价于 0 until (n * 2)
  • xs union ys as Set<> 等价于 xs union (ys as Set<>) 另一方面,中缀函数调用的优先级高于布尔操作符 && 与 ||、is- 与 in- 检测以及其他一些操作符。这些表达式也是等价的
  • a && b xor c 等价于 a && (b xor c)
  • a xor b in c 等价于 (a xor b) in c

a && b xor c 等价于 a && (b xor c) a xor b in c 等价于 (a xor b) in c

1.2 函数作用域

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

1.2.1 局部函数

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

fun dfs(graph: Graph) {
    fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v, visited)
    }

    dfs(graph.vertices[0], HashSet())
}

局部函数可以访问外部函数(闭包)的局部变量。在上例中,visited 可以是局部变量:

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.vertices[0])
}

1.2.2 成员函数

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

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

1.2.3 泛型函数

fun <T> singletonList(item: T): List<T> { /*……*/ }

2.高阶函数与 lambda 表达式

2.1 高阶函数

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

fun <T, R> Collection<T>.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
}

2.2 函数类型

Kotlin 使用类似 (Int) -> String 的函数类型来处理函数的声明: val onClick: () -> Unit = ……。 Unit 返回类型不可省略

(acc: R, nextElement: T) -> Unit

2.3 函数类型实例化

有几种方法可以实例化函数类型:

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

val intFunction: (Int) -> Int = IntTransformer()

2.4 函数类型实例调用

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

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

fun main() {
    //sampleStart
    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)) // 类扩展调用
    //sampleEnd
}

2.5 内联函数

有时使用内联函数可以为高阶函数提供灵活的控制流。

2.6 Lambda 表达式与匿名函数

满足SAM的情况下可直接用Lambda表达式取代,示例: 函数 max 是一个高阶函数,因为它接受一个函数作为第二个参数。

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

2.7 Lambda 表达式语法

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

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
  • lambda 表达式总是括在花括号中。
  • 完整语法形式的参数声明放在花括号内,并有可选的类型标注。
  • 函数体跟在一个 -> 之后。
  • 如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值。 如果将所有可选标注都留下,看起来如下:
val sum = { x: Int, y: Int -> x + y }

2.8 传递末尾的 lambda 表达式

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

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

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

如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略:

run { println("...") }

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

当一个 lambda 表达式只有一个参数时,默认会隐藏为 it

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

2.10 从 lambda 表达式中返回一个值

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

因此,以下两个片段是等价的:

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

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

这一约定连同在圆括号外传递 lambda 表达式一起支持 LINQ-风格) 的代码:

strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }

2.11 下划线用于未使用的变量

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

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

2.12 在 lambda 表达式中解构

在 lambda 表达式中解构是作为解构声明的一部分描述的。

2.13 匿名函数

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

fun(x: Int, y: Int): Int = x + y

匿名函数看起来非常像一个常规函数声明,除了其名称省略了。其函数体既可以是表达式(如上所示)也可以是代码块:

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

2.14 闭包

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

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

3. 内联函数

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

所我们通过内联化lambda表达式消除这类的开销,内联的本质就是将lanbda中的代码解包在调用处,这样就会避免以上说的开销问题。 举个例子:

fun main(){
     inlineFun(5) {
            println("do something...")
            println("do something2...")
        }
}

 inline fun inlineFun(
        b: Int, event: () -> Unit
    ) {
        val a = b + 1
        val b = b + 1
        event()
        val c = 101 + 1
        val d = 101 + 1
    }

通过内联化后,实际上编译器可以解包成

fun main(){
    val a = b + 1
    val b = b + 1
    println("do something...")
    println("do something2...")
    val c = 101 + 1
    val d = 101 + 1
}

为了让编译器这么做,需要使用 inline 修饰符标记 inlineFun() 函数:

 inline fun inlineFun(
        b: Int, event: () -> Unit
    )  

inline 修饰符影响函数本身和传给它的 lambda 表达式:所有这些都将内联到调用处。

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

3.1 noinline

如果不希望解包某个lambda,那么可以用 noinline 修饰符标记不希望内联的函数参数:

 inline fun inlineFun(
        b: Int, noinline event: () -> Unit
    )  

3.2 非局部返回

在 Kotlin 中,只能对==具名==或==匿名函数==使用==正常的、非限定的 return==来退出。 要==退出一个 lambda 表达式,需要使用一个标签==。 在 lambda 表达式内部禁止使用裸 return,因为 lambda 表达式不能使包含它的函数返回:

fun ordinaryFunction(block: () -> Unit) {
    println("hi!")
}
//sampleStart
fun foo() {
    ordinaryFunction {
        return // 错误:不能使 `foo` 在此处返回
    }
}
//sampleEnd
fun main() {
    foo()
}

但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联。因此可以这样:

inline fun inlined(block: () -> Unit) {
    println("hi!")
}
//sampleStart
fun foo() {
    inlined {
        return // OK:该 lambda 表达式是内联的
    }
}
//sampleEnd
fun main() {
    foo()
}

这种返回(位于 lambda 表达式中,但退出包含它的函数)称为非局部返回。 通常会在循环中用到这种结构,其内联函数通常包含:

fun hasZeros(ints: List<Int>): Boolean {
    ints.forEach {
        if (it == 0) return true // 从 hasZeros 返回
    }
    return false
}

3.3 具体化的类型参数

在某些场景下,我们必须在参数中传入类型class对象,但是这样就十分不优雅。比如,我们的Json:

    fun <T> fromJson(json: String, clazz: Class<T>): T {
        return Gson().fromJson(json, clazz)
    }
    
    fun main (){
        formJson("jsonString",Any::class.java)
    }
    

可以看出,十分不优雅,那么我们可以通过内联函数支持的具体化类型来优化以上代码

    inline fun <reified T> fromJson(json: String): T {
        return Gson().fromJson(json, T::class.java)
    }
    
    fun main (){
      val any =  formJson("jsonString")
    }
    

瞬间优雅,只需要用到reified 修饰函数中的泛型即可,前提是内联函数

4. 操作符重载

在 Kotlin 中可以为类型提供预定义的一组操作符的自定义实现。这些操作符具有预定义的符号表示(如 + 或 *)与优先级。为了实现这样的操作符,需要为相应的类型提供一个指定名称的==成员函数==或==扩展函数==。这个类型会成为二元操作符左侧的类型及一元操作符的参数类型。

要重载操作符,需要在方法前添加==operator==关键字:

interface IndexedContainer {
    operator fun get(index: Int)
}

当重载父类的重载操作符方法时,可省略==operator==:

class OrdersList: IndexedContainer {
    override fun get(index: Int) { /*...*/ }   
}

4.1 一元操作

4.1.1 一元前缀操作符

表达式翻译为
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()

这个表是说,当编译器处理例如表达式 +a 时,它执行以下步骤:

  • 确定 a 的类型,令其为 T。
  • 为接收者 T 查找一个带有 operator 修饰符的无参函数 unaryPlus(),即成员函数或扩展函数。
  • 如果函数不存在或不明确,则导致编译错误。
  • 如果函数存在且其返回类型为 R,那就表达式 +a 具有类型 R。

以下是如何重载一元减运算符的示例:

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)

fun main() {
   println(-point)  // 输出“Point(x=-10, y=-20)”
}

4.1.2 递增与递减

表达式翻译为
a++a.inc()
a--a.dec()
inc() 和 dec() 函数必须返回一个值,它用于赋值给使用 ++ 或 -- 操作的变量。它们不应该改变在其上调用 inc() 或 dec() 的对象。

编译器执行以下步骤来解析后缀形式的操作符,例如 a++:

  • 确定 a 的类型,令其为 T。
  • 查找一个适用于类型为 T 的接收者的、带有 operator 修饰符的无参数函数 inc()。
  • 检测函数的返回类型是 T 的子类型。

计算表达式的步骤是:

  • 把 a 的初始值存储到临时存储 a0 中。
  • 把 a0.inc() 结果赋值给 a。
  • 把 a0 作为该表达式的结果返回。

4.2 二元操作

4.2.1 算数运算符

表达式翻译为
a + ba.plus()
a - ba.minus()
a * ba.times()
a / ba.div()
a % ba.rem()
a .. ba.rangeTo()

对于此表中的操作,编译器只是解析成翻译为列中的表达式。

下面是一个从给定值起始的 Counter 类的示例,它可以使用重载的 + 运算符来增加计数:

data class Counter(val dayIndex: Int) {
    operator fun plus(increment: Int): Counter {
        return Counter(dayIndex + increment)
    }
}

4.2.2 in操作符

表达式翻译为
a in ba.contains()
a !in b!a.contains()

对于 in 和 !in,过程是相同的,但是参数的顺序是相反的。

4.2.3 invoke操作符

表达式翻译为
a()a.invoke()
a(i)a.invoke(i)
a(i1,...,in)a.contains(i1,...,in)

4.2.4 索引访问操作符

表达式翻译为
a[i]a.get(i)
a[i1, ……, in]a.get(i1,...,in)
a[i] = ba.set(i,b)
a[i1, ……, in] = ba.set(i1, ……, in, b)

方括号转换为调用带有适当数量参数的 get 和 set。

4.2.5 广义赋值

表达式翻译为
a += ba.plusAssign(b)
a -= ba.minusAssign(b)
a *= ba.timesAssign(b)
a /= ba.divAssign(b)
a %= ba.remAssign(b)

4.2.6 相等与不等操作符

==、!= 操作符只使用函数 equals(other: Any?): Boolean, 可以覆盖它来提供自定义的相等性检测实现。不会调用任何其他同名函数(如 equals(other: Foo))。

4.2.7 比较操作符

所有的比较都转换为对 compareTo 的调用,这个函数需要返回 Int 值