文章目录
一、高阶函数
可以在顶层直接定义一个函数
也可以在函数内部定义一个局部函数
还可以直接将函数像普通变量一样传递给另一个函数,或在其他函数内被返回
1.1、需要高阶函数的例子
根据不同条件筛选出一个国家列表,定义了了三个重载方法。
data class Country(val name: String, val continent: String, val population: Int)
class CountryApp {
fun filterCountries(countries: List<Country>): List<Country> {
val res = mutableListOf<Country>()
for (c in countries) {
if (c.continent == "EU") {
res.add(c)
}
}
return res
}
fun filterCountries(countries: List<Country>, continent: String): List<Country> {
val res = mutableListOf<Country>()
for (c in countries) {
if (c.continent == continent) {
res.add(c)
}
}
return res
}
fun filterCountries(countries: List<Country>, continent: String, population: Int): List<Country> {
val res = mutableListOf<Country>()
for (c in countries) {
if (c.continent == continent && c.population > population) {
res.add(c)
}
}
return res
}
}
随着筛选条件的增多,我们需要无限制的重载筛选的函数。这时就可以使用 Kotlin 中的高阶函数来解决,首先了解一下什么是函数类型。
1.2、函数类型
- 通过 -> 符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型。
- 必须用一个括号来包裹参数类型。
- 返回值类型即使是 Unit,也必须显式声明。
(Int) -> Unit
() -> Unit //如果是一个没有参数的函数类型,参数类型部分就用 () 来表示
(Int,String) -> Unit //如果是多个参数的情况,需要用逗号来分割
(errCode: Int, errMsg: String) -> Unit //可以为声明参数指定名字
(errCode: Int, errMsg: String?) -> Unit //用 ? 表示可空类型
高阶函数还支持返回另一个函数。
(Int) ->((Int) -> Unit) //传入一个类型为 Int 的参数,然后返回另一个类型为 (Int) -> Unit 的函数
(Int) -> Int -> Unit // 简化写法
传入一个函数类型的参数,再返回一个 Unit。
((Int) -> Int) -> Unit
现在 1.1 中的三个重载方法就可以替换为一个高阶函数。那么现在该如何传参呢,在 1.3 小节中说明。
class CountryApp {
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
}
}
1.3、类成员的引用
定义一个方法来判断该国家是否符合筛选条件
class CountryTest {
fun isBigEuropeanCountry(country: Country): Boolean {
//判断一个国家是否是欧洲国家并且人口数超过10000
return country.continent == "EU" && country.population > 10000
}
}
Kotlin 存在一种特殊的语法,通过两个冒号来实现对于某个类的成员进行引用。
val countries = ArrayList<Country>()
... //添加所有国家到列表 countries
val countryApp = CountryApp()
val countryTest = CountryTest()
countryApp.filterCountries(countries, countryTest::isBigEuropeanCountry)
相对于函数重载,这样的写法简化了很多,但是还存在一个问题,每增加一个需求都需要在类中专门写一个新增的筛选方法,这时就可以考虑用匿名函数。
1.4、匿名函数
Kotlin 支持在缺省函数名的情况下,直接定义一个函数。所以我们上面判断一个国家的方法可以直接定义到参数中,注意这个方法是去掉了函数名 isBigEuropeanCountry 的,没有函数名,就叫匿名函数。
val countries = ArrayList<Country>()
val countryApp = CountryApp()
countryApp.filterCountries(countries, fun(country: Country): Boolean {
//判断一个国家是否是欧洲国家并且人口数超过10000
return country.continent == "EU" && country.population > 10000
})
1.5、 Lambda 语法糖
分析 1.4 中 filterCountries 方法中的匿名函数,可以发现。
- Kotlin 支持类型推导机制,可以将 fun(country: Country) 用一个变量名 country 来代替,返回类型可以省略。
- 省略 return 关键字。
- 模仿函数类型的语法,用 -> 把参数和返回值连接在一起。
countryApp.filterCountries(countries) { country -> country.continent == "EU" && country.population > 10000 }
上述代码 { } 中的就是 Lambda 表达式,现在用 Lambda 的形式来定义一个加法操作。
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
//由于支持类型推导,可以采用以下两种方式进行简化
val sum1 = { x: Int, y: Int -> x + y }
val sum2: (Int, Int) -> Int = { x, y -> x + y }
总结 Lambda 表达式的语法:
- 一个Lambda 表达式必须通过 { } 来包裹。
- 如果 Lambda 声明了函数类型,那么 Lambda 的参数部分的类型就可以省略。
- 此外,如果 Lambda 表达式返回的不是 Unit,那么默认最后一行表达式的值类型就是返回值类型。
- Lambda 表达式作为最后一个参数时可以放到参数的小括号外面。
单个参数的隐式名称:it
fun foo(int: Int) = {
print(int)
}
//3 种调用,依次简化,打印:123
listOf(1, 2, 3).forEach { item -> foo(item).invoke() }
listOf(1, 2, 3).forEach { foo(it).invoke() }
listOf(1, 2, 3).forEach { foo(it)() }
1.6、函数、Lambda 和闭包
在不熟悉 Kotlin 语法的情况下,很容易对 fun 声明函数、Lambda 表达式的语法产生混淆,因为它们都可以存在花括号。
- fun 在没有等号、只有花括号的情况下,就是一个常见的代码块函数体,如果指明了返回值,必须带有 return。
fun foo1(x: Int) {
print(x)
}
fun foo2(x: Int, y: Int): Int {
return x * y
}
- fun 带有等号,没有花括号,就是一个单表达式函数体,该情况下可以省略 return。
fun foo3(x: Int, y: Int) = x + y
- 不管是用 val 还是 fun,如果等号和花括号同时存在,那么构建的就是一个 Lambda 表达式,Lambda 的参数在花括号内声明。如果左侧是 fun,Lambda 表达式函数体,必须通过 invoke 或者 () 来调用 Lambda。
val foo1 = { x: Int, y: Int -> x + y } //调用:foo1.invoke(1,2) 或 foo1(1,2)
fun foo2(x: Int) = { y: Int -> x + y } //调用:foo2(1).invoke(2) 或 foo2(1)(2)
- 匿名函数体、Lambda 在语法上都存在 { } ,在 { } 内部如果访问了外部环境变量则被称为一个闭包。
1.7、“柯里化”风格
柯里化指的是把接收多个参数的函数变化成一系列仅接收单一参数函数的过程,在返回最终结果之前,前面的函数依次接收单个参数,然后返回下一个新的函数。例如以下是一个多参数的版本。
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)
二、面向表达式编程
什么是语句,比如一个函数中的赋值、循环控制、打印等操作,这些都可以被称为语句。
fun main(args:Array<String>){
var a=1;
while(a<10){
println(a)
a++
}
}
什么是表达式,可以是一个值、常量、变量、操作符、函数等操作,或它们之间的组合,编程语言对其进行解释和计算,以求产生另一个值。通俗的理解,表达式就是可以返回值的语句。
1 //单纯的字面量表达式,值为 1
-1 //增加前缀操作符,值为 -1
1+1 //加法操作符,返回 2
listOf(1,2,3) //列表表达式
"kotlin".length //值为 6
{ x: Int -> x+1 } // Lambda 表达式,类型为 (Int) -> Int
fun(x: Int){ println(x) } // 匿名函数表达式,类型为 (Int) -> Unit
if ( x>1 ) x else 1 // if-else 表达式,类型为 Int,假设 x 已赋值
可以看出,一些在其他语言中的普通语句,在 Kotlin 中也可以是表达式。
2.1、表达式比语句更安全
下面是一段简单的 Java 代码,思考,由于 if 在这里不是一个表达式,所以我们只能够在外部对变量 a 进行声明,这就存在一个潜在的问题,a 在外部声明时被初始化为 null ,只在 if 内部对 a 赋值,而忽略了 else 分支,当 flag 为 true 时没有问题,当 flag 为 false 时就会报空指针异常,即使程序依旧会编译通过。如果 a 来自上下文其他更远的地方,这种潜在危险则会更容易被忽视。
void ifStatement(Boolean flag) {
String a = null;
if (flag) {
a = "dive into kotlin";
}
System.out.println(a.toUpperCase());
}
接下来创建一个 Kotlin 版本,现在 if 会被作为表达式来使用。可以看出,表达式让代码变得更加紧凑了,可以把赋值语句与 if 表达式混合使用,就不存在变量 a 没有初始值的情况。
在 if 作为表达式时,else 分支也必须被考虑,因为表达式具备类型信息,最终它的类型就是 if、else 多个分支类型的相同类型或公共父类型。
可以看出,基于表达式的方案能让程序更加安全。
2.2、复合表达式:更好的表达力
相比语句而言,表达式更倾向于自成一块,避免与上下文共享状态,相互依赖,因此我们可以说它具备更好的隔离性。表达式更容易进行组合,由于每个表达式都具有值,并且也可以将另一个表达式作为组成自身的一部分,即复合表达式,比如下面的代码就是一个复合表达式。
val res: Int? = try {
if (flag) {
100
} else null
} catch (e: Exception) {
null
}
- try 在 Kotlin 中也是一个表达式,try/catch/finally 语法的返回值类型由 try 或 catch 部分决定,finally 不会产生影响。
- 在 Kotlin 中,if-else 很大程度上代替了传统三元运算符的做法,虽然增加了语法词数量,但是减少了概念,同时更利于阅读。
- if-else 的返回值即 try 部分的返回值,最终 res 的值由 try 或 catch 部分决定。
Kotlin 中的 “?:”
val maybeInt: Int? = null
val maybeInt ?: 1
2.3、枚举类和 when 表达式
枚举是类
与 Java 中的 enum 语法大体相似,无非多了一个 class 关键词,表示它是一个枚举类。由于它是一个类,它自然可以拥有构造函数,以及定义额外的属性和方法。
enum class DayOfWeek(val day: Int) {
MON(1),
TUE(2),
WEN(3),
THU(4),
FRI(5),
SAT(6),
SUN(7);//如果以下有额外的方法或属性定义,则必须加上分号
fun getDayNumber(): Int {
return day
}
}
when 表达式
- 一个完整的 when 表达式类似 switch 语句,由 when 关键字开始,用花括号包含多个逻辑分支,每个分支由 -> 连接,不再需要 switch 的 break,由上到下匹配,一直匹配完为止,否则执行 else 分支的逻辑,类似 switch 的 default。
- 每个逻辑分支具有返回值,最终整个 when 表达式的返回类型就是所有分支相同的返回类型,或公共的父类型。假设所有活动函数的返回值为 Unit,那编译器就会自动推导出 when 表达式的类型,即 Unit。
- when 关键字的参数可以省略。
- 表达式可以组合。
2.4、 for 循环和范围表达式
for 循环
for (i in 1..10) println(i)
for (i: Int in 1..10) println(i)
范围表达式
Range 表达式是通过 rangeTo 函数实现的,通过 “…” 操作符与某种类型的对象组成,除了整形的基本类型之外,该类型需实现 java.lang.Comparable 接口。
"abc".."xyz" //比如,String 类实现了 Comparable 接口,字符串之间可以比较大小,就可以创建一个字符串区间:
for (i in 1..10 step 2) print(i) //step 函数来定义迭代的步长,打印 13579
for (i in 10 downTo 1 step 2) print(i) //倒序,打印 108642
for (i in 1 until 10) print(i) //until 函数实现一个半开区间,打印 123456789,不包含 10
用 in 来检查成员关系
in 关键字,还可以用来检查一个元素是否是一个区间或集合中的成员。
"a" in listOf("b","c") //打印 false
"a" !in listOf("b","c") //打印 true
"kot" in "abc".."xyz" //打印 true
"kot" >= "abc" && "abc" <= "xyz" //等价于上一句
2.5、中缀表达式
上面介绍的 in、step、downTo、until,它们都不通过点号调用,在 Kotlin 标准库中还有另一个类似的方法 to 的方法,这是一个通过泛型实现的方法,返回一个 Pair,源码如下,这种形式定义的函数就被称为中缀函数。
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
中缀函数的表达式为:A 中缀方法 B,可以发现,定义一个中缀函数需要满足如下条件:
- 该中缀函数必须是某个类型的扩展函数或成员方法。
- 该中缀函数只能有一个参数。
- 中缀函数的参数不能有默认值,否则表达式中的 B 会缺失,从而对中缀函数的语义造成破坏。
- 同样,中缀函数的参数不能是可变参数,因为需要保持参数数量始终为 1 个。
可变参数:Kotlin 通过 varargs 关键字来定义函数中的可变参数,类似 Java 中的 “…” 的效果。需要注意,Java 中的可变参数必须是最后一个参数,Kotlin 中没有这个限制,但两者都可以在函数体中以数组的方式来使用可变参数变量。
自定义一个中缀函数 called,它是类 Person 中的一个成员方法。
class Person {
infix fun called(name: String) {
println("My name is $name")
}
}
val p = Person()
p called "Tom" //运行结果 My name is Tom
p.called("Tom") //Kotlin 仍然支持使用普通方法的语法习惯来调用一个中缀函数