一. 一等公民函数
在程序世界里,有且不仅有这么几种权力:创建,赋值,传递
。在JAVA中这些权力,object 都具备,function 都不具备
。object 可以通过参数传递到另一个对象里,从而两个对象可以互相通信。函数却不行,两个函数想要通信,必须以对象为介质
。
以 Java 举个例子:函数a,想要调用函数b。虽然a并不关心函数b是从哪儿来的,只要函数b可以完成这个特定的功能即可。但是在 Java 的世界里函数必须要依附在一个对象上,所以函数a必须依附在对象A上,函数b必须依附在对象B上,函数a必须通过一个对象才能找到函数b,如下:
public class A {
public void a(Object o) {
System.out.println("a is invoked");
o.getClass().getMethod("b").invoke(o);
}
}
public class B {
public void b() {
System.out.println("b is invoked");
}
}
函数b可以这样传递给函数a:
new A().a(new B());
结果如下:
a is invoked
b is invoked
通过这个简单的例子,可以看出,非一等公民的函数生存条件有多么的恶劣,通讯的阻力有多大。
在函数是一等公民的世界里,函数a可以不再依附于对象A而单独存在,函数a可以直接与函数b交流,不再需要通过对象才能找到函数b。
函数是"一等公民"
特点指的就是函数与变量、对象类型一样,处于平等地位。一等公民函数有三个主要的特点。
- 函数可以赋值给一个变量。
- 函数可以作为参数传入另一个函数。
- 函数可以作为别的函数的返回值。
1. 函数可以赋值给一个变量
既然函数可以赋值给一个变量,那么这个变量的类型就是函数类型。Kotlin 中每一个函数都有一个类型,称为 “函数类型”,函数类型是一种数据类型,它与 Int、Boolean等数据类型 在使用场景上没有区别。“::” 可以取出函数的地址引用。
例如:
// 计算一个矩形面积 (Double, Double) -> Double
fun rectangleArea(width: Double, height: Double): Double {
return width * height
}
fun main(args: Array<String>) {
// 通过::取出rectangleArea函数的地址 将函数rectangleArea赋值给一个变量areaFunction,
// 此时areaFunction变量的类型为(Double, Double) -> Double
val areaFunction: (Double, Double) -> Double = ::rectangleArea
//靠变量来调用函数
val area = areaFunction(50.0, 40.0)
println(area) // 2000.0
}
一些相对简单的函数类型:
//无参、无返回值的函数类型(Unit 返回类型不可省略)
() -> Unit
//接收T类型参数、无返回值的函数类型
(T) -> Unit
//接收T类型和A类型参数、无返回值的函数类型(多个参数同理)
(T,A) -> Unit
//接收T类型参数,并且返回R类型值的函数类型
(T) -> R
//接收T类型和A类型参数、并且返回R类型值的函数类型(多个参数同理)
(T,A) -> R
2. 函数作为参数
函数可以作为参数进行传递,如果函数可以作为参数进行传递,那么就可以将不同函数进行组合,提高代码的复用,代码会更简洁,这部分就可以引出高阶函数,类似f(g(x))的形式。
// 计算一个矩形面积 (Double, Double) -> Double
fun rectangleArea(width: Double, height: Double): Double {
return width * height
}
//计算一个三角形面积
fun triangleArea(bottom: Double, height: Double): Double {
return (bottom * height) / 2
}
//获取面积
fun getAreaByFun(funName: (Double, Double) -> Double, a: Double, b: Double): Double {
return funName(a, b)
}
fun main(args: Array<String>) {
//参数为函数,传入不同的函数类型
var triangleArea: Double = getAreaByFun(::triangleArea, 10.0, 15.0)
print(triangleArea)
var rectangleArea = getAreaByFun(::rectangleArea, 10.0, 15.0)
print(rectangleArea)
}
3. 函数可以作为别的函数的返回值。
函数可以作为返回值,那么函数内应该可以定义函数,并且函数可以返回函数内定义的函数。
//获取面积,返回值是一个函数的类型
fun getArea(type: String): (Double, Double) -> Double {
val resultFunction: (Double, Double) -> Double
if (type == "rectangle") {
resultFunction = ::rectangleArea
} else {
resultFunction = ::triangleArea
}
return resultFunction
}
fun main(args: Array<String>) {
//调用函数
val rectangleAreaFun: (Double, Double) -> Double = getArea("rectangle")
println("底 10 高 15,计算三角形面积:${rectangleAreaFun(10.0, 15.0)}")
//调用函数
val triangleAreaFun: (Double, Double) -> Double = getArea("triangle")
println("底 10 高 15,计算长方形面积:${triangleAreaFun(10.0, 15.0)}")
}
二. 函数式编程
函数是“一等公民”是函数式编程的核心概念。
使用表达式,不用语句:函数式编程关心输入和输出,即参数和返回值。在程序中使用表达式可以有返回值,而语句没有。例如控制结构中的 if 和 when 结构都属于表达式。
高阶函数:函数式编程支持高阶函数,所谓的高阶函数就是一个函数可以作为另一个函数的参数或返回值。
无副作用:是指函数执行过程会返回同一个结果,不会修改外部变量,这就是“纯函数”,同样的输入参数一定会有同样的输出结果。
Kotlin 语言支持函数式编程,提供了函数类型、高阶函数 和 Lambda 表达式。
四. 匿名函数
匿名函数就是没有名字的函数对象(注意匿名函数不是函数,而是函数类型的对象),大多数情况下我们定义的函数都是具名函数(有名字的函数)。匿名函数就是只定义参数列表、返回值类型和函数体,把一个匿名函数赋给一个没有定义函数体的函数对象。那这种没有匿名函数我们怎么调用呢?答案是无法直接调用。匿名函数可以赋值给一个变量,或者当作实参直接传递给一个函数类型的形参。
具名函数如下:
fun sum(arg1 : Int,arg2 : Int): Int{
return arg1 + arg2
}
这个函数的名字就叫sum。
那匿名函数定义:
fun(arg1 : Int, arg2 : Int) : Int{
return arg1 + arg2
}
这样写还不行,因为压根不知道什么时候用,所以我们需要付给一个引用,用来保存它,然后在需要使用的时候调用:
val sum = fun(arg1 : Int, arg2 : Int) : Int{
return arg1 + arg2
}
三. lambda表达式
1. 定义
Lambda 表达式的本质其实就是匿名函数。而函数其实就是功能(function),匿名函数,就是匿名的功能代码了。Lambda表达式才是与高阶函数的绝配,平时我们给高阶函数中的函数类型参数传递值时,一般都会选择传入Lambda表达式,因为它足够简洁与强大。
Lambda表达式的本质是匿名函数,而匿名函数的本质是函数类型的对象。因此,Lambda表达式、匿名函数、双冒号+函数名这三个东西,都是函数类型的对象,他们都能够赋值给变量以及当作函数的参数传递!
创建一个函数类型的对象(函数字面量)有三种方式:
- 函数引用,::函数名,表示函数引用,会拿到一个 函数的对象 ;注意不是函数本身。
- 匿名函数,没有名字的函数类型的对象。
- lambda是匿名函数的表现形式也就同上。
通常这样写匿名函数:
val addFun = fun(x: Int, y: Int): Int {
return x + y
}
使用lambda表达式可以简化:
//lambda表达式
val addLambda = { x: Int, y: Int -> x * y }
2. 语法
- 总是被大括号扩着
- 其参数(如果存在)在->之前声明(参数类型可以省略)
- 函数体(如果存在)在->后面
- 无参数的情况
val/var 变量名 = { 操作代码 }
val sum = { }
- 有参数的情况
val/var 变量名:(参数类型,参数类型,...)->返回值类型 = (参数1,参数2,...->操作参数的代码)
val sum:(Int,Int)->Int = {a,b->a+b}
可等价于
//此种写法:即表达式的返回值类型会根据操作代码自推导出来
val/var 变量名 = {参数1:类型,参数2:类型...->操作代码}
val sum={a:Int,b:Int ->a+b}
3.lambda表达式作为函数中的参数的时候
fun sum(a:Int,参数名:(参数1:类型,参数2:类型...)->表达式返回类型){ ... }
简化写法
当 lambda 表达式只接受一个参数时,该参数可以省略,使用时用 it 来表示该参数:
add("xxx", { s -> s + "xxx" })
//等同于
add("xxx", { it + "xxx" })复制代码
当函数最后一个参数为函数时,该函数可以写在 () 外,并用 {} 包裹
add("xxx", { s -> s + "xxx" })
//等同于
add("xxx") { s -> s + "xxx" }
//等同于
foo("xxx") { it + "xxx" }复制代码
当函数只有一个参数,且该参数为函数时,可以直接省去 ()
foo({ s -> s + "xxx" })
//等同于
foo { s -> s + "xxx" }复制代码
当参数在函数体中没有引用时,可以将其设为 _,若此时只有一个参数(且该参数没有被引用),则可以直接省略该参数;若有两个或以上的参数,就算全部都没有被引用,也不可以省略
foo({ s -> print("xxx") })
//等同于
foo({ _ -> print("xxx") })
//等同于
foo({ print("xxx") })
//等同于
foo { print("xxx") }
3. 返回值
lambda表达式返回值总是返回函数体内部最后一行表达式的值。
lambda表达式语法缺少指定函数的返回类型的能力,因此Lambda表达式不能指定返回值类型,当需要显式指定返回类型时,可以使用匿名函数。
fun(x: Int, y: Int): Int {
return x + y
}
4. 带接收者的Lambda
目前讲到的lambda都是普通lambda,lambda中还有一种类型:带接收者的lambda。
带接受者的lambda的类型定义:
A.() -> C
表示可以在A类型的接收者对象上调用并返回一个C类型值的函数。
带接收者的lambda好处是,在lambda函数体可以无需任何额外的限定符的情况下,直接使用接收者对象的成员(属性或方法),亦可使用this访问接收者对象。
Kotlin的标准库中就有提供带接收者的lambda表达式:with和apply。
在kotlin中,提供了指定的接受者对象调用Lambda表达式的功能。在函数字面值的函数体中,可以调用该接收者对象上的方法而无需任何额外的限定符。它类似于扩展函数,它允许在函数体内访问接收者对象的成员。
val iop = fun Int.( other : Int) : Int = this + other
println(2.iop(3))
结果为5
5. 在android使用例子
Java8 开始支持 Lambda 表达式
Java 在使用 单 抽象方法的接口时,允许使用 lambda 表达式
在 Kotlin 中就不支持这么写了,因为没有必要(可以直接传函数对象)
但在 Kotlin 和 Java 做交互的时候可以这么写。
首先来通过一个例子直观感受一下lambda表达式。Android开发中经常会给一个Button设置OnClickListener。比如我们需要让按钮点击后消失,平时我们可能是这样写的:
//传统Java式写法
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
view.setVisibility(View.GONE);
}
});
而在Kotlin中,使用函数式语法,我们可以这样写:
//Kotlin函数式写法
mButton.setOnClickListener {
it.visibility = View.GONE
}
直观来讲,似乎跟我们平时的写法差别有点大,比如,函数调用的小括号不见了,匿名内部类直接被一个函数体取代了,View参数不见了,分号也消失了,还有那个it是什么……其实,就像数学公式推导一样,精简的写法也是通过一步一步简化来的。下面就让我们来看一下代码段2的“推导过程”:
- 首先,代码段1转换为Kotlin代码,并替换为函数式写法:
mButton.setOnClickListener({ view: View ->
view.visibility = View.GONE
})
这段代码非常清晰,花括号包裹的是一段lambda表达式,可以把它作为实参传递给函数,这一步把匿名内部类省略掉了;另外也干掉了分号,因为在Kotlin中行末尾的分号可以省略;最后还省略了set方法,在Kotlin中,会默认把对属性的直接访问转换成get/set方法调用。
- 然后,根据Kotlin的语法约定,如果lambda表达式是函数调用的最后一个实参,就可以把它挪到小括号外面:
mButton.setOnClickListener() { view: View ->
view.visibility = View.GONE
}
- 当lambda是函数的唯一实参,就可以去掉空的小括号对:
mButton.setOnClickListener { view: View ->
view.visibility = View.GONE
}
- 如果lambda的参数的类型可以被编译器推导出来,就可以省略它:
mButton.setOnClickListener { view ->
view.visibility = View.GONE
}
- 最后,如果这个lambda只有一个参数,并且这个参数的类型可以被推断出来(也就是同时满足3和4),那么这个参数也可以省略掉。代码中引用这个参数的地方可以通过编译器自动生成的名称it来替代:
mButton.setOnClickListener {
it.visibility = View.GONE
}
经过上述5个步骤,就得到了最简洁、最清晰的代码段。
四. 闭包
1. 定义
我们都知道,程序的变量分为全局变量和局部变量,全局变量,顾名思义,其作用域是当前文件甚至文件外的所有地方;而局部变量,我们只能再其有限的作用域里获取。
那么,如何在外部调用局部变量呢?答案就是——闭包,与此给闭包下个定义:闭包就是能够读取其他函数内部变量的函数
闭包,即是函数中包含函数,这里的函数我们可以包含(Lambda表达式,匿名函数,局部函数,对象表达式)。
fun test1(){
fun test2(){
}
}
2. 闭包使用
我们来看一个闭包的例子:
fun returnFun(): () -> Int {
var count = 0
return { count++ }
}
fun main() {
val function = returnFun()
val function2 = returnFun()
println(function()) // 0
println(function()) // 1
println(function()) // 2
println(function2()) // 0
println(function2()) // 1
println(function2()) // 2
}
returnFun返回了一个函数,这个函数没有入参,返回值是Int。我们可以用变量接收它,还可以调用它。function和function2分别是创建的两个函数实例。
每调用一次function(),count都会加一,说明count 被function持有了而且可以被修改。而function2和function的count是独立的,不是共享的。
通过 jadx 反编译可以看到:
public final class ClosureKt {
@NotNull
public static final Function0<Integer> returnFun() {
IntRef intRef = new IntRef();
intRef.element = 0;
return (Function0) new 1<>(intRef);
}
public static final void main() {
Function0 function = returnFun();
Function0 function2 = returnFun();
System.out.println(((Number) function.invoke()).intValue());
System.out.println(((Number) function.invoke()).intValue());
System.out.println(((Number) function2.invoke()).intValue());
System.out.println(((Number) function2.invoke()).intValue());
}
}
被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。这个 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。
3. 捕获变量
闭包可以访问函数体之外的变量,这个过程称为捕获变量。
// 全局变量
var value = 0
fun main(args: Array<String>?) {
// 局部变量
var localValue = 20
val result = { a: Int ->
value++
localValue++
val c = a + value + localValue
println(c)
}
result(30)
println("value = $value")
println("localValue = $localValue")
}
System.out: 52
System.out: value = 1
System.out: localValue = 21
闭包是捕获 value 和 localValue 变量的 Lambda 表达式。
Java 与 Koltin 中 Lambda 捕获局部变量区别
在函数不是“一等公民”的 Java 这里,匿名类其实就是代替闭包而存在的。只不过 Java 严格要求所有函数都需要在类里面,所以巧妙的把“声明一个函数”这样的行为变成了“声明一个接口”或“重写一个方法”。匿名类也可以捕获当前环境的 final 局部变量。但和闭包不一样的是,匿名类无法修改捕获的局部变量(final 不可修改)。而匿名类能引用 final 的局部变量,是因为在编译阶段,会把该局部变量作为匿名类的构造参数传入。因为匿名类修改的变量不是真正的局部变量,而是自己的构造参数,外部局部变量并没有被修改。所以 Java 编译器不允许匿名类引用非 final 变量。jdk7在 Lambda 体中只能读取局部变量,不能修改局部变量。而 kotlin 中没有这个限制,可以读取和修改局部变量。如下面代码:
// 声明了一个Java代码接口
public interface Clickable {
void onClick();
}
// Java中的Lambda表达式局部变量捕获
public class Closure {
private void closure(Clickable clickable) {
clickable.onClick();
}
public void main(ArrayList<String> args) {
int count = 0;
closure(() -> {
count += 1; // 编译错误,count需要使用final修饰
});
System.out.println(count);
}
}
这样的Java代码是编译不过的,必须设置为 count 为 final 才能通过编译,但又不能对 count 进行修改,如果非要修改 count 只能把 count 声明为 Closure 的成员变量。
对比 Kotlin 代码实现:
class Closure {
private fun closure(clickable: Clickable) {
clickable.onClick()
}
fun main(args: Array<String>) {
var count: Int = 0
closure(Clickable { count += 1 }) // 编译正常
println(count) // 2
}
}
再来看一个闭包的例子:
fun returnFun(): () -> Int {
var count = 0
return { count++ }
}
fun main() {
val function = returnFun()
val function2 = returnFun()
println(function()) // 0
println(function()) // 1
println(function()) // 2
println(function2()) // 0
println(function2()) // 1
println(function2()) // 2
}
每调用一次function(),count都会加一,说明count 被function持有了而且可以被修改。而function2和function的count是独立的,不是共享的。
五. 扩展函数
扩展函数数是指在一个类上增加一种新的行为,甚至我们没有这个类代码的访问权限。在Java中,通常会实现很多带有static方法的工具类,而Kotlin中扩展函数的一个优势是我们不需要在调用方法的时候把整个对象当作参数传入,它表现得就像是属于这个类的一样,而且我们可以使用this关键字和调用所有public方法。
fun 被扩展类名.扩展函数名( 参数 ){
//实现代码
}
Java调用扩展函数:
扩展类名Kt.扩展函数名(参数);
六. 内联函数
1. inline
如果没有内联修饰符标记函数,在使用lambda带来的性能开销。举个接收函数类型的例子:
//callAction 接受一个函数类型(lambda)
private fun callAction(action: () -> Unit) {
println("call Action before")
action()
println("call Action after")
}
fun main(args: Array<String>) {
callAction {
println("call action")
}
}
反编译为:
public final void main(@NotNull String[] args) {
callAction((Function0)null.INSTANCE);
}
private final void callAction(Function0 action) {
String var2 = "call Action before";
boolean var3 = false;
System.out.println(var2);
action.invoke();
var2 = "call Action after";
var3 = false;
System.out.println(var2);
}
由此可见当调用callAction(action: () -> Unit)
时,传递的lambda会被Function0所代替,而Function0是一个被定义为如下的接口:
public interface Function0<out R> : Function<R> {
public operator fun invoke(): R
}
在调用callAction时,编译器会额外生成一个Function0的实例传递给callAction,内部会调用 Function0 的 invoke() 方法。到目前为止,我们知道使用lambda会带来额外的性能开销。通过内联函数消除lambda带来的运行时开销。
被inline标记的函数就是内联函数,其原理就是:在编译时期,把调用这个函数的地方用这个函数的方法体进行替换。
在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用。还是拿 callAction(action: () -> Unit) 方法举例,当给该函数添加inline修饰符后,编译后的调用代码如下
public final void main(@NotNull String[] {
...省略无关紧要的代码
System.out.println("call Action before");
System.out.println("call action");
System.out.println("call Action after");
}
总结下:
- 被inline修饰的函数叫内联函数。
- 内联函数会在被调用的位置内联。内联函数的代码会被拷贝到使用它的位置,并把lambda替换到其中。
在kotlin中lambda 表达式会被正常地编译成匿名类。这表示每调用一次lambda 表达式,一个额外的类就会被创建。并且如果lambda 捕捉了某个变量,那么每次调用的时候都会创建一个新的对象。这会带来运行时的额外开销,导致使用lambda 比使用一个直接执行相同代码的函数效率更低。
如果使用 inline 修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用。
2. noinline
虽然内联非常好用,但是会出现这么一个问题,就是内联函数的参数(ps:参数是函数,比如上面的body函数)如果在内联函数的方法体内被其他非内联函数调用,就会报错。
举个栗子:
inline fun <T> mehtod(lock: Lock, body: () -> T): T {
lock.lock()
try {
otherMehtod(body)//会报错
return body()
} finally {
lock.unlock()
}
}
fun <T> otherMehtod(body: ()-> T){
}
原因:因为method是内联函数,所以它的形参也是inline的,所以body就是inline的,但是在编译时期,body已经不是一个函数对象,而是一个具体的值,然而otherMehtod却要接收一个body的函数对象,所以就编译不通过了
解决方法:当然就是加noinline了,它的作用就已经非常明显了.就是让内联函数的形参函数不是内联的,保留原有的函数特征.
具体操作:
fun main(args: Array<String>) {
val lock = ReentrantLock()
mehtod(lock,{"body方法体"})
}
inline fun <T> mehtod(lock: Lock, noinline body: () -> T): T {
lock.lock()
try {
otherMehtod(body)
return body()
} finally {
lock.unlock()
}
}
fun <T> otherMehtod(body: ()-> T){
}
这样编译时期这个body函数就不会被内联了
反编译看下
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
ReentrantLock lock = new ReentrantLock();
//这里是生成了一个函数对象
Function0 body$iv = (Function0)null.INSTANCE;
((Lock)lock).lock();
try {
otherMehtod(body$iv);
Object var3 = body$iv.invoke();
} finally {
((Lock)lock).unlock();
}
}
public static final Object mehtod(@NotNull Lock lock, @NotNull Function0 body) {
Intrinsics.checkParameterIsNotNull(lock, "lock");
Intrinsics.checkParameterIsNotNull(body, "body");
lock.lock();
Object var3;
try {
otherMehtod(body);
var3 = body.invoke();
} finally {
InlineMarker.finallyStart(1);
lock.unlock();
InlineMarker.finallyEnd(1);
}
return var3;
}
public static final void otherMehtod(@NotNull Function0 body) {
Intrinsics.checkParameterIsNotNull(body, "body");
}
3. crossinline
很少用到crossinline修饰符,什么是crossinline呢,crossinline 的作用是内联函数中让被标记为crossinline 的lambda表达式不允许非局部返回。
在kotlin中,return 只可以用在有名字的函数,或者匿名函数中,使得该函数执行完毕。
而针对lambda表达式,你不能直接使用return
你可以使用return+label的形式,将这个lambda结束。
但是
若你的lambda应用在一个内联函数的时候,这时候你可以在lambda中使用return
可以这么理解,内联函数在编译的时候,将相关的代码贴入你调用的地方。
lambda表达式就是一段代码而已,这时候你在lambda中的return,相当于在你调用的方法内return
crossinline就是为了让其不能直接return。
函数式编程——闭包
理解Kotlin函数式编程
函数式编程-Kotlin
函数式编程——闭包
kotlin 闭包简单例子
理解Kotlin函数式编程
kotlin的内联函数的使用
inline,包治百病的性能良药
理解Kotlin函数式编程