一、概念
函数类型:
- 自定义类型、基本类型都是类型,函数也能抽象出类型,叫函数类型。既然函数可以抽象成一个类型,那么函数就可以作为变量 赋值/传参 了。
- ->左边的部分用来声明该函数接收什么参数的,多个参数之间用逗号隔开,如果没有参数直接使用()表示就可以了;->右边表示该函数的返回值是什么类型,如果没有返回值直接使用
Unit
即可。匿名函数、具名函数:在定义时是否指定函数名字。
函数字面值:一段代码,本身没有名字,我们可以把它绑定到一个变量上,通过这个变量操作它,Lambda表达式和匿名函数都叫函数字面值。
Lambda:只是对匿名函数进一步简写的语法糖,本质是一个东西。Lambda 表达式始终被大括号包围,-> 右箭头用作分割,读作“goes to”。
高阶函数:参数或者返回值是函数的函数。通常不会单独定义一个Lambda玩(一般写代码都是定义普通函数),而是把Lambda作为另一个函数的参数使用,传参的时候能直接写而不是麻烦地专门写个函数再去传参(注意时间顺序,成员引用是刚好已存在满足要求的函数,而不是这里说的需要了再去写)(就像Java接口回调中你不会专门写个SAM接口实现类再创建实例去传参,而是用创建匿名内部类写法)。
函数式编程:OOP(Object Oriented Programming) 面向对象编程,FP(Functional Programming) 函数式编程。
二、好处
- Java 中定义工具类方法的时候,一般把这些方法定义成类方法。在 Kotlin 中直接在文件中定义方法就行,不用类包裹了,不过编译器最后还是把它编译成了类方法。
- Java 使用 Lambda 表达式需要函数式接口,它只是一种对匿名内部类的简写作用。而 Kotlin 支持函数式编程,函数可用于 传参/赋值,不需要像 Java 那样自己写或者用系统提供的函数接口。
- Lambda 表达式是对匿名函数的进一步简写形式。
三、演变写法
这是一个具名函数(普通函数)。
fun sum(x: Int, y: Int): Int { ... }
只有变量才能传参使用,因此赋值给变量。aa是一个函数变量,它的类型是函数类型 (Int, Int) → Int,-> 左边是参数类型,右边是返回值类型。
//aa的类型是(Int, Int) -> Int
val aa = fun sum(x: Int, y: Int): Int { ... }
函数名字此时没有作用了,去掉就变成了匿名函数。
val aa = fun (x: Int, y: Int): Int { ... } //写法一:指定参数类型编译器就能反推实例类型
val aa: (Int, Int) -> Int = fun (x, y) { ... } //写法二:指定实例类型编译器就能推导参数类型
去掉fun关键字、去掉返回值类型、去掉参数列表的形参类型(编译器无法推断函数类型因此就要显示声明函数变量的函数类型了)进一步精简成Lambda。函数体中 ->左边是形参(使用的时候有的形参用不到可以使用_下划线替代来减少变量生成节约性能),右边是业务代码,函数体最后一行会被当作返回值。
val aa = { x: Int, y: Int -> ... } //写法一:指定参数类型
val aa: (Int, Int) -> Int = { x, y -> ... } //写法二:指定实例类型
val aa: (Int, Int) -> Int = { _, y -> ... } //用不到参数x可以屏蔽掉
一些语法简化:
view.setOnClickListener(fun (v: View): Unit { ... }) //匿名函数
view.setOnClickListener({ v: View -> ... }) //Lambda
view.setOnClickListener({ v -> ... }) //Lambda参数类型能被推导可以省略
view.setOnClickListener() { v -> ... } //Lambda是函数最后一个参数可以写到小括号外面
view.setOnClickListener { v -> ... } //Lambda是函数唯一参数可以去掉小括号
view.setOnClickListener { it.setVisiable(GONE) } //Lambda只有一个参数可以省去不写,调用有默认名称 it
四、用作函数传参
传参的时候Lambda(即函数形参 result)并没有立即执行,而是在函数体中显式调用 result 来执行。相当于函数体提供了 result 所需形参(往Lambda中传参),result是在编写这些数据的具体操作逻辑并给出结果(返回值)。
//函数类型变量作为函数形参 result: (Int, Int) -> Int
fun show(num: Int, result: (Int, Int) -> Int): Int {
...
result(num,5) //具体执行的地方,相当于函数中提供了参数(num和5)
}
//相当于具体编写参数的操作逻辑并给出结果(返回值)
val result = {a: Int, b: Int -> a + b}
show(2, result)
五、Function接口
针对函数对象的行为(传参、处理、返回)抽象出接口。Kotlin 定义了 kotlin.Function<out R> 接口来抽象所有的函数,它没有定义任何方法。kotlin.jvm.functions 包里定义了 Function0<out R> 到 Function22<out R> 来分别抽象无参到 22 个参数的函数,它们都继承了 kotlin.Function 接口,同时定义了一个 invoke() 函数。invoke() 函数定义了“调用”这个行为,它同时重载了括号操作符“()”,允许我们用括号来传入参数、得到返回值。
//例如Function2接口
//P1、P2是传入的两个参数的类型,R是返回值类型
//因为我们只会向函数中传入参数取出返回值,所以用in out收窄功能
interface Function2<in P1, in P2, out R> : Function<R> {
operator fun invoke(p1: P1, p2: P2): R
}
//对于我们写的sum函数对象
//会变编译成Function2类型的对象,(Int, Int) -> Int 是具体实现类
//这时我们就可以用invoke()来调用它
val sum: (Int, Int) -> Int = {a, b -> a + b}
println(sum.invoke(1, 2)) //打印:3
println(sum(1, 2)) //打印:3
六、SAM转换
Java 不支持函数编程(函数只能存在于类中),变通方案是接口回调(函数式接口)。抽象方法一样是定义了参数列表和返回值类型(就像Kotlin中的Lambda,多了无卵用的函数名),只是外层套了一个接口(没有函数类型的对象用来存储传递,实在需要写个类实现后创建对象),都是等实际使用的时候根据情况再编写具体内容,通过匿名内部类对象创建(Java的Lambda只是对匿名内部类简写的语法糖),而这就是SAM转换。
Java中Lambda调用SAM:
interface Cutlery{
String getCutlery(String name,int num);
}
public void eat(int type, Cutlery cutlery){ }
eat(5, (name,num) -> "餐具");
Kotlin调用Java中定义的方法(带有Java的SAM参数)时:同样支持SAM转换。底层是将Java的SAM翻译成函数类型。
//底层是将Java中的eat()翻译成函数类型
public void eat(int type, Cutlery cutlery) {}
↓
fun eat(type: Int, cutlery: (name: String, num: Int) -> String) {}
//调用
eat (5) { name, num -> "餐具" }
Kotlin调用Kotlin中定义的方法(带有Java的SAM参数)时:可以使用SAM构造和匿名内部类的方式调用。由于该方法是在Kotlin中明确定义的,因此它不是一个函数类型无法使用Lambda调用。SAM构造相对于匿名内部类创建的好处是,除了书写简便,在不使用外部变量的情况下SAM会保持一个静态对象的引用而不是每次创建对象,性能会好点。
fun eat(type: Int, cutlery: Cutlery)
//使用SAM构造调用(推荐)
eat(5, Cutlery { name, num -> "餐具" })
//使用匿名内部类调用
eat(5,object : Cutlery {
override fun getCutlery(name: String?, num: Int): String ="餐具"
})
Kotlin调用Kotlin中定义的方法(带有Kotlin的SAM参数)时:只能使用匿名内部类方式调用。既然Kotlin支持函数编程,就不希望你用Java这种SAM编程。
interface Cutlery {
fun getCutlery(name: String, num: Int):String
}
fun eat(type: Int, cutlery: Cutlery){}
//只能使用匿名内部类调用
eat(5, object : Cutlery {
override fun getCutlery(name: String, num: Int): String ="餐具"
})
七、成员引用
需要传入函数形参时,现有的具名函数刚好满足要求(函数类型以及代码逻辑),通过成员引用将该函数转换成一个值传递它。
成员变量/函数 扩展属性/函数 | 实例名 :: 属性名/函数名 类名() :: 属性名/函数名 |
单例或伴生对象中的变量/函数 | 类名 :: 属性名/函数名 |
顶层属性/方法 | :: 属性名/函数名 (不属于任何一个类,类型省略) |
构造函数 | :: 类名 |
//这是一个具名函数
fun sum(x: Int, y: Int): Int = x + y
//具名函数加上::双冒号就变成一个函数类型的变量,只有变量才具有 传参/赋值 的功能。
//函数类型的对象才有invoke(),具名函数是没有的。
val aa = ::sum //赋值,aa的类型 KFunction2<Int, Int, Int>
aa(2,3) //调用,等效于 aa.invoke(2,3)
method(::sum) //传参
(::sum)(2,3) //调用,等效于 (::sum).invoke(2,3)
八、带接收者的Lambda
A.() -> B
- 和扩展函数的区别就像 Lambda 和普通函数的区别,可以当作扩展函数的对象化形式,,表示可以在 A 类型(接收者)的实例上调用该函数并返回一个 B 类型的值。
- 在 Lambda 函数体中(即代码块中)可以直接调用接收者的 public 成员属性或函数(省略了this)。
- 主要用于建造者模式,DSL。
//扩展函数(一个类型作为接收者, 它接收一些参数,然后和这些参数相互作用,最后产出一个结果)
fun Int.show(other: Int) = this + other
//匿名函数
val cc = fun Int.(other: Int) = this + other //写法一:指定参数类型
val dd: Int.(Int) -> Int = fun Int.(other) = this + other //写法二:指定实例类型
//带接收者的Lambda
val ee: Int.(Int) -> Int = { this + it } //这里的this是Int类型的实例,即调用者
//使用
ee(1,2) //像普通函数那样
1.ee(2) //像扩展函数那样
ee.invoke(1, 2) //使用invoke
//传参
fun method(a: Int, b: Int, lambda: Int.(Int) -> Int): Int {
return a.lambda(b)
}
//省略this
val ff: Int.() -> Unit ={
//直接调用Int的public成员
}
九、invoke约定
- 我们只需要在一个类中使用 operator 来修饰 invoke() 函数,这样的类的对象就可以直接像一个保存 lambda 表达式的变量一样直接调用,而调用后执行的函数就是invoke函数。(把对象当作函数名一样调用函数)
- 还有另一种方式来实现可调用的对象,即让类继承自函数类型,然后重写 invoke() 方法.
- 让函数变量像 对象调用函数 一样调用自身。
class A(val str: String) {
operator fun invoke() {
println(str)
}
}
val a = A("Hello")
a() //等价于a.invoke()
class B : (String) -> String {
override fun invoke(str: String): String {
return str
}
}
val b = B()
println(b("Hello")) //等价于println(b.invoke("Hello"))
val aa: (Int, Int) -> Unit = { a, b -> println(a + b) }
aa(2, 3) //等价于aa.invoke(2, 3)
十、闭包
函数(具名函数、匿名函数、Lambda)可以捕获(引用和修改)自己外部函数中的局部变量,但会使外部变量保存在内存中避免其随着外部环境的销毁,保证了变量的安全性但有性能开销。闭包赋值给变量后,变量销毁内存释放。
10.1 Java
Java中函数不是一等公民无法嵌套声明,函数必须存在于类/接口中,使用的是函数式接口解决方案。匿名内部类类似于一个闭包,当捕捉外部的final变量时,它的值和使用这个值的Lambda代码(匿名内部类)一起存储。而对于非final变量来说,因为对外部的局部变量引用和修改无法阻止该变量随着外部环境运行结束而一起销毁(栈帧出栈)。解决办法是变量的存储位置由栈转堆,具体实现是外部定义长度为1的数组用arr[0]操作变量,原理是Java会为数组中的元素在堆内存中开辟连续的空间,这样该变量就会因为被引用而不被回收(GC可达性),通过持有该变量的引用来使得内外可以修改同一个变量。但我们一般都是操作类的成员变量,而不是受到栈帧出栈影响的外部函数中的局部变量。
//经典案例
onCreate() {
//这里是外部环境
int num = 0;
btn.setOnClickListener(v -> {
//这个内部的函数里引用了外部变量(匿名内部类的函数)
num++; //报错:num必须声明为final
});
}
//解决办法
onCreate() {
int[] num = new int[]{1};
btn.setOnClickListener(v -> {
num[0]++;
});
}
10.2 Kotlin
Kotlin的捕捉只不过把 Java 中的一些实现细节给优化了,比如捕捉 val 变量时,它的值会被拷贝下来,当捕捉 var 变量时,它的值会被作为 Ref 类的一个实例被保存下来。
//反编译后的java代码
//被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。
//这个 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。
//Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等
//如果是非基础类型,就统一用 ObjectRef 即可。
onCreate() {
final Ref.IntRef num = new Ref.IntRef();
num.element = 0;
btn.setOnClickListener(v -> {
num.element++;
});
}