1 函数定义
函数是用来运行代码的载体,可以在一个函数里编写很多行代码,当运行这个函数时,函数中的所有代码会全部运行。
Kotlin
中的函数,语法规则如下:
fun methodName(param1: Int, param2: Int): Int {
return 0
}
fun
(function
的简写)是定义函数的关键字,无论定义什么函数,都一定要使用fun
来声明。
在fun
后面的是函数名,这个就没有什么要求了,但是良好的编程习惯是函数名最好要有一定的意义,能表达这个函数的作用是什么。函数名后面紧跟着一对括号,里面可以声明该函数接收什么参数,参数的数量可以是任意多个,例如上述示例就表示该函数接收两个Int
类型的参数。参数的声明格式是:参数名: 参数类型
, 其中参数名也是可以随便定义的,这一点和函数名类似。如果不想接收任何参数,那么写一对空括号就可以了。
参数括号后面的那部分是可选的,用于声明该函数会返回什么类型的数据,上述示例就表示该函数会返回一个Int
类型的数据。如果函数不需要返回任何数据,会默认被当成返回Unit
类型,可以省略不写。
对于Unit
类型可以暂时把它当作Java
中的void
,不过它们是不同的,Unit
是一个类型,而void
只是一个关键字。
对于有返回类型的函数,即使Kotlin
在很大程度上支持了类型推导,也不意味着就可以不声明函数返回值类型:
在以上的例子中,因为没有声明返回值的类型,函数会默认被当成返回Unit
类型,然而实际上返回的是Int
,所以编译就会报错。这种情况下必须显式声明返回值类型。
最后两个大括号之间的内容就是函数体了,可以在这里编写一个函数的具体逻辑。
接下来按照上述定义函数的语法规则来定义一个有意义的函数,如下所示:
fun largeNumber(num1: Int, num2: Int): Int {
return max(num1, num2)
}
largerNumber()
函数已经编写好了,接下来在main()
函数中调用这个函数,代码如下所示:
fun largeNumber(num1: Int, num2: Int): Int {
return max(num1, num2)
}
fun main() {
val a = 37
val b = 40
val value = largeNumber(a, b)
println("larger number is $value") // larger number is 40
}
当一个函数中只有一行代码时,Kotlin
允许我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。 比如largerNumber()
函数就只有一行代码,于是可以将代码简化成如下形式:
fun largeNumber(num1: Int, num2: Int) = max(num1, num2)
Kotlin
支持这种用单行表达式与等号的语法来定义函数,叫作表达式函数体,作为区分,普通的函数声明则可叫作代码块函数体。 在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。
再来一段递归程序试试看:
if
在这里不同寻常的用法——没有return
关键字。在Kotlin
中,if
是一 个表达式,它的返回值类型是各个逻辑分支的相同类型或公共父类型。
我们发现,当前编译器并不能针对递归函数的情况推导类型。由于像Kotlin
、Scala
这类语言支持子类型和继承,这导致类型系统很难做到所谓的全局类型推导。
所以,在一些诸如递归的复杂情况下,即使用表达式定义函数,我们也必须显式声明类型,才能让程序正常工作:
fun foo(n: Int): Int = if (n == 0) 1 else n * foo(n - 1)
此外,如果这是一个表达式定义的接口方法,显式声明类型虽然不是必需的,但可以在很大程度上提升代码的可读性。
2 函数的参数默认值
次构造函数在Kotlin
中很少用,因为Kotlin
提供了给函数设定参数默认值的功能,它在很大程度上能够替代次构造函数的作用。
具体来讲,我们可以在定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。给参数设定默认值的方式也很简单,代码如下所示:
fun printParams(num: Int, str: String = "Hello") {
println("num is $num, str is $str")
}
可以看到,这里给printParams()
函数的第二个参数设定了一个默认值,这样当调用printParams()
函数时,可以选择给第二个参数传值,也可以选择不传,在不传的情况下就会自动使用默认值。在main()
函数中调用一下printParams()
函数来进行测试,代码如下:
fun main() {
printParams(123)
}
// num is 123, str is Hello
可以看到,在没有给第二个参数传值的情况下,printParams()
函数自动使用了参数的默认值。
上面这个例子比较理想化,因为正好是给最后一个参数设定了默认值,现在将代码改成给第一个参数设定默认值,如下所示:
fun printParams(num: Int = 100, str: String) {
println("num is $num, str is $str")
}
这时如果想让num
参数使用默认值该怎么办呢?模仿刚才的写法肯定是行不通的,因为编译器会认为我们想把字符串赋值给第一个num
参数,从而报类型不匹配的错误,如图所示:
为了解决这个问题,Kotlin
提供了另外一种机制,就是可以通过键值对的方式来传参,从而不必像传统写法那样按照参数定义的顺序来传参。 比如调用printParams()
函数,还可以这样写:
printParams(str = "world", num = 123)
此时哪个参数在前哪个参数在后都无所谓,Kotlin
可以准确地将参数匹配上。而使用这种键值对的传参方式之后,就可以省略num
参数了,代码如下:
fun main() {
printParams(str = "world")
}
fun printParams(num: Int = 100, str: String) {
println("num is $num, str is $str")
}
// num is 100, str is world
对于以下次构造函数的代码:
open class Person(val name: String, val age: Int)
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
constructor(name: String, age: Int) : this("", 0, name, age)
constructor() : this("", 0)
}
上述代码中有一个主构造函数和两个次构造函数,次构造函数在这里的作用是提供了使用更少参数来对Student
类进行实例化的方式。无参的次构造函数会调用两个参数的次构造函数,并将这两个参数赋值成初始值。两个参数的次构造函数会调用4
个参数的主构造函数,并将缺失的两个参数也赋值成初始值。
这种写法在Kotlin
中其实是不必要的,因为我们完全可以通过只编写一个主构造函数,然后给参数设定默认值的方式来实现,代码如下:
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) : Person(name, age) {
}
在给主构造函数的每个参数都设定了默认值之后,我们就可以使用任何传参组合的方式来对Student
类进行实例化,当然也包含了刚才两种次构造函数的使用场景。
2 函数的类型
在Kotlin
中,函数类型的格式非常简单,举个例子:
(Int) -> Unit
从中发现,Kotlin
中的函数类型声明需遵循以下几点:
- 通过
->
符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型; - 必须用一个括号来包裹参数类型;
- 返回值类型即使是
Unit
,也必须显式声明;
如果是一个没有参数的函数类型,参数类型部分就用()
来表示:
() -> Unit
如果是多个参数的情况,就需要用逗号来进行分隔,如:
(Int, String) -> Unit
此外,Kotlin
还支持为声明参数指定名字,如下所示:
(errorCode: Int, errMsg: String) -> Unit
如果errMsg
在某种情况下可空,那么就可以如此声明类型:
(errorCode: Int, errMsg: String?) -> Unit
如果该函数类型的变量也是可选的话,还可以把整个函数类型变成可选:
((errorCode: Int, errMsg: String?) -> Unit)?
高阶函数还支持返回另一个函数,所以还可以这么做:
(Int) -> ((Int) -> Unit)
这表示传入一个类型为Int
的参数,然后返回另一个类型为(Int) -> Unit
的函数。简化它的表达,可以把后半部分的括号给省略:
(Int) -> Int -> Unit
需要注意的是,以下的函数类型则不同,它表示的是传入一个函数类型的参数,再返回一个Unit
:
((Int) -> Int) -> Unit
3 方法和成员引用
Kotlin
存在一种特殊的语法,通过两个冒号来实现对于某个类的方法或成员进行引用。 以上面的代码为例,假如有一个CountryTest
类的对象实例countryTest
,如果要引用它的isBigEuropeanCountry
方法,就可以这么写:
countryTest::isBigEuropeanCountry
此外,还可以直接通过这种语法,来定义一个类的构造方法引用变量:
class Book(val name: String)
fun main() {
val getBook = ::Book
println(getBook("Dive into Kotlin").name) // Dive into Kotlin
}
getBook
的类型为(name: String) -> Book
。类似的道理,如果要引用某个类中的成员变量,如Book.name
,就可以这样引用:
Book::name
以上创建的Book::name
的类型为(Book) -> String
。当我们在对Book
类对象的集合应用一些函数式API
的时候,这会显得格外有用,比如:
fun main() {
val bookNames = listOf(
Book("Thinking in Java"),
Book("Dive into Kotlin")
).map(Book::name)
println(bookNames) // [Thinking in Java, Dive into Kotlin]
}
4 高阶函数
高阶函数:以其他函数作为参数或返回值的函数。
举例来说,Shaw因为旅游喜欢上了地理,然后他建了一个所有国家的数据库,设计了一个CountryApp
类对国家数据进行操作。Shaw偏好欧洲的国家,于是他设计了一个程序来获取欧洲的所有国家:
data class Country(val name: String, val continient: String, val population: Int)
class CountryApp {
fun filterCountries(countries: List<Country>): List<Country> {
val res = mutableListOf<Country>()
for (c in countries) {
if (c.continient == "EU") {
res.add(c)
}
}
return res
}
}
后来,Shaw对非洲也产生了兴趣,于是他又改进了上述方法的实现,支持根据具体的洲来筛选国家:
class CountryApp {
fun filterCountries(countries: List<Country>, continient: String): List<Country> {
val res = mutableListOf<Country>()
for (c in countries) {
if (c.continient == continient) {
res.add(c)
}
}
return res
}
}
以上的程序具备了一定的复用性。然而,Shaw的地理知识越来越丰富了,他想对国家的特点做进一步的研究,比如筛选具有一定人口规模的国家,于是代码又变成下面这个样子:
class CountryApp {
fun filterCountries(countries: List<Country>, continient: String, population: Int): List<Country> {
val res = mutableListOf<Country>()
for (c in countries) {
if (c.continient == continient && c.population > population) {
res.add(c)
}
}
return res
}
}
新增了一个population
的参数来代表人口(单位:万)。如果按照现有的设计,更多的筛选条件会作为方法参数而不断累加,而且业务逻辑也高度耦合。
解决问题的核心在于对filterCountries
方法进行解耦,能否把所有的筛选逻辑行为都抽象成一个参数呢?传入一个类对象是一种解决方法,我们可以根据不同的筛选需求创建不同的子类,它们都各自实现了一个校验方法。然而,了解到Kotlin
是支持高阶函数的,理论上可以把筛选的逻辑变成一个方法来传入,这种思路更加简单。代码如下所示:
class CountryTest {
fun isBigEuropeanCountry(country: Country): Boolean {
return country.continient == "EU" && country.population > 10000
}
}
调用isBigEuropeanCountry
方法就能够判断一个国家是否是一个人口超过1
亿的欧洲国家。然而,怎样才能把这个方法变成filterCountries
方法的一个参数呢?要实现这一点要先解决以下两个问题:
- 方法作为参数传入,必须像其他参数一样具备具体的类型信息;
- 需要把
isBigEuropeanCountry
的方法引用当作参数传递给filterCountries
;
方法作为参数传入,优化filterCountries
方法的参数声明:
fun filterCountries(countries: List<Country>, test: (Country) -> Boolean): List<Country> {
val res = mutableListOf<Country>()
for (c in countries) {
if (test(c)) {
res.add(c)
}
}
return res
}
那么,如果将isBigEuropeanCountry
方法传递给filterCountries
呢?直接把isBigEuropeanCountry
当参数肯定不行,因为函数名并不是一个表达式,不具有类型信息,它在带上括号、被执行后才存在值。可以看出,我们需要的是一个单纯的方法引用表达式,用它在filterCountries
内部来调用参数:
fun main() {
val countryApp = CountryApp()
val countryTest = CountryTest()
val countries = ...
countryApp.filterCountries(countries, countryTest::isBigEuropeanCountry)
}
5 匿名函数
对于使用CountryTest
类,仍算不上一种很好的方案。因为每增加一个需求,都需要在类中专门写一个新增的筛选方法。然而Shaw的需求很多都是临时性的,不需要被复用。Shaw觉得这样还是比较麻烦,他打算用匿名函数对程序做进一 步的优化。
Kotlin
支持在缺省函数名的情况下,直接定义一个函数。所以isBigEuropeanCountry
方法我们可以直接定义为:
fun isBigEuropeanCountry(country: Country): Boolean {
return country.continient == "EU" && country.population > 10000
}
直接调用filterCountries
,代码如下所示:
countryApp.filterCountries(countries, fun(country: Country): Boolean {
return country.continient == "EU" && country.population > 10000
})
这个时候都不需要CountryTest
这个类了,代码更加简洁了。
6 Lambda
表达式
Lambda
表达式可以理解成简化表达后的匿名函数,实质上它就是一种语法糖。
对于上一节中filterCountries
方法的匿名函数,会发现:
-
fun(country: Country)
显得比较啰唆,因为编译器会推导类型,所以只需要一个代表变量的country
就行了; -
return
关键字也可以省略,这里返回的是一个有值的表达式; - 模仿函数类型的语法,我们可以用
->
把参数和返回值连接在一起;
因此,简化后的表达就变成了这个样子:
countryApp.filterCountries(countries, { country -> country.continient == "EU" && country.population > 10000 })
这个就是Lambda
表达式,它与匿名函数一样,是一种函数字面量。 现在用Lambda
的形式来定义一个加法操作:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
由于Kotlin
支持类型推导,可以采用两种方式进行简化:
val sum = { x: Int, y: Int -> x + y }
// 或者
val sum: (Int, Int) -> Int = { x, y -> x + y }
Lambda
表达式的语法:
- 一个
Lambda
表达式必须通过{}
来包裹; - 如果
Lambda
声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda
变量就可以省略函数类型声明; - 如果
Lambda
变量声明了函数类型,那么Lambda
的参数部分的类型就可以省略;
此外,如果Lambda
表达式返回的不是Unit
,那么默认最后一行表达式的值类型就是返回值类型, 如:
val foo = { x: Int ->
val y = x + 1
y
}
fun main() {
println(foo(1)) // 2
}
如果用fun
关键字来声明Lambda
表达式,代码如下所示:
fun foo(int: Int) = {
print(int)
}
fun main() {
listOf(1, 2, 3).forEach { foo(it) }
}
it
是Kotlin
简化Lambda
表达的一种语法糖,叫作单个参数的隐式名称,代表了这个Lambda
所接收的单个参数。 这里的调用等价于:
listOf(1, 2, 3).forEach { item -> foo(item) }
默认情况下,可以直接用it
来代表item
,而不需要用item->
进行声明。
为什么会这样呢?把foo
函数用转化成Java
代码:
@NotNull
public static final Function0 foo(final int var0) {
return (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
int var1 = var0;
System.out.print(var1);
}
});
}
Kotlin
在JVM
层设计了Function
类型(Function0
、Function1
…Function22
)来兼容Java
的Lambda
表达式,其中的后缀数字代表了Lambda
参数的数量,如以上的foo
函数构建的其实是一个无参Lambda
,所以对应的接口是Function0
,如果有一个参数那么对应的就是Function1
。它在源码中是如下定义的:
package kotlin.jvm.functions
/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}
/** A function that takes 3 arguments. */
public interface Function3<in P1, in P2, in P3, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3): R
}
/** A function that takes 4 arguments. */
public interface Function4<in P1, in P2, in P3, in P4, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4): R
}
...
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}
设计Function
类型的主要目的之一就是要兼容Java
,实现在Kotlin
中也能调用Java
的Lambda
。 在Java
中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这一特性。所以如果我们要把Java
的Lambda
传给Kotlin
,那么它们就必须实现Kotlin
的Function
接口,在Kotlin
中我们则不需要跟它们打交道。
为什么这里Function
类型最大的是Function22
?如果Lambda
的参数超过了22
个,那该怎么办呢?Kotlin
在设计的时候便考虑到了这种情况,除了23
个常用的Function
类型外,还有一个FunctionN
。在参数真的超过22
个的时候,就可以依靠它来解决问题。以下是FunctionN
的源码:
package kotlin.jvm.functions
import kotlin.jvm.internal.FunctionBase
/**
* A function that takes N >= 23 arguments.
*
* This interface must only be used in Java sources to reference a Kotlin function type with more than 22 arguments.
*/
@SinceKotlin("1.3")
interface FunctionN<out R> : Function<R>, FunctionBase<R> {
operator fun invoke(vararg args: Any?): R
override val arity: Int
}
foo
函数的返回类型是Function0
。这也意味着,如果我们调用了foo(n)
,那么实质上仅仅是构造了一个Function0
对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用Function0
的invoke
方法才能执行println
方法。 所以,上述的例子必须如下修改,才能够最终打印出我们想要的结果:
fun foo(int: Int) = {
print(int)
}
fun main() {
listOf(1, 2, 3).forEach { foo(it).invoke() }
}
我们还可以用熟悉的括号调用来替代invoke
, 如下所示:
listOf(1, 2, 3).forEach { foo(it)() }
7 函数、Lambda
和闭包
fun
在没有等号、只有花括号的情况下,是我们最常见的代码块函数体,如果返回非Unit
值,必须带return
:
fun foo(int: Int) { print(int) }
fun foo(x: Int, y: Int): Int { return x + y }
fun
带有等号,是单表达式函数体,该情况下可以省略return
:
fun foo(x: Int, y: Int) = x + y
不管是用val
还是fun
,如果是等号加花括号的语法,那么构建的就是一个Lambda
表达式,Lambda
的参数在花括号内部声明。所以,如果左侧是fun
,那么就是Lambda
表达式函数体,也必须通过()
或invoke
来调用Lambda
:
val foo = { x: Int, y: Int -> x + y } // foo.invoke(1, 2)或foo(1, 2)
fun foo(x: Int) = { y: Int -> x + y } // foo(1).invoke(2)或foo(1)(2)
在Kotlin
中,匿名函数体、Lambda
(以及局部函数、object
表达式)在语法上都存在{}
,由这对花括号包裹的代码块如果访问了外部环境变量则被称为一个闭包。 一个闭包可以被当作参数传递或者直接使用,它可以简单地看成“访问外部环境变量的函数”。Lambda
是Kotlin
中最常见的闭包形式。(闭包就是能够读取其他函数内部变量的函数)
与Java
不一样的地方在于,Kotlin
中的闭包不仅可以访问外部变量,还能够对其进行修改:
var sum = 0
listOf(1, 2, 3).filter { it > 0 }.forEach { sum += it }
println(sum) // 6
此外,Kotlin
还支持一种自运行的Lambda
语法:
{ x: Int -> println(x) }(1) // 1
8 "柯里化"风格、扩展函数
下面我们再了解下高阶函数在Kotlin
中另一方面的表现,即一个函数返回另一个函数作为结果。对于以下例子:
fun foo(x: Int) = { y: Int -> x + y }
// 等价于
fun foo(x: Int): (Int) -> Int {
return { y: Int -> x + y }
}
有了函数类型信息之后,可以很清晰地发现,执行foo
函数之后,会返回另一个 类型为(Int)->Int
的函数。
“柯里化(Currying)”的语法,其实就是函数作为返回值的一种典型的应用。简单来说,柯里化指的是把接收多个参数的函数变换成一系列仅接收单一参数函数的过程,在返回最终结果值之前,前面的函数依次接收单个参数,然后返回下一个新的函数。
拿我们最熟悉的加法举例子,以下是多参数的版本:
fun sum(x: Int, y: Int, z: Int) = x + y + z
sum(1, 2, 3)
如果我们把它按照柯里化的思路重新设计,那么最终就可以实现链式调用:
fun sum(x: Int) = { y: Int -> { z: Int -> x + y + z } }
sum(1)(2)(3)
柯里化是为了简化Lambda
演算理论中函数接收多参数而出现的,它简化了理论,将多元函数变成了一元。然而,在实际工程中,Kotlin
等语言并不存在这种问题, 因为它们的函数都可以接收多个参数进行计算。那么,这是否意味着柯里化对我们而言, 仅仅只有理论上的研究价值呢?虽然柯里化在工程中并没有大规模的应用,然而在某些情况下确实起到了某种奇效。
在Lambda
表达式中,还存在一种特殊的语法:如果一个函数只有一个参数,且该参数为函数类型,那么在调用该函数时,外面的括号就可以省略:
fun omitParentheses(block: () -> Unit) {
block()
}
omitParentheses {
println("parentheses is omitted")
}
此外,如果参数不止一个,且最后一个参数为函数类型时,就可以采用类似柯里化风格的调用:
fun curryingLike(content: String, block: (String) -> Unit) {
block(content)
}
fun main() {
curryingLike("looks like currying style") { content ->
println(content) // looks like currying style
}
// 等价于
curryingLike("looks like currying style", { content ->
println(content)
})
}
Kotlin
中的扩展函数允许我们在不修改已有类的前提下,给它增加新的方法:
fun View.invisible() {
this.visibility = View.INVISIBLE
}
在这个例子中,类型View
被称为接收者类型,this
对应的是这个类型所创建的接收者对象。this
可以被省略,就像这样子:
fun View.invisible() {
visibility = View.INVISIBLE
}
我们给Android
中的View
类定义了一个invisible
方法,之后View
对象就可以直接调用该方法来隐藏视图。
views.forEach { it.invisible() }