稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Understanding Kotlin Functions

函数对于编程语言来说是极其重要的一个组成部分,函数可以视为是程序的执行,是真正活的代码,为啥呢?因为运行的时候你必须要执行一个函数,一般从主函数入口,开始一个套一个的函数调用。函数更能体现程序的运行。特别是近些年函数式编程的编程范式开始广泛流行,让函数的地位再次在各种语言中都得到了极大的进升。对于任何一门编程语言,如果没有学好函数,那就相当于没有学,今天就要深入的学习一下Kotlin中的函数。

首先来区分一下,什么是函数什么是方法,函数是编程语言中的一级对象,地位等同于其他Type,函数可以声明在任何地方:顶层(即在任何类任何方法的外面),类里面,另一个函数里面等。一般支持函数式编程语言更喜欢用函数。声明在类里面的函数叫作成员函数,但更准确的说是方法。比如像纯OO的编程语言Java就只会说方法(Method),而像函数式编程语言(Kotlin/Scala/Groovy)喜欢说函数。

函数的基本使用

函数(Functions)在Kotlin中的一级对象,这就意味着它能像其他类型那样,可以声明变量,可以当作参数传递,可以在函数内部定义,先从基本的使用开始。

函数的声明与定义

关键字fun来声明函数,然后是函数名字,参数列表,返回值函数体修饰符 fun 函数名(参数列表) :返回类型 {函数体}

  • 修饰符,对于类的成员函数才有,一般是权限open/private
  • fun,用于声明这是一个函数的关键字
  • 函数名,就像变量名一样,是函数的名字
  • (参数列表),要用括号约束起来,就是变量的声明,多个要用逗号分隔
  • :返回类型,注意冒号,也即是函数返回值的类型,如果很明显类型可以推断出来时,就可以省略
  • {函数体},也即函数的真实定义部分,想要执行的一些语句

如:

1
2
3
fun double(x: Int): Int {
     return x + x
}

这就是一个标准的函数。

函数的使用

函数的使用有三种,一是调用,另一种是声明变量,再有就是当作参数(这其实是在定义一个变量,然后当作参数)。

函数的调用

函数都是表达式,都有返回值,但可以不用管返回值,调用函数的时候用括号来标识,比如前面的函数double,可以这样来调用:

1
val dx = double(5)

这是函数最为常用的使用方法,因为程序最终要执行,所以所有的函数最终都是要被调用的。

声明函数变量

前面说了,函数是一级类型,它可以像其他类型那样去定义变量,比如前面的double也可以这来写:

1
2
val myDouble = {x: Int -> x + x}
println(myDouble(6))

这里的myDouble就是一个函数变量,它的函数类型与前面的double是一样的,它是一个变量,要想执行它的函数体要加括号。

把函数当作参数

这里会涉及高阶函数,高阶函数就是涉及函数中的函数,主要体现在函数的参数或者返回值也是一个函数。比如数组和集合的过滤(filter)和遍历(forEach)里面的参数就是一个函数:

1
2
3
val asc = arrayOf(1, 2, 3, 4, 5)
asc.filter({(it and 0x01) == 0}).map(myDouble).forEach({ println(it) })
// output-> 4, 8

因为把函数当作参数传递时都涉及函数的类型定义,而一般情况下用lambda是最方便的,先有个印象,后面会详细讲解。

参数

函数的参数还有两种比较有用的变体,称之为命名参数和默认值,这两个通常会一起使用。

命名参数

当一个函数的参数比较多时,那么在调用时想要传递参数就比较蛋疼,特别是还有相同类型的参数的时候,一片混乱,比如:

1
2
3
4
5
6
7
8
fun log(tag: String, event: String, source: String, amount: Int, price: Float, persist: Boolean): Unit {
    println("$tag, $event, $amount $price")
    if (persist) {
        // write to file
    }
}

log("func", "Function arguments", "Hard way", 5, 2.3f, false)

这样调用,参数太多了,并且相同类型的有三个,这三个极容易传错,而且因为类型检查 不会报错,可能会引发极难调试的bug。

这时就可以使用命名参数来缓解了,命名参数,就是在调用函数,传递参数的时候,指定参数的名字,即就是在声明函数时参数的名字,用以指定具体参数,然后这时就可不用管参数的相对顺序了,比如上面的函数也可以这样调用:

1
2
3
4
5
6
7
   log(event = "Named arguments",
        tag = "func",
        source = "Elegant",
        amount = 5,
        persist = false,
        price = 100f
    )

不会出错,而且可读性大大加强。但需要注意的是,如果要使用命名参数,就要保持一致性,给所有的参数都要命名。所以,当参数比较多的时候还是比较蛋疼,这时就需要用到参数默认值了。

参数默认值

默认值也即是在声明参数的时候指定一个默认值,在调用的时候可以省略这个参数了,比如:

1
2
3
4
5
6
7
8
9
10
fun foo(x: Int, y: Int = 0): Int {
    return x + y
}

fun afoo(x: Int = 0, y: Int): Int {
    return x - y
}

foo(3)
afoo(y = 5)

注意,如果默认参数是最后一个参数,那么可以直接省略它,如示例中的foo(3),但如果默认参数不是最后一个,想省略的话,必须要用命名参数,如afoo(y = 5)。当然了,两个参数都传也可以的:

1
2
foo(3, 5)
afoo(3, 5)

所以要把默认值和命名参数结合起来才能发挥最大的价值:

1
2
3
4
5
6
7
8
9
10
11
fun log(tag: String, event: String, source: String = "Elegant", amount: Int = 0, price: Float, persist: Boolean = false): Unit {
    println("$tag, $event, $amount $price")
    if (persist) {
        // write to file
    }
}

    log(event = "Named arguments",
        tag = "func",
        price = 100f
    )

把握一下使用原则:如果参数不多(4个以内),那么就把默认参数往后放,调用的时候也可以不用命名参数,直接省略默认参数就好;如果参数比较多,也要把默认参数往后放,在调用的时候尽可能的使用命名参数。

返回值

函数的返回值是在参数列表之后,函数体之前用冒号加类型来声明。

1
2
3
4
5
6
7
fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")
    // `return Unit` or `return` is optional
}

如果函数没有返回值就用Unit来声明,相当于Java中的void,但更多的时候是可以省略的:

1
fun printHello(name: String?) { ... }

函数体只有一个表达式的时候,这个时候可以省略掉函数体,而把表达式直接写在函数声明的后面,用赋值符=来连接,如前面的double也可以这样写:

1
fun double(x: Int): Int = x + x

这个时候,因为函数体只有一个表达式,所以返回类型很容易推断出来,意味着这时返回类型的声明也可以省略掉:

1
fun double(x: Int) = x + x

这会让代码非常的简洁,又不失可读性。

解构返回

Kotlin的函数只能有一个返回值,代表某一个类型的一个变量,如果想有多个返回值,就需要用复杂的类型,比如同一类型的多个有规律的变量可能就要用集合,如数组列表等。但如果类型不同,但逻辑上有关系的2个到3个值,如果想要一起返回,就需要用到组合类型如Pair和Triple,Pair可以把两个不同类型的变量组合成一个对象,Triple可以把三个不同类型的变量组合成一个对象,这样就可以在函数中返回了。

1
2
3
fun nameAge() = Pair("Alex", 50)

fun fullName() = Triple("Donald", "Jonh", "Trump")

对于函数的调用者也很麻烦,要先声明Pair或者Triple对象,然后再拆解,比如这样:

1
2
val pna = nameAge()
println("Name ${pna.first}, age ${pna.second}")

这显然比较笨拙,不够简洁。在Kotlin中有更好的做法,可以在函数调用的时候,对返回值进行拆解,称之为解构,如下写法与上面是一样的:

1
2
val (name, age) = nameAge()
println("Name $name, age $age")

而且,如果只对组合中的某几个感兴趣,可以把不想要的变量用下划线_(underscore)来表示,比如说:

1
2
val (firstName, _, lastName) = fullName()
println("This is $firstName $lastName")

尾部lambda参数传递

前面说了函数可以作为参数传递给其他函数,但我们在使用的时候,一般会直接把一个lambda传递进去,比如说:

1
2
3
4
5
6
fun execute(a: Int, f: (Int)->Int): Int {
    if (a < 0) {
        return -1
    }
    return f(a)
}

调用的时候,可以这样:

1
execute(3, { it * it })

但更建议的方式是把lambda放到函数调用之外:

1
execute(5) { it + it }

再比如像集合的函数式写法,通常也只传递一个lambda,这时一般都写在函数调用之外,并且当目标函数没有其他参数时也即除了要传入的lambda外无其他参数时,代表函数调用的括号也可以省略:

1
2
3
4
val nums = arrayOf(1, 2, 3, 4, 5)
nums.filter { it and 0x01 == 0 } // 等同于filter({ it and 0x01 == 0 })
        .map { it * it } // 等同于map({ it * it })
        .forEach { println(it) } // 等同于forEach({ println(it) })

这样写非常的简洁,但会牺牲一些可读性,因为花样多了,就会比较难识别出来函数的声明与函数的调用,甚至有时候会分不清函数与普通的变量。所以,识别函数调用有两种方式,一是看有没有括号,另外就看有没有尾部lambda

匿名函数

匿名函数就是不指定函数的名字,但保留关键字fun,通常用于把函数当作 参数传递给高阶函数时使用。如:

1
list.forEach(fun (item) { println(item) })

但,因为对lambda支持的非常好,所以凡事用到匿名函数的地言都可以用lambda来替换(当然了,lambda也是匿名函数的一个实现方式)。匿名函数的一个用处就 可以加代码标签(label),以方便终止语句的精准控制,可以参见另一篇文章中关于局部控制的讨论,可能也只有这个地方适用匿名函数,其他情况都应该直接用lambda。

内部函数

就是定义在另外一个函数内部的函数,通常用作于传递参数或者返回值,如:

1
2
3
4
5
6
fun foo(): ()->Unit {
   fun bar() {
      println("Func bar inside fun foo")
   }
   return bar
}

事实上,随处可见的lambda都可以算上是内部函数。

高阶函数

高阶函数就是函数的函数,也就是说函数的参数或者返回值是一个函数的函数,也即把函数像其他类型那样使用。函数在Kotlin中一级类型(first class type),因此从语义层面支持了函数式编程范式,当然也就支持了高阶函数以及lambdas。比如像集合的操作filter/map/fold都是高阶函数,因为它们接受一个函数作为参数。

函数类型

高阶函数是把函数作为参数或者返回值,但显然并不是所有的函数都能当作高阶函数的参数或者返回值,换句话说,函数本身其实也是有类型之别的,两个函数不见得就是一样的。函数是用来针对其参数,然后在函数体内进行一些运算最终返回一个值,所以区分不同的函数最关键的是输入参数和返回值,与其名字其实没有关系,因此输入参数一致,返回值一致就可以视为同一种函数。

函数的类型用参数和返回值来表示,如(A, B) -> R形式,A和B是参数,R是返回值,需要注意的是括号不能省略,常见的具体形式有:

  • () -> Unit 无参数无返回值
  • () -> R 无参数有返回值
  • (A) -> Unit 有一个参数,无返回值
  • (A) -> R 一个参数,一个返回值
  • (A, B) -> Unit 两个参数,无返回值
  • (A, B) -> R 两个参数,一个返回值

函数的类型与方法签名类似(method signature),代表着某一类的函数。在高阶函数的函数参数或者返回函数就需要用函数类型来声明。

实例化一个函数类型

有很多种途径可以实例化一个函数类型,比较常见的有:

  • 通过lambda表达式,如{ a, b -> a + b },这就是一个函数类型(A, B) -> R的实例
  • 匿名函数,如fun(a: Int, b: Int): Int { return if (a > 0 && b > 0) a + b else -1 }
  • 引用现存的某一个函数,函数签名(参数相同,返回值相同)就视为同一种函数类型,那么已定义好的函数中有能匹配的就可以直接引用过来,顶级函数和构造函数用::来引用,类成员函数用类名::来引用,如::isOdd, String::toInt, ::Tripple

lambda表达式

就是匿名隐式函数体,匿名是不用指定函数的名字,连参数的类型和返回值的类型也都省略,有时甚至连参数都可以省略,只有一个函数体,是最为简洁的一种函数定义方式,通常用于传递给高阶函数的参数,lambda力求简洁,所以但凡能推断出来的都可以省略。最简洁的lambda只有函数体,如val asc = IntArray(5) { it * it } // 创建一个长度为5的整数数组并初始化为[0,1,4,9,16]。

lambda的形式是{ A, B -> expressions },外面的花括号不可省略,这是lambda的标识,然后是参数列表,->用于分隔参数和函数体,除了函数体,其余的都可以省略掉,只要能推断出来。

Trailing lambdas(尾部lambda)

这个前面讲过了,再复习一下,当一个函数的最后一个参数是一个函数时,就可以在函数的调用外部写lambda,比如:

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

run { println("...") }

隐式参数

如果lambda表达式只有一个参数,那么这个参数也可以省略,只写函数体就可以,并且可以用隐式参数it,比如:

1
2
3
ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'

val asc = IntArray(5) { it * it }

lambda的返回值

lambda力求简洁,所以函数体的最后一个表达式的值即是此lambda的返回值,一般不用显式的return:

1
2
3
4
5
6
7
8
ints.filter { it > 0 } // boolean result of 'it > 0' is returned

val asc = IntArray(5) { it * it } // it * it is the return

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

如果要用显式的return语句,要注意scope,在这篇文章有深入讨论,用隐式label来限定scope:

1
2
3
4
ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}

丢弃参数

有时候,参数有多个,但可能并不会全都使用,仅使用了其中一个,这时不使用的参数就可以用下划线_(underscore)来代替,以表示这个参数不会被使用:

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

Kotlin的lambda可以写出非常简洁的函数式链式语句,一气呵成可读性又非常的好,比如:

1
2
3
4
5
val headers = fetchHeaders()
headers.filter { it.length == 5 }
  .sortedBy { it }
  .map { it.uppercase() }
  .forEach { println(it) }

未完待续

函数是Kotlin中最最重要的一个类型,博大精深,用法多样,但万变不离其宗,重点理解函数就是行为,是可以运行的一组语句和表达式的小集合;区分开函数的声明和定义就可以了。

Kotlin的函数类型很多,这里只介绍了一些基础,后面将会继续介绍一些高级的函数如内联函数(inline functions),infix notation以及Extensions和Scope functions。

参考资料

Comments