函数对于编程语言来说是极其重要的一个组成部分,函数可以视为是程序的执行,是真正活的代码,为啥呢?因为运行的时候你必须要执行一个函数,一般从主函数入口,开始一个套一个的函数调用。函数更能体现程序的运行。特别是近些年函数式编程的编程范式开始广泛流行,让函数的地位再次在各种语言中都得到了极大的进升。对于任何一门编程语言,如果没有学好函数,那就相当于没有学,今天就要深入的学习一下Kotlin中的函数。
首先来区分一下,什么是函数什么是方法,函数是编程语言中的一级对象,地位等同于其他Type,函数可以声明在任何地方:顶层(即在任何类任何方法的外面),类里面,另一个函数里面等。一般支持函数式编程语言更喜欢用函数。声明在类里面的函数叫作成员函数,但更准确的说是方法。比如像纯OO的编程语言Java就只会说方法(Method),而像函数式编程语言(Kotlin/Scala/Groovy)喜欢说函数。
函数的基本使用
函数(Functions)在Kotlin中的一级对象,这就意味着它能像其他类型那样,可以声明变量,可以当作参数传递,可以在函数内部定义,先从基本的使用开始。
函数的声明与定义
用关键字fun来声明函数,然后是函数名字,参数列表,返回值和函数体:修饰符 fun 函数名(参数列表) :返回类型 {函数体}
- 修饰符,对于类的成员函数才有,一般是权限open/private
- fun,用于声明这是一个函数的关键字
- 函数名,就像变量名一样,是函数的名字
- (参数列表),要用括号约束起来,就是变量的声明,多个要用逗号分隔
- :返回类型,注意冒号,也即是函数返回值的类型,如果很明显类型可以推断出来时,就可以省略
- {函数体},也即函数的真实定义部分,想要执行的一些语句
如:
fun double(x: Int): Int {
return x + x
}
这就是一个标准的函数。
函数的使用
函数的使用有三种,一是调用,另一种是声明变量,再有就是当作参数(这其实是在定义一个变量,然后当作参数)。
函数的调用
函数都是表达式,都有返回值,但可以不用管返回值,调用函数的时候用括号来标识,比如前面的函数double,可以这样来调用:
val dx = double(5)
这是函数最为常用的使用方法,因为程序最终要执行,所以所有的函数最终都是要被调用的。
声明函数变量
前面说了,函数是一级类型,它可以像其他类型那样去定义变量,比如前面的double也可以这来写:
val myDouble = {x: Int -> x + x}
println(myDouble(6))
这里的myDouble就是一个函数变量,它的函数类型与前面的double是一样的,它是一个变量,要想执行它的函数体要加括号。
把函数当作参数
这里会涉及高阶函数,高阶函数就是涉及函数中的函数,主要体现在函数的参数或者返回值也是一个函数。比如数组和集合的过滤(filter)和遍历(forEach)里面的参数就是一个函数:
val asc = arrayOf(1, 2, 3, 4, 5)
asc.filter({(it and 0x01) == 0}).map(myDouble).forEach({ println(it) })
// output-> 4, 8
因为把函数当作参数传递时都涉及函数的类型定义,而一般情况下用lambda是最方便的,先有个印象,后面会详细讲解。
参数
函数的参数还有两种比较有用的变体,称之为命名参数和默认值,这两个通常会一起使用。
命名参数
当一个函数的参数比较多时,那么在调用时想要传递参数就比较蛋疼,特别是还有相同类型的参数的时候,一片混乱,比如:
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。
这时就可以使用命名参数来缓解了,命名参数,就是在调用函数,传递参数的时候,指定参数的名字,即就是在声明函数时参数的名字,用以指定具体参数,然后这时就可不用管参数的相对顺序了,比如上面的函数也可以这样调用:
log(event = "Named arguments",
tag = "func",
source = "Elegant",
amount = 5,
persist = false,
price = 100f
)
不会出错,而且可读性大大加强。但需要注意的是,如果要使用命名参数,就要保持一致性,给所有的参数都要命名。所以,当参数比较多的时候还是比较蛋疼,这时就需要用到参数默认值了。
参数默认值
默认值也即是在声明参数的时候指定一个默认值,在调用的时候可以省略这个参数了,比如:
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)。当然了,两个参数都传也可以的:
foo(3, 5)
afoo(3, 5)
所以要把默认值和命名参数结合起来才能发挥最大的价值:
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个以内),那么就把默认参数往后放,调用的时候也可以不用命名参数,直接省略默认参数就好;如果参数比较多,也要把默认参数往后放,在调用的时候尽可能的使用命名参数。
返回值
函数的返回值是在参数列表之后,函数体之前用冒号加类型来声明。
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
// `return Unit` or `return` is optional
}
如果函数没有返回值就用Unit来声明,相当于Java中的void,但更多的时候是可以省略的:
fun printHello(name: String?) { ... }
当函数体只有一个表达式的时候,这个时候可以省略掉函数体,而把表达式直接写在函数声明的后面,用赋值符=来连接,如前面的double也可以这样写:
fun double(x: Int): Int = x + x
这个时候,因为函数体只有一个表达式,所以返回类型很容易推断出来,意味着这时返回类型的声明也可以省略掉:
fun double(x: Int) = x + x
这会让代码非常的简洁,又不失可读性。
解构返回
Kotlin的函数只能有一个返回值,代表某一个类型的一个变量,如果想有多个返回值,就需要用复杂的类型,比如同一类型的多个有规律的变量可能就要用集合,如数组列表等。但如果类型不同,但逻辑上有关系的2个到3个值,如果想要一起返回,就需要用到组合类型如Pair和Triple,Pair可以把两个不同类型的变量组合成一个对象,Triple可以把三个不同类型的变量组合成一个对象,这样就可以在函数中返回了。
fun nameAge() = Pair("Alex", 50)
fun fullName() = Triple("Donald", "Jonh", "Trump")
对于函数的调用者也很麻烦,要先声明Pair或者Triple对象,然后再拆解,比如这样:
val pna = nameAge()
println("Name ${pna.first}, age ${pna.second}")
这显然比较笨拙,不够简洁。在Kotlin中有更好的做法,可以在函数调用的时候,对返回值进行拆解,称之为解构,如下写法与上面是一样的:
val (name, age) = nameAge()
println("Name $name, age $age")
而且,如果只对组合中的某几个感兴趣,可以把不想要的变量用下划线_(underscore)来表示,比如说:
val (firstName, _, lastName) = fullName()
println("This is $firstName $lastName")
尾部lambda参数传递
前面说了函数可以作为参数传递给其他函数,但我们在使用的时候,一般会直接把一个lambda传递进去,比如说:
fun execute(a: Int, f: (Int)->Int): Int {
if (a < 0) {
return -1
}
return f(a)
}
调用的时候,可以这样:
execute(3, { it * it })
但更建议的方式是把lambda放到函数调用之外:
execute(5) { it + it }
再比如像集合的函数式写法,通常也只传递一个lambda,这时一般都写在函数调用之外,并且当目标函数没有其他参数时也即除了要传入的lambda外无其他参数时,代表函数调用的括号也可以省略:
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,通常用于把函数当作 参数传递给高阶函数时使用。如:
list.forEach(fun (item) { println(item) })
但,因为对lambda支持的非常好,所以凡事用到匿名函数的地言都可以用lambda来替换(当然了,lambda也是匿名函数的一个实现方式)。匿名函数的一个用处就 可以加代码标签(label),以方便终止语句的精准控制,可以参见另一篇文章中关于局部控制的讨论,可能也只有这个地方适用匿名函数,其他情况都应该直接用lambda。
内部函数
就是定义在另外一个函数内部的函数,通常用作于传递参数或者返回值,如:
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,比如:
val product = items.fold(1) { acc, e -> acc * e }
run { println("...") }
隐式参数
如果lambda表达式只有一个参数,那么这个参数也可以省略,只写函数体就可以,并且可以用隐式参数it,比如:
ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'
val asc = IntArray(5) { it * it }
lambda的返回值
lambda力求简洁,所以函数体的最后一个表达式的值即是此lambda的返回值,一般不用显式的return:
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:
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
丢弃参数
有时候,参数有多个,但可能并不会全都使用,仅使用了其中一个,这时不使用的参数就可以用下划线_(underscore)来代替,以表示这个参数不会被使用:
map.forEach { (_, value) -> println("$value!") }
Kotlin的lambda可以写出非常简洁的函数式链式语句,一气呵成可读性又非常的好,比如:
val headers = fetchHeaders()
headers.filter { it.length == 5 }
.sortedBy { it }
.map { it.uppercase() }
.forEach { println(it) }