Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民——我们不仅可以像类一样在底层直接定义一个函数,也可以在一个函数内部定义一个局部函数。
fun foo(x: Int) {
fun double(y: Int): Int {
return y * 2
}
println(double(x))
}
我们会善于对熟悉或重复的事物进行抽象,比如2岁左右的小孩就会开始认识数字1、2、3....之后,我们总结除了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。
过程也是一种抽象,几乎我们所熟悉的所有高级语言都包含了定义过程的能力,也就是函数。
然而,在我们以往熟悉的编程中,过程限制为只能接收数据为参数,这个无疑限制了进一步抽象的能力。
由于我们经常会遇到一些同样的程序设计模式能够用于不同的过程,比如一个包含了正整数的列表,需要对它的元素进行各种转换操作,例如对所有元素都乘以3,或者都除以2。我们就需要提供一种模式,同时接收这个列表及不同的元素操作过程,最终返回一个新的列表。
为了把这种类似的模式描述为相应的概念,我们就需要构造出一种更加高级的过程,表现为:接收一个或多个过程为参数,或者以一个过程作为返回结果。这个就是所谓的高阶函数,你可以把它理解为“以其他函数作为参数或返回值的函数”。高阶函数是一种更加高级的抽象机制,它极大地增强了语言的表达能力。
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.continent == "EU") { // EU代表欧洲
res.add(c)
}
}
return res
}
}
后来Shaw对非洲也产生了兴趣,于是他又改进了上述方法的实现,支持根据具体的州来筛选国家。
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的地理知识越来越丰富了,他想对国家的特点做进一步的研究,比如筛选具有一定人口规模的国家,于是代码又变成下面这个样子:
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的参数来代表人口(单位:万)。Shaw开始感觉到不对劲,如果按照现有的设计,更多的筛选条件会作为方法参数而不断增加,而且业务逻辑也会高度耦合。
解决问题的核心在于对filterCountries方法进行解耦,我们能否把所有的筛选逻辑行为都抽象成一个参数呢?传入一个类对象是一种解决方法,我们可以根据不同的筛选需求创建不同的子类,它们都各自实现了一个校检方法。然而,Shaw了解到Kotlin是支持高阶函数的,理论上我们同样可以把筛选的逻辑变成一个方法来传入,这样思路更简单。
他想要进一步了解高级的特性,所以很快写了一个新的测试类:
class CountryTest {
fun isBigEuropeanCountry(country: Country): Boolean {
return country.continient == "EU" && country.population > 10000
}
}
调用isBigEuropeanCountry方法就能够判断一个国家是否是一个人口超过1亿的欧洲国家。然而,怎样才能把这个方法变成filterCountries方法的一个参数呢?要实现这一点似乎要先解决以下两个问题:
-
方法作为参数传入,必须像其他参数一样具备具体的类型信息
在kotlin中,函数类型的格式非常简单:
- 通过 -> 符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型
- 必须用一个括号来包裹参数类型,如果是一个没有参数的函数类型,参数类型部分就用()表示
- 返回值类型即使是Unit,也必须显式声明
举个例子:
(Int) -> Unit () -> Unit (Int, String) -> Unit // 还支持为声明参数指定名字 (errCode: Int, errMsg: String) -> Unit // ?表示可选,可在某种情况下为空 ((errCode: Int, errMsg: String?) -> Unit)? // 表示传入一个类型为Int的参数,然后返回另一个类型为(Int) -> Unit的函数 (Int) -> ((Int) -> Unit)
在学习了Kotlin函数类型知识之后,Shaw重新定义了filterCountries方法的参数声明:
// 增加了一个函数类型的参数test fun filterCountries(countries: List<Country>, test: (Country) -> Boolean): List<Country> { val res = mutableListOf<Country>() for (c in countries) { // 直接调用test函数来进行筛选 if (test(c)) { res.add(c) } } return res }
接下来就是如何把isBigEuropeanCountry方法传递给filterCountries呢? 直接把isBigEuropeanCountry当参数肯定不行,因为函数名并不是一个表达式不具有类型信息。所以我们需要的是一个单纯的方法引用表达式。
-
需要把isBigEuropeanCountry的方法引用当做参数传递给filterCountries
Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用(方法引用表达式)。以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这样写:
countryTest::isBigEuropeanCountry
于是,Shaw便使用了方法引用来传递参数:
val countryApp = CountryApp() val countryTest = CountryTest() val countries = ... countryApp.filterContries(countries, countryTest::isBigEuropeanCountry)
经过重构后的程序显然比之前要优雅许多,程序可以根据任意的需求筛选,调用同一个filterCountries方法来获取国家数据。
此外,我们还可以直接通过这种语法,来定义一个类的构造方法引用变量。
class Book(val name: String) {
fun main(args: Array<String>) {
val getBook = ::Book
println(getBook("Dive into Kotlin").name)
}
}
可以发现,getBook类型为(name: String) -> Book。类似的道理,如果我们要引用某个类的成员变量,如Book类中的name,就可以这样引用:
Book::name
以上创建的Book::name的类型为(Book) -> String。当我们再对Book类对象的集合应用一些函数式API的时候,这会显得格外有用,比如:
fun main(args: Array<String>) {
val bookNames = listOf (
Book("Thinking in java")
Book("Dive into Kotlin")
).map(Book::name)
println(bookNames)
}
再来思考下上面代码中的CountryTest类,这仍算不上是一种很好的方案。因为每增加一个需求,我们都需要在类中专门写一个新增的筛选方法。然而Shaw的需求很多都是临时性的,不需要被复用。Shaw觉得这样还是比较麻烦,他打算用匿名函数对程序进一步的优化。
Kotlin支持在缺省函数名的情况下,直接定义一个函数。所以isBigEuropeanCountry方法我们可以直接定义为:
// 没有函数名字
fun(country: Country): Boolean {
return country.continient == "EU" && country.population > 10000
}
于是,Shaw直接调用filterCountries,如下:
countryApp.filterCountries(countries, fun(country: Country): Boolean) {
return country.continient == "EU" && country.population > 10000
})
这一次我们甚至不需要CountryTest这个类了,代码的简洁性又上了一层楼。Shaw开始意识到Kotlin这门语言的魅力,很快他发现还有一种语法可以让代码更简单,这就是Lambda表达式。
我们继续看上面的filterCountries方法的匿名函数,会发现:
- fun(country: Country)显得比较啰嗦,因为编译器会推导类型,所以只需要一个代表变量的country就行了。
- return关键字也可以省略,这里返回的是一个有值的表达式
- 模仿函数类型的语法,我们可以用 -> 把函数和返回值连接在一起
因此,简化后的表达就变成了这个样子:
countryApp.filterCountries(countries, {
country ->
country.continient == "EU" && country.population > 10000
})
这就是Lambda表达式,它与匿名函数一样,是一种函数字面量。
Lambda的语法:
- 一个Lambda表达式必须通过{}来包裹
- 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明
- 如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略
此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如:
val foo = { x: Int ->
val y = x + 1
y // 返回值是y
}
“Lambda 表达式”(lambda expression)其实就是匿名函数,
Lambda
表达式基于数学中的λ
演算得名,直接对应于其中的lambda
抽象(lambda abstraction)
,是一个匿名函数,即没有函数名的函数。Lambda
表达式可以表示闭包。
Java 8
的一个大亮点是引入Lambda
表达式,使用它设计的代码会更加简洁。
// 没有使用Lambda的老方法:
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent ae){
System.out.println("Actiondetected");
}
});
// 使用Lambda:
button.addActionListener(()->{
System.out.println("Actiondetected");
});
// 不采用Lambda的老方法:
Runnable runnable1=new Runnable(){
@Override
public void run(){
System.out.println("RunningwithoutLambda");
}
};
// 使用Lambda:
Runnable runnable2=()->{
System.out.println("RunningfromLambda");
};
Lambda
能让代码更简洁,Kotlin的支持如下:
lambda
表达式总是被大括号括着- 其参数(如果有的话)在
->
之前声明(参数类型可以省略), - 函数体(如果存在的话)在
->
后面。
Lambda
表达式是定义匿名函数的简单方法。由于Lambda
表达式避免在抽象类或接口中编写明确的函数声明,进而也避免了类的实现部分,
所以它是非常有用的。
Lambda
表达式由箭头左侧函数的参数(在圆括号里的内容)定义的,将值返回到箭头右侧。
view.setOnClickListener({ view -> toast("Click")})
在定义函数时,必须在箭头的左侧用方括号,并指定参数值,而函数的执行代码在箭头右侧。如果左侧不使用参数,甚至可以省去左侧部分:
view.setOnClickListener({ toast("Click") })
如果函数的最后一个参数是一个函数的话,可以将作为参数的函数移到圆括号外面:
view.setOnClickListener() { toast("Click") }
先看一个例子:
fun compare(a: String, b: String): Boolean {
return a.length < b.length
}
max(strings, compare)
就是找出strings
里面最长的那个。但是我个人觉得compare
还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。
max(strings, (a,b)->{a.length < b.length})
(a,b)->{a.length < b.length}
就是一个没有名字的函数,直接作为参数赋给max
方法的第二个参数。但这个方法有很多东西都没有写明,如:
- 参数的类型
- 返回值的类型
但这些真的必要吗?a.length < b.length
很明显返回一个Boolean
的值,再就是max
的定义中肯定也定义了这个函数的参数类型和返回值类型。
这么明显的事为什么不让计算机自己去做而要让人写代码去做呢?这就是匿名函数的好处了。到这里,我们已经和Lambda
很接近了。
val sum: (Int, Int) -> Int = { x, y -> x + y }
Lambda
表达式就是被大括号括着的那一部分,在->
符号之前有参数声明,函数体跟在一个->
符号之后。
而且此Lambda
表达式之前有一个匿名的函数声明(在此例中两个Int
型的输入,一个Int
型的返回值),这个声明是可以不使用的。
则此Lambda
表达式变成val sum = { x: Int, y: Int -> x + y }
,此时Lambda
表达式会根据主体中的最后一个(或可能是单个)表达式会视为
返回值。当然,在某些特定情况下,x
、y
的类型了是可以推断的,所以val sum = { x, y -> x + y }
。
通过调用lambda来执行它的代码你可以使用invoke函数调用lambda,并传入参数的值。例如,以下代码定义了变量addInts,并将用于将两个Int参数相加的lambda赋值给它。然后代码调用了该lambda,传入参数值6和7,并将结果赋值给变量result:
val addInts = { x: Int, y: Int -> x + y }
val result = addInts.invoke(6, 7)
// 还可以使用如下快捷方式调用lambda:
val result = addInts(6, 7)
就像任何其他类型的对象一样,lambda也具有类型。然而,lambda类型的不同点在于,它不会为lambda的实现指定类名,而是指定lambda的参数和返回值的类型。lambda类型的格式如下:
(parameters) -> return_type
因此,如果你的lambda具有单独的Int参数并返回一个字符串,如下代码所示:
val msg = { x: Int -> "xxx" }
其类型为:
(Int) -> String
如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用:
val add: (Int, Int) -> Int
add = { x: Int, y: Int -> x + y }
Lambda类型也被认为是函数类型。
fun foo(int: Int) = {
print(int)
}
listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用foo
这里,你可定会纳闷it是啥?其实它也是Kotlin简化Lambda表达的一种语法糖,叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于:
listOf(1, 2, 3).forEach { item -> foo(item) }
如果lambda具有一个单独的参数,而且编译器能够推断其类型,你可以省略该参数,并在lambda的主体中使用关键字it指代它。要了解它是如何工作的,如前所述,假设使用以下代码将lambda赋值给变量:
val addFive: (Int) -> Int = { x -> x + 5 }
由于lambda具有单独的参数x,而且编译器能够推断出x为Int类型,因此我们可以省略该x参数,并在lambda的主体中使用it替换它:
val addFive: (Int) -> Int = { it + 5 }
在上述代码中,{it+5}等价于{x->x+5},但更加简洁。请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。例如,以下代码将无法编译,因为编译器不知道it应该是什么类型:
val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断其类型
我们看一下foo函数用IDE转换后的Java代码:
@JvmStatic
@NotNull
public static final Function0 foo(final int var0) {
return (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Ojbect invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
int var1 = var0;
System.out.printlln(var1);
}
});
}
以上是字节码反编译的Java代码,从中我们可以发现Kotlin实现Lambda表达式的机理。
Kotlin在JVM层设计了Function类型(Function0、Function1 ... Function22、FunctionN)来兼容Java的Lambda表达式,其中的后缀数字代表了Lambda参数的数量,如以上的foo函数构建的其实是一个无参Lambda,所以对应的接口是Function0,如果有一个参数那么对应的就是Function1.它在源码是如下定义的:
package kotlin.jvm.functions
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
可见每个Function类型都有一个invoke方法。设计Function类型的主要目的之一就是要兼容Java,实现在Kotlin中也能调用Java的Lambda。在Java中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这一特性。
foo函数的返回类型是Function()。这也意味着,如果我们调用了foo(n),那么实质上仅仅是构造了一个Function()对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用Function()的invoke方法才能执行println方法。所以上面的例子必须如下修改,才能最终打印出我们想要的结果:
fun foo(int: Int) = {
print(int)
}
listOf(1, 2, 3).forEach { foo(it).invoke() } // 增加了invoke调用
但是invoke这种语法显得丑陋,不符合Kotlin简洁表达的设计理念,所以我们还可以用熟悉的括号调用来替代invoke,如下所示:
listOf(1, 2, 3).forEach{ foo(it)() }
在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。
与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改(我有点疑惑,Java为啥不能修改?下面说),如下:
var sum = 0
listOf(1, 2, 3).filter { it > 0 }.forEach {
sum += it
}
println(sum) // 6
看到这里我是懵逼的? 到底什么是闭包? 闭包有什么作用?
闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。--百度百科 第一句总结的很简洁了:闭包就是能够读取其他函数内部变量的函数。
在Java8之前,是不支持闭包的,但是可以通过“接口+匿名内部类”来实现一个伪闭包的功能,为什么说是伪闭包?
Anonymous classes in java are close to being called as a closure. They don’t 100% support the definition but come close to it and thats why we see lot of literature calling anonymous inner classes as closure. Why do I say its not 100%? An anonymous inner class can access “only” the final local variable of the enclosing method. It is because of this restriction, anonymous inner class in java is not a closure.
If you remember the memory management in java, you can recall that the local variables are stored in a stack. These java stacks are created when the method starts and destroyed when it returns. Unlike local variables, final fields are stored in method area which lives longer even after the return of the method. If we want to make anonymous inner class as a closure, then we should allow it to access all the fields surrounding it. But, as per the current memory management, they will be destroyed and will not be accessible after the method has returned.
In that case will we get closure in java in future? We have a specification written by Peter Ahe, James Gosling, Neal Gafter and Gilad Bracha on closures for java. It gives detailed description of how a closure can be implemented in java and example code on how to use them. We have JSR 335 for closures in java named as Lambda Expressions for the Java Programming Language.
class ClosureTest {
public interface MutableAdder {
int add(int x, boolean change);
}
public MutableAdder makeAdderB(int n) {
// Variable 'intHolder' is accessed from within inner class, needs to be declared final
final int[] intHolder = new int[]{n};
return new MutableAdder() {
public int add(int x, boolean change) {
if (change) {
intHolder[0] = x;
return x;
} else {
return intHolder[0] + x;
}
}
};
}
}
OK,实现看完了,那这么做有什么用呢? 反正我平时没用到...
现在继续来说上面提到的与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改。
我学java的时候我就记着这种情况一定要加final,但是我不知道为啥。 今天就仔细看看
Variable 'a' is accessed from within inner class, needs to be final or effectively final(java 8)。
Java doesn't support closures, i.e. local variable can’t be accessed outside the method, but fields of class can be accessed from outside the class.
What are local variables in java?
All variables of the method are called local variables in java.
Where do local variables live in java?
Methods are pushed on stack so local variables live on the stack.
Local variables of the method are kept on the stack and are lost as soon as the method ends in java.
Where do object of local inner class live in java?
As object of local inner class live on the heap, objects may be alive even after method ends in which local inner class have been defined.
As, local variables of the method are kept on the stack and are lost as soon as the method ends, but even after the method ends, the local inner class object may still be alive on the heap.
What java docs says about “Local Inner class cannot access the non-final local variables but can access final local variables.”
A local class can only access local variables that are declared final in java. When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter in java.
JVM create create a synthetic field inside the inner class in java -
As final variable will not change after initialization, when a inner class access final local variables compiler create a synthetic field inside the inner class and also copy that variable into the heap. So, these synthetic fields can be accessed inside local inner class even when execution of method is over in java.
You must be wondering, What are synthetic fields in java?
Synthetic fields are created by compiler and they actually doesn’t exist in source code in java.
The reason is that after the enclosing method returns, the local variable no longer exists. Therefore a copy of the variable is created when the anonymous class is instanciated. If Java allowed the local variable to be changed afterwards, the anonymous class would only know the old value.
简单的说就是: JVM在内部类初始化的时候帮我们拷贝了一个局部变量的备份到内部类中,并且把它的值复制到了堆内存中(变量有两份,同样的名字,一个在局部变量中用,一个在内部类中)。所以要是不用final修饰,那你后面把外部类中的变量的值修改了,而内部类中拷贝的值还是原来的,那这样岂不是两边的值不一样了? 所以不能让你改,必须加final。The solution was to required that captured variables are final (before JDK 8) or effectively final (since JDK 8), which means they cannot be assigned to.
想要理解kotlin中闭包的实现,首先要懂kotlin中的一个概念:在Kotlin中,函数是“一等公民”。
对比一下java和kotlin更好理解:
java代码:
public class TestJava{
private void test(){
private void test(){//错误,因为Java中不支持函数包含函数
}
}
}
在java中是不支持这种写法的,因为函数是“二等公民”。
下面再看下kotlin代码:
fun test(): () -> Unit {
var a = 0
return fun() {
a++
println(a)
}
}
fun main() {
val t = test()
t()
}
是不是发现了新世界的大门,内部函数很轻松地调用了外部变量a。
这只是一个最简单的闭包实现。按照这种思想,其他的实现例如:函数、条件语句、Lambda表达式等等都可以理解为闭包,这里不再赘述。不过万变不离其宗,只要记得一句话:闭包就是能够读取其他函数内部变量的函数。就是一个函数A可以访问另一个函数B的局部变量,即便另一个函数B执行完成了也没关系。目前把满足这样条件的函数A叫做闭包。
刚被闭包搞蒙,这里又没搞明白内联函数到底是干什么? 有什么作用?Kotlin中的内联函数其实显得有点尴尬,因为它之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。
在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。
在讲述内联函数具体的语法之前,我们先来看看Java中是如何解决这个问题的。与Kotlin这种在编译期通过硬编码生成Lambda转换类的机制不同,Java在SE 7之后通过invokedynamic技术实现了在运行期才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码。这种做法的好处主要体现在:
- 由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少。
- 与编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在JDK库的实现, 这极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对编译策略不断优化升级
- JVM天然支持了针对该方式的Lambda表达式的翻译和优化,这也意味着开发者在书写Lambda表达式的同时,可以完全不用关心这个问题,这极大地提升了开发的体验。
invokedynamic固然不错,但Kotlin不支持它的理由似乎也很充分,我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致它无法通过invovkedynamic来解决Android平台的Lambda开销问题。
因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。
所以如果你想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就有必要来学习下内联函数的相关语法。
这里通过一个实际的例子,看看Kotlin的内联函数是具体如何操作的:
fun main(args: Array<String>) {
foo {
println("dive into Kotlin...")
}
}
fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
首先,我们声明了一个高阶函数foo,可以接受一个类型为() -> Unit的Lambda,然后在main函数中调用它。以下是通过字节码反编译的相关Java代码:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
foo((Function0)null.INSTANCE);
}
public static final void foo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var1 = "before block";
System.out.println(var1);
block.invoke();
var1 = "end block";
System.out.println(var1);
}
据我们所知,调用foo就会产生一个Function()类型的block类,然后通过invovke方法来执行,这会增加额外的生成类和调用开销。现在,我们给foo函数加上inline修饰符,如下:
inline fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
再来看看相应的Java代码:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String va1 = "before block";
System.out.println(var1);
// block函数体在这里开始粘贴
String var2 = "dive into Kotlin...";
System.out.println(var2);
// block函数体在这里结束粘贴
var1 = "end block";
System.out.println(var1);
}
public static final void foo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "before block";
System.out.println(var2);
block.invoke();
var2 = "end block";
System.out.println(var2);
}
果然,foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法势必会被调用很多次。通过inline的语法,我们可以彻底消除这种额外调用,从而节省了开销。
内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过Kotlin的集合类API文档或者源码实现就会发现,集合函数式API,如map、filter都被定义成内联函数,如:
inline fun <T, R> Array<out T>.map {
transform: (T) -> R
}: List<R>
inline fun <T> Array<out T>.filter {
predicate: (T) -> Boolean
}: List<T>
这个很容易理解,由于这些方法都接收Lambda作为参数,同时都需要对集合元素进行遍历操作,所以把相应的实现进行内联无疑是非常适合的。
但是内联函数不是万能的,以下情况我们应避免使用内联函数:
- 由于JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。
- 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
- 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把他们声明为internal。
通过上面的例子我们已经知道,如果在一个函数的开头加上inline修饰符,那么它的函数体及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接受多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,这个又该如何处理?
解决这个问题也很简单,Kotlin在引入inline的同时,也新增了noinline关键字,我们可以把它加在不想要被内联的参数开头,该参数便不会具有内联的效果:
fun main(args: Array<String>) {
foo ( {
println("I am inlined...")
}, {
println("I am not inlined...")
})
}
inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {
println("before block")
block1()
block2()
println("end block")
}
同样的方法,再来看看反编译的Java版本:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Function0 block2$iv = (Function0)null.INSTANCE;
String var2 = "before block";
System.out.println(var2);
// block1 被内联了
String var3 = "I am inlined...";
System.out.println(var3);
// block2 还是原样
block2$iv.invoke();
System.out.println(var2);
}
public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {
Intrinsics.checkParameterIsNotNull(block1, "block1");
Intrinsics.checkParameterIsNotNull(block2, "block2");
String var3 = "before block";
System.out.println(var3);
block1.invoke();
block2.invoke();
var3 = "end block";
System.out.println(var3);
}
可以看出,foo函数的block2参数在带上noinline之后,反编译后的Java代码中并没有将其函数体代码在调用处进行替换。
Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。我们先来看下Kotlin如何支持非局部返回。
以下是我们常见的局部返回的例子:
fun main(args: Array<String>) {
foo()
}
fun localReturn() {
return
}
fun foo() {
println("before local return")
localReturn()
println("after local return")
return
}
// 运行结果
before local return
after local return
正如我们所熟知的,localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。我们再把这个函数换成Lambda表达式的版本:
fun main(args: Array<String>) {
foo { return }
}
fun foo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
// 运行结果
Error:(2, 11)Kotlin: 'return' is not allowed here
这时,编译器报错了,就是说在Kotlin中,正常情况下Lambda表达式不允许存在return关键字。这时候,内联函数又可以排上用场了。我们把foo进行内联后再试试看:
fun main(args: Array<String>) {
foo { return }
}
inline fun foo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
// 运行结果
before local return
编译顺利通过了,但结果与我们的局部返回效果不同,Lambda的return执行后直接让foo函数退出了执行。如果你仔细考虑一下,可能很快就想出了原因。因为内联函数foo的函数体及参数Lambda会直接替代具体的调用。所以实际产生的代码中,retrurn相当于是直接暴露在main函数中,所以returning()之后的代码自然不会执行,这个就是所谓的非局部返回。
另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。同样以上的例子,我们可以在不声明inline修饰符的情况下,这么做来实现相同的效果:
fun main(args: Array<String>) {
foo { return@foo }
}
fun foo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
// 运行结果
before local return
非局部返回尤其在循环控制中显得特别有用,比如Kotlin的forEach接口,它接收的就是一个Lambda参数,由于它也是一个内联函数,所以我们可以直接在它调用的Lambda中执行return退出上一层的程序。
fun hasZeros(list: List<Int>): Boolean {
list.forEach {
if (it == 0) return true // 直接返回foo函数结果
}
return false
}
值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有return的Lambda参数产生破坏,我们还可以使用crossinline关键字来修饰该参数,从而杜绝此类问题的发生。就像这样子:
fun main(args: Array<String>) {
foo { return }
}
inline fun foo(crossinline returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
// 运行结果
Error: (2, 11) Kotlin: 'return' is not allowed here
除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。
fun main(args: Array<String>) {
getType<Int>()
}
inline fun <reified T> getType() {
print(T::class)
}
// 运行结果
class kotlin.Int
这个特性在Android开发中也格外有用。比如在Java中,当我们要调用startActivity时,通常需要把具体的目标视图类作为一个参数。然而,在Kotlin中,我们可以用reified来进行简化:
inline fun <refied T : Activity> Activity.startActivity() {
startActivity(Intent(this, T::class.java))
}
这样,我们进行视图导航就非常容易了,如:
startActivity<DetailActivity>()
- 邮箱 :[email protected]
- Good Luck!