提纲
高阶函数
定义:高阶函数是将函数用作参数或返回值的函数
其实理解一个新东西,定义是特别重要的,上述定义是kotlin官方文档给出的,它描述了高阶函数是什么,接受了这个定义就不会畏难了。通过上述定义知道高阶函数和函数的区别是输入参数可以是函数,返回值也可以是函数。
组成
函数由输入输出组成
输入
- 普通类型实例
- 函数类型实例
输出
- 普通类型实例
- 函数类型实例
函数类型:定义一个函数
- (A,B) -> C:用普通函数定义,输入为A,B输出为C
- A.(B) -> C:用扩展函数定义
- 挂起函数:用特殊类型的函数定义
- 别名:给声明的函数起名
一个典型的高阶函数
// 定义一个高阶函数
fun <T, R> Collection<T>.fold(
initial: R,
// 普通函数定义的函数类型
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
combine.invoke(accumulator, element)
accumulator = combine(accumulator, element)
}
return accumulator
}
// 简单调用该高阶函数
// Lambdas 表达式是花括号括起来的代码块。
val items = listOf(1, 2, 3, 4, 5)
items.fold(0,
// 这是之后将要谈到的labmda表达式,作用类似普通对象的new即取实例化函数类型
{
// 如果一个 lambda 表达式有参数,前面是参数,后跟“->”
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// lambda 表达式中的最后一个表达式是返回值:
result
})
函数类型的实例化
类似普通对象实例化的new
关键字或者各种依赖注入形式的实例化比如工厂模式,注解等实例化方式,函数类型也由很多方式取实例化
lambda表达式
- 结构:参数+函数体
{x: Int, y: Int -> x + y}
- 实例声明:
val sum = {x: Int, y: Int -> x + y}
vsval sum = Int(2)
- 实例调用:
print(sum(1, 2))
- 作用:更方便的实例化函数类型,往往利用它进行更好的封装
- demo:
fun max(v1: String, v2: String, comparetor: (String, String) -> Boolean): Boolean {
return comparetor.invoke(v1, v2)
}
fun testLambda() {
// 其实和普通对象的实例并无本质上的区别
max("123", "234", { a: String, b: String ->
a.length < b.length
})
// lambda表达式可以写在末尾
max("123", "234") { a: String, b: String ->
a.length < b.length
}
// Kotlin的类型推断:String类型可以推断
max("123", "234") { a, b ->
a.length < b.length
}
// 用lambda编码的形式声明一个函数类型的实例
val compare = { a: String, b: String -> a.length < b.length }
max("123", "234", compare)
}
匿名函数: 没有名字函数
- 结构:顾名思义,如下:
fun(param: Int): String {
return param.toString()
}
注意,这个不像函数可以直接声明,因为它是一个值,一个实例而不是一个声明。
- 声明
val toString = fun(param: Int): String {
return param.toString()
}
我们可以简单认为toString
就是该匿名函数名称, 但是其实编译后的java代码并不是如此。
- 调用:和简单的函数一样的调用方式
toString(1)
- 作用:函数类型的实例化
- demo: 这个demo大家可以比较一下和labmda表达式在格式上的区别
// 比较匿名函数与labmda表达式的区别
fun testNmhs() {
// labmda表达式实例化函数类型
max("123", "234") { a: String, b: String ->
a.length < b.length
}
// 匿名函数实例化函数类型
max("123", "234", fun(a: String, b: String): Boolean {
return a.length < b.length
})
// 等号定义函数
max("123", "234", fun(a: String, b: String) = a.length < b.length)
// 同样可以类型推断
max("123", "234", fun(a, b) = a.length < b.length)
}
其实除了格式上的区别,引入匿名函数还有一个重要的原因在于返回的范围,简单来讲匿名函数可以实现局部返回而lambda表达式实现局部返回就比较麻烦,下面的例子实现了从列表中找到一个名为Alice
的人后就推出循环,也用它来说明什么叫局部返回什么叫非局部返回。
// 换成foreach
fun lookForAlice(people: List<Person>) {
// 局部返回方案
// forEach 接受一个lambda函数
people.forEach { person ->
if (person.name == "Alice") {
println("Found")
// 这里的局部返回需要带标签
return@forEach
}
}
println("Alice is not found")
// 匿名函数局部返回
println("匿名函数实验开始")
people.forEach(fun(person) {
if (person.name == "Alice") {
println("Found")
// 这里的局部返回不用带标签
return
}
println("Alice is not found")
})
println("匿名函数实验结束")
// forEach 接受一个lambda函数
people.forEach { person ->
if (person.name == "Alice") {
println("Found")
// 后面会讲到,内联+lambda可以实现非局部返回即结束了整个函数
return
}
}
println("Alice is not found")
}
下面我们来看第三种函数类型实例化的方式::
使用已有声明的可调用引用即用::
操作符:
- 结构:(类::成员)如
Person::age
,Person::isAudit
- 声明:和所有的实例化方式一样都是可以赋值给一个变量的
val age = Person::age
- 调用:调用就像传值一样简单
- 作用:更简明的一个调用单个方法或访问单个属性的函数值
- demo
val log = listOf(SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit(" / signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID))
data class SiteVisit(val path: String, val duration: Double, val os: OS)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
fun testSiteLog() {
log.filter { it.os == OS.WINDOWS }.map { it.duration }.average()
val time1 = log.filter { it.os == OS.WINDOWS }
// 这里直接调用了属性,我们知道duration属性在kotlin中式由get,set方法的这里其实是传了一个get()函数,get()是一个输入是SiteVisit输出是Int函数,可以理解为对(A,B) -> C类型的实例化
.map(SiteVisit::duration).average()
val time2 = log.averageDurationFor {
it.os == OS.ANDROID
}
}
另外提纲里面提到的因为不常用就不一一解释了。下面我们来看内联函数
内联函数
定义:
对加了
inline
关键字的函数成为内联函数
作用:
在谈具体作用前需要解释函数类型实例化在kotlin中的具体原理,其实lambda函数等实例化函数类型的方式都会被编译成一个匿名类,即每调用一次该函数实例就会有一个额外的类被创建,这会带来不小的开销,所以内联函数出现了,它的作用如下所述:
在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用
运作方式:
- 不使用内联:
fun <T> synchronized(lock: Lock, action: () -> T) : T {
lock.lock()
try {
return action()
} finally {
lock.unlock()
}
}
fun main() {
synchronized(lock) {
println("synchronized inline test")
}
}
// 编译后的java代码如下
public static final Object synchronized(@NotNull Lock lock, @NotNull Function0 action) {
Intrinsics.checkParameterIsNotNull(lock, "lock");
Intrinsics.checkParameterIsNotNull(action, "action");
lock.lock();
Object var2;
try {
var2 = action.invoke();
} finally {
lock.unlock();
}
return var2;
}
public static final void main() {
//实例化了一个Fuction0
synchronized((Lock)lock, (Function0)null.INSTANCE);
}
我们可以看到其实例化了一个类Function0
- 使用内联
public static final void main() {
// 我们可以看到synchronized内部的代码直接被提到了main函数内部
int $i$f$synchronized = false;
((Lock)lock).lock();
boolean $i$f$runUnderLock;
try {
$i$f$runUnderLock = false;
String var3 = "synchronized inline test";
boolean var4 = false;
System.out.println(var3);
Unit var8 = Unit.INSTANCE;
} finally {
((Lock)lock).unlock();
}
}
我们可以看到synchronized内部的代码直接被提到了main函数内部
这其实就是内联函数的核心。
当然kotlin也会提供局部使用内联的方式:
禁用内联: 这个特性很少用到
- 关键字:
noinline
- 作用:被添加
noinline
关键字的函数类型不会被提到调用者的内部 - demo:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }
具体化类型参数
- 关键字:
reified
- 作用:消除类型擦除,可以直接用模板访问比如实例
instance is T
- 用法:该关键字只能添加到内联函数的泛型上
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
// 如果没有reified关键子,因为类型擦除的原因,参数是无法使用is的,
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
- 原理
其实还是基于内联函数的性质,内联函数中的代码都会被提到调用者的作用域,所以在编译期其实就知道泛型T是什么类型,所以上述代码编译后的java代码如下
public static void main() {
TreeNode p$iv;
// 这里的instanceOf MyTreeNode印证了上述表诉
for(p$iv = $this$findParentOfType$iv.getParent(); p$iv != null && !(p$iv instanceof MyTreeNode); p$iv = p$iv.getParent()) {
}
}
总结:
- 函数类型可以让你定义一个具有输入,输出的普通函数模板。
- 高阶函数是以函数类型作为参数或者返回值的函数
- 函数类型的实例化方式由labmda表达式,匿名函数和
::
等方式 - labmda表达式和匿名函数的两个区别是编码方式和返回作用域
- 内联函数可以消除调用函数类型实例带来的性能开销