Skip to content

Latest commit

 

History

History
1146 lines (819 loc) · 48.2 KB

2.Kotlin_高阶函数&Lambda&内联函数.md

File metadata and controls

1146 lines (819 loc) · 48.2 KB

2.Kotlin_高阶函数&Lambda&内联函数

函数还是属性

在某些情况下,不带参数的函数可与只读属性互换。 虽然语义相似,但是在某种程度上有一些风格上的约定。

底层算法优先使用属性而不是函数:

  • 不会抛异常
  • 计算开销小(或者在首次运行时缓存)
  • 如果对象状态没有改变,那么多次调用都会返回相同结果

高阶函数

高阶函数(Higher Order Function)是一种特殊的函数,它接收函数作为参数,或者返回一个函数。
函数的基本格式如下:

fun 高阶函数名(参数函数名: 参数函数类型): 高阶函数返回类型 {
    // 高阶函数体
}

高阶函数的一个很好的例子就是lock函数,它的参数是一个Lock类型对象和一个函数。

该函数执行时获取锁,运行函数参数,运行结束后再释放锁。

fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
        return body()
    } finally {
        lock.unlock()
    }
}

在这个例子中,body为函数的类型对象,该函数是一个无参函数,而且返回值类型是T。

调用lock函数时,可以传入另一个函数作为参数。

和许多编程语言类似,如果函数的最后一个参数也是函数,则该函数参数还可以定义在括号外面,如:

val result = lock(lock, { sharedResource.operation()})
// 等同于
lock(lock) {
    sharedResource.operation()
}

高阶函数类似C语言的函数指针,它的另一个使用场景是map函数。

如果函数只有一个参数,则可以忽略声明的函数参数,用it来代替。

val doubled = mMap.map {it -> it * 2}

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却允许传入一小段代码。这里两次使用了“一小段代码”这种描述,那么到底多少代码才算一小段代码呢?Kotlin对此并没有进行限制,但是通常不建议在Lambda表达式中编写太长的代码,否则可能会影响代码的可读性。

Lambda的语法:

  • 一个Lambda表达式必须通过{}来包裹
  • 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明
  • 如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略

此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如:

val foo = { x: Int -> 
    val y = x + 1
	y // 返回值是y           
}

Lambda表达式

{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}

“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表达式会根据主体中的最后一个(或可能是单个)表达式会视为 返回值。当然,在某些特定情况下,xy的类型了是可以推断的,所以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的参数和返回值的类型。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类型也被认为是函数类型。

当Lambda表达式的参数列表中只有一个参数时,那么可以不声明唯一的参数名,而是可以使用it关键字来代替,因为Kotlin会隐含的声明一个名为it的参数.这叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数

val list = listOf("Apple", "Bnana", "Orange", "Pear")
val maxLengthFruit = list.maxBy {it.length}

Lambda开销

fun foo(int: Int) = {
    print(int)
}
listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用foo

这里的调用等价于:

listOf(1, 2, 3).forEach { item -> foo(item) }

假设使用以下代码将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表达式的机理。

Function类型

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表达式或匿名函数(局部函数、对象表达式等)都可以访问它的闭包。

在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。
一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。

与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改(我有点疑惑,Java为啥不能修改?下面说),如下:

var sum = 0
listOf(1, 2, 3).filter { it > 0 }.forEach {
    // 外部的变量,且可以修改
    sum += it
}
println(sum)  // 6

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。--百度百科 第一句总结的很简洁了:闭包就是能够读取其他函数内部变量的函数。

Java实现闭包

在Java8之前,是不支持闭包的,但是可以通过“接口+匿名内部类”来实现一个伪闭包的功能,为什么说是伪闭包?

Anonymous inner class is not closure

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.

Closure in Java

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修饰

我学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 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中的一个概念:在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。

优化Lambda开销

在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。

该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。

可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。

尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。

1. invokedynamic

在讲述内联函数具体的语法之前,我们先来看看Java中是如何解决这个问题的。与Kotlin这种在编译期通过硬编码生成Lambda转换类的机制不同,Java在SE 7之后通过invokedynamic技术实现了在运行期才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码。这种做法的好处主要体现在:

  • 由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少。
  • 与编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在JDK库的实现, 这极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对编译策略不断优化升级
  • JVM天然支持了针对该方式的Lambda表达式的翻译和优化,这也意味着开发者在书写Lambda表达式的同时,可以完全不用关心这个问题,这极大地提升了开发的体验。

2. 内联函数

invokedynamic固然不错,但Kotlin不支持它的理由似乎也很充分,我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致它无法通过invovkedynamic来解决Android平台的Lambda开销问题。

因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。

简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。 所以内联函数的工作原理并不复杂,就是Kotlin编译器会将内敛函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。

所以如果你想在用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。

noinline: 避免参数被内联

通过上面的例子我们已经知道,如果在一个函数的开头加上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非局部返回

另外一种等效的方式,是通过标签利用@符号来实现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
}
为什么要设计noinline

这里我已经蒙了,前面已经说了内联函数的好处,那为什么Kotlin还要提供一个noinline关键字来排除内联功能呢? 这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。 非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。 另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这个问题,我们来看下面的例子:

fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return@printString
        println(s)
        println("lambda end")
    }
    println("main end")
}

这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。

注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下:

main start
printString begin
lambda start
printString end
main end

可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日志是正常打印的,说明return@printString确实只能进行局部返回。但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示:

inline fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

fun main() {
    println("main start")
    val str = ""
    printString(str) { s ->
        println("lambda start")
        if (s.isEmpty()) return
        println(s)
        println("lambda end")
    }
    println("main end")
}

现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程。现在重新运行一下程序,打印结果如下:

main start
printString begin 
lambda start

可以看到,不管是main()函数还是printString()函数,确实都在return关键字之后停止执行了,和我们所预期的结果一致。 将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。观察下面的代码示例:

inline fun runRunnable(block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

这段代码在没有加上inline关键字声明的时候绝对是可以正常工作的,但是在加上inline关键字之后就会提示如下:

Image

这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。

而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。

而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。 也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。

那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键字就可以很好地解决这个问题:

inline fun runRunnable(crossinline block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

可以看到,这里在函数类型参数的前面加上了crossinline的声明,代码就可以正常编译通过了。 那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示上面所示的错误,就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。

声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他所有特性。

crossinline

值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的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>()