Kotlin细节文章笔记整理更新进度:
Kotlin系列 - 基础类型结构细节小结(一)
Kotlin系列 - 函数与类相关细节小结(二)
1.高阶函数
基本概念: 传入或者返回函数的函数
函数引用:引用的函数名前加上 ::
- 有以下几种类型:
- 类成员方法引用:
类名::成员方法名
- 扩展函数引用:
类名::扩展函数名
- 实例函数引用:
实例名::成员方法名
- 包级别函数引用:
::函数名
第一个例子:
打印数组中的元素(传入包级别函数)
fun main(args:Array) {
args.forEach(::println) //函数引用
}
public actual inline fun println(message: Any?) {
System.out.println(message)
}
public inline fun Array.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
forEach(action: (T) -> Unit)
:要求传入了一个函数,参数为(action: (T) -> Unit)
,类型为一个参数T
,返回值为Unit
println(message: Any?)
:类型为一个参数T
,返回值为Unit
我们调用args.forEach(::println)
将println
函数传入给forEach
第二个例子:
过滤数组中的空字符串(传入类成员函数)
fun main(args:Array) {
args.filter(String::isNotEmpty)
}
public inline fun Array.filter(predicate: (T) -> Boolean): List {
return filterTo(ArrayList(), predicate)
}
public inline fun CharSequence.isNotEmpty(): Boolean = length > 0
这里有个点要注意下: filter
要求传入的函数类型为(predicate: (T) -> Boolean)
,但是我们传入的String::isNotEmpty
这个方法并没有参数!!!public inline fun CharSequence.isNotEmpty(): Boolean = length > 0
只有一个返回值Boolean
为什么可以呢???
答案:因为
类名::成员方法名
默认就有一个参数,这个函数类型就是类名这个类型的
。比如上面的String::isNotEmpty
相当于isNotEmpty(String)
第三个例子:
打印数组中的元素(传入实例函数)
fun main(args:Array) {
val t = Test()
args.forEach(t::testName)
}
class Test{
fun testName(name:String){
println(name)
}
}
public inline fun Array.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
这里传入的是t::testName
,实例名::成员方法名
就不会默认多出一个参数。如果使用Test::testName
会显示报错信息,也验证了我们上面说的类名::成员方法名
默认就有一个参数。
这里总结一下:函数引用,就是将函数作为参数变量传入具体某个方法中,也可以赋值给变量。注意的是,如果是类成员函数、扩展函数引用(
类名:函数名
),默认参数会多一个就是类本身这个参数
2. 闭包
- 函数运行的环境
- 持有函数运行状态
- 函数内部可以定义函数/类
fun add(x: Int): (Int) -> Int {
return fun(y: Int): Int {
return x + y
}
}
fun main() {
var add2 = add(2)
println(add2(10))
}
函数的定义方法可以传入函数,也可以返回函数,函数内的作用域包含了函数内的子函数跟子类等。
格式 :fun 方法名(形参:函数类型) 函数类型{}
函数类型基本写法:
() -> Unit
(多个参数) -> 返回类型
3. 函数复合
- f(g(x)) 函数传入函数
//定义两个函数
val add5 = { i: Int -> i + 5 }
val multiplyBy2 = { i: Int -> i * 2 }
fun main() {
println(multiplyBy2(add5(9)))
}
-----打印出来的Log
28
上面是基本的展示,函数中传入函数。
下面扩展一下函数:
//定义三个函数
val add5 = { i: Int -> i + 5 }
val multiplyBy2 = { i: Int -> i * 2 }
val sum = { q: Int, w: Int -> q + w }
//关键点1:
infix fun Function1.andThen(function: Function1): Function1 {
return fun(p1: P1): R {
return function.invoke(this.invoke(p1))
}
}
// 关键点2:
infix fun Function1.compose(function:Function1):Function1{
return fun (p1:P1):R{
return this.invoke(function.invoke(p1))
}
}
//关键点3:
fun Function2.toAllSum(
function: Function1,
function1: Function1
): Function2 {
return fun(p1: P1, p2: P2): R {
return this.invoke(function.invoke(p1), function1.invoke(p2))
}
}
fun main() {
// 关键点3:
val add5AndMulti2 = add5 andThen multiplyBy2
// 关键点4:
val add5ComposeMulti2 = add5 compose multiplyBy2
//关键点5:
val sum = sum.toAllSum(add5, multiplyBy2)
println(add5AndMulti2(10))
println(add5ComposeMulti2(10))
println(sum(10,10))
}
-----打印出来的Log
30
25
35
上面实际上就是扩展函数,然后在函数中传入函数跟返回函数,只有一个参数的则使用了
infix
中缀关键字。关键点1、2、3都是扩展了函数类型,其中关键点1跟2 扩展函数类型为传入一个函数参数,关键点3扩展函数传入两个函数参数
举例:关键点1:函数类型为
Function
扩展函数andThen
,传入函数类型Function1
,返回函数类型Function1
第一个return
:返回函数类型为fun(p1: P1): R
第二个return
:function.invoke(this.invoke(p1))
,先是传入this.invoke(p1)
再将这里返回的值传入
function.invoke()
.
先调用了了andThen
前的函数,再调用andThen
后面的函数。
大家可以根据这些写法自定义多种扩展函数~~
4. run、let、with、apply、also等语法糖部分解析
- 先以
run
为例子
//方法一
public inline fun run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
//方法二
public inline fun T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
方法一函数签名:run(block: () -> R): R
直接传入代码块,并返回R
方法二函数签名:T.run(block: T.() -> R): R
,传入的代码块block: T.() -> R
,也就是调用者本身的引用,则在block
中则直接可以使用T
中的成员变量及函数等
//使用
var sum = run { 5+3 }
println(sum)
var aList = arrayListOf("小明", "小红", "小黑")
var aListSize = aList.run { size }
println(aListSize)
--------------------打印出来的
8
3
这个
contract {...}
看不懂可以暂时不用管它,kotlin
中契约的一种写法,详情可以看一下https://kotlinlang.org/docs/reference/whatsnew13.html#contracts
-
with
、apply
、also
、let
public inline fun with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
public inline fun T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
public inline fun T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
public inline fun T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
-
with
:接受两个参数,一个是自己本身,一个block: T.() -> R
,返回return receiver.block()
;
with
用法:
var aList = arrayListOf("小明", "小红", "小黑")
var l = with(aList){
add("小黄")
removeAt(0)
forEach {
print("$it、")
}
size
}
println(l)
---------------------打印
小红、小黑、小黄、3
-
apply
:方法的扩展函数,传入block: T.() -> Unit
,返回调用者本身,用法与run
一致,但是最后返回的是调用者本身。 -
also
:方法的扩展函数,传入block: (T) -> Unit
,这里更前面几个方法有点不一样,block
传入了T
这个调用者本身,并且函数最后返回调用者本身。
also
用法:
var aList = arrayListOf("小明", "小红", "小黑")
val sizeFinally = aList.also {
println(it.size)
it.add("小黄")
it.add("小绿")
}.size
println(sizeFinally)
---------打印
3
5
-
let
:方法的扩张函数,传入block: (T) -> R
,let
方法返回R
。
let
用法:
val sizeFinally = aList.let {
println(it.size)
it.add("小黄")
it.add("小绿")
it.size
}
println(sizeFinally)
---------------打印
3
5
补充: 尾递归优化 tailrec
- 将
tailrec
关键字添加到fun
前提示编译器尾递归优化。
尾递归:是递归的一种形式,递归中在调用完自己后没有其他操作的称为尾递归。 - 尾递归与迭代的关系:尾递归可以直接转换成迭代(好吧,其实这个我也不是很清楚~)
//符合尾递归 可以加tailrec 关键字优化
tailrec fun findListNode(head: ListNode?, value: Int): ListNode? {
head ?: return null
if (head.value == value) return head
return findListNode(head.next, value)
}
// 不符合尾递归 因为最后调用完自己还跟n相乘
fun factorial(n:Long):Long{
return n * factorial(n-1)
}
上面的方法中第一种是符合尾递归的形式,这种我们可以加tailrec
关键字,有什么好处呢?
fun main() {
var listNode = ListNode(0)
var p =listNode
for (i in 1..100000) {
p.next = ListNode(i)
p = p.next!!
}
println(findListNode(listNode,99998)?.value)
}
-----------有加了关键字tailrec -打印出来的Log
99998
----------没有加关键字打印出来的Log
Exception in thread "main" java.lang.StackOverflowError
因为加了tailrec
关键字,实际上是优化成了迭代相比递归减低了内存空间的开销。