类和对象学习完成之后,我们就来到了函数和Lambda表达式啊的学习中了。本节内容与Java部分相差相对较大,所以需要我们仔细的学习学习
-
函数
-
函数声明
用fun关键字进行声明
-
函数用法
包级函数直接访问,对象内的函数需要初始化对象之后使用点表示法进行调用
-
中缀表示法
这个我觉得比较好玩,我们在Kotlin中很多位运算符,其实都是中缀表示法,来看看源码
public infix fun shl(bitCount: Int): Int
public infix fun shr(bitCount: Int): Int
public infix fun ushr(bitCount: Int): Int
public infix fun and(other: Int): Int
public infix fun or(other: Int): Int
public infix fun xor(other: Int): Int
这里最明显的莫过于infix关键字了,以or函数为例看看传统写法与中缀写法的区别
val value1 = 0b1111.or(0b1100)
println(value1)
val value = 0b1111 or 0b1100
println(value)
原来就是用函数名来将前后2个参数关联起来了
中缀表示法的要求是
(1) 他们是成员函数或扩展函数
(2) 他们只有一个参数
(3) 他们用infix关键字标注
自己写一个看看呢,取2个Int里面的较小值
infix fun Int.min(value: Int) : Int {
return if (this > value) value else this
}
println(3 min 2)
-
参数
函数参数使用Pascal表示法定义,即name:type。参数用逗号分隔开,并且每个参数都必须有显式类型
-
默认参数
函数的参数可以有默认值,当调用时省略相应的参数,那么函数就会使用默认值进行操作。
fun a(value: Int = 2) {
println("$value")
}
默认值通过类型后面的等号给出
当覆盖一个带有默认参数值的函数时,必须从函数签名中省略默认参数值
open class A {
open fun a(value: Int = 1) {}
}
class B : A() {
override fun a(value: Int) {
super.a(value)
}
}
-
命名参数
当一个函数有大量参数或者默认参数的时候,我们可以用它来增强可读性,就像这样
a(value = 4)
当然你想给哪个参数加命名这是你的自由,不需要所有参数都加命名的。
请注意,在调用Java函数时不能使用命名参数
-
返回Unit函数
如果一个函数不需要返回任何有用的值,它的返回类型就是Unit,这个值不需要显式返回
-
单表达式函数
当函数返回单个表达式时,可以省略花括号并且在等号之后指定代码体即可
fun c() = 1+1
-
显式返回类型
具有块代码体的函数必须显式指定返回类型,除非是Unit,在这种情况下它才是可选的
-
可变数量的参数
函数的参数(通常是最后一个)可以用vararg修饰符来标记可变参数,来看看我们之前一直用的arrayOf函数的源码
public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T): Array<T>
我们在使用过程中用逗号分割传递的参数
var list = arrayOf(2, 3, 5)
如果vararg参数不是最后一个参数,我们就需要使用命名参数语法来传递其后的参数的值
fun d(name: Int, vararg value: Int, other: Int) {
}
d(2, 1, 2, 4, other = 1)
由于第一个参数的类型还有位置是明确的,所以可以自动判断vararg的开始位置,结束位置就要手动告知了,因为类型不一定一致
如果我们之前已经有了一个数组,并希望能调用这个函数的时候,我们就可以用spread操作符(在数组前加*)了
var list = arrayOf(2, 3, 5)
var list2 = arrayOf(1, *list, 4)
-
函数作用域
Kotlin中的函数可以在文件顶层进行声明,这点Java是做不到的,除此之外,Kotlin还可以声明在局部函数、作为成员函数以及扩展函数。
局部函数即一个函数在另外一个函数内部
fun e1() {
val e1_value="e1"
fun e2() {
println(e1_value)
}
}
局部函数可以访问外部函数(即闭包)的局部变量,如例子中的e1_value
-
尾递归函数
Kotlin支持一种称为尾递归的函数式编程风格,它允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。当一个函数用tailrec修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本
tailrec fun f(start: Int, end: Int, result: Int) : Int {
return if (start >= end) {
result
} else {
f(start+1, end, result+start)
}
}
这段代码就是得到范围的累加结果值,这个代码用传统风格表示就是
fun f1(start: Int, end: Int, result: Int) : Int {
var start_=start
var result_=result
while (start_ < end) {
result_+=start_
start_++
}
return result_
}
要符合tailrec修饰符条件,函数必须
(1) 将其自身调用作为它执行的最后一个操作。如果在递归调用后还有更多代码,那么就不能使用尾递归。
(2) 递归部分不能在try/catch/finally中。
(3) 尾递归目前只支持JVM环境
-
科里化(Currying)
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
对比一下与传统写法的区别
fun efg(a: String, b: String, c: String) {
println("$a + $b + $c")
}
fun efg1(a: String) = fun(b: String) = fun(c: String) {
efg(a, b, c)
}
一眼看上去有点蒙蔽,但是看看如何调用的,或许你就会觉得是那么个意思了
efg1("1")("2")("3")
不过要是你的参数传递不符合要求的话,虽然编译时不会出现问题,但是运行时就不会执行调用代码了,这点需要特别小心
-
高阶函数和Lambda表达式
-
高阶函数
高阶函数是将函数作为参数或返回值的函数。Kotlin的函数中有很多是高阶函数,比如filter过滤,他需要你传入一个T类型,如果返回值为true则放置到一个新集合中保存
public inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
先简单分析一下这个函数,首先入参predicate的函数类型为(T) -> Boolean,所以它是一个带T类型值参数并且返回Boolean类型值的函数。我们需要传递一个这样的函数作为入参。当然你可以选择传递一个函数进来,但是同样你也可以选择传递一个函数代码块,就像这样
val filterList = list.filter({value -> if (value>2) true else false})
for (i in filterList) {
println(i)
}
我们对其进行一层层的精简,
(1)
val filterList = list.filter({value -> value>2 })
(2)
val filterList = list.filter() {value -> value>2 }
(3)
val filterList = list.filter {value -> value>2 }
(4)
val filterList = list.filter {it -> it>2 }
(5)
val filterList = list.filter { it>2 }
简短的描述一下Lambda表达式
(1) Lambda表达式总是被大括号括着的
(2) 入参(如果有的话)在->之前声明
(3) 函数体(如果有的话)在->之后
如果最后一个参数是一个函数,并且传入的是一个Lambda表达式,那么可以将其放在圆括号的外面
第一步if else没什么问题
第二步就是从圆括号中拿出来
第三步,如果Lambda表达式时该函数中的唯一参数,那么这里无需保留圆括号,直接去掉
第四步,如果函数字面值只有一个参数,那么它的声明可以连同"->"一起忽略,而用it来作为入参使用,it是单个参数的隐式名称
这里有一个容易疏忽的地方,在调用Lambda函数的时候,记得使用时一定要带有括号,因为就算你忘记加括号了,它也不会报错,因为编译器当你在使用其对象了
你可以认为Lambda表达式就是一个内部类,我们可以通过打印它的实例方法来证明这个观点
val codeBlock: (String, String) -> (() -> String) = {
a: String, b: String -> {
println(a+b)
a+b
}
}
Lambda对象所拥有的方法
看到里面的$1了吧
-
下划线用于未使用的变量
如果Lambda表达式的参数未使用,那么可以用下划线取代其名称
fun <T> g(value1: T, value2: T, funValue: (T, T) -> Unit) {
funValue(value1, value2)
}
g(1, 2) {_, _ ->
println("没有值进来")
}
-
在Lambda表达式中解构
之前我们对解构有了初步的认识,这里我再简单的说一下,后面章节会详细说明
之前我们学习用map进行遍历的时候,通常是这样做的
val map = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3")
for ((key, value) in map) {
println("$key + $value")
}
这里我们相当于把Map.Entry进行拆分解构成(key, value)
回到Lambda表达式解构中来,我们以系统函数mapValues为例
public inline fun <K, V, R> Map<out K, V>.mapValues(transform: (Map.Entry<K, V>) -> R): Map<K, R>
这里我们传一个函数类型为(Map.Entry<K, V>) -> R的参数transform,对比一下解构前后的代码,很好理解
map.map { entry: Map.Entry<String, String> -> { "${entry.key} + ${entry.value}"} } .forEach { println(it) }
map.map { (key, value) -> { "$key + $value"} } .forEach { println(it) }
-
内联函数
Lambda表达式在编译过程中会被编译成内部类,这样每调用一次Lambda表达式,一个额外的类就会被创建。这样就会带来运行时的额外开销,导致使用Lambda表达式比使用一个直接执行相同代码的函数效率更低。有没有可能让编译器生成跟Java语句同样高效的代码,但还是能够把重复的逻辑抽取到库函数中呢?Kotlin的编译器就可以做到这点。如果使用inline修饰符标记一个函数,该函数在被使用的时候编译器不会生成函数调用代码,而是使用函数实现的真实代码替换每一次的函数调用
-
内联函数运作形式
当一个函数被声明为inline之后,它的函数体是内联的,也就是说函数体会被直接替换到函数被调用的地方,而不是一般情况下的调用。来看一个例子
inline fun i(funValue: (Int) -> Unit) {
println("i fun")
funValue(3)
}
我们对比一下加inline跟不加inline编译出的Java代码,看看有何区别
加inline
不加inline
加inline之后,由Lambda表达式生成的字节码成为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中
在调用内联函数的时候也可以传递函数类型的变量作为参数,就像这样
val funValue: (Int) -> Unit = {
println("${it+100}")
}
i(funValue)
在这种情况下,Lambda的代码在内联函数被调用点是不可用的,因此不会被内联
传递一个函数类型的变量作为参数,而不是Lambda
如果在两个不同位置使用同一个内联函数,但使用的是不同的Lambda,那么内联函数会在每一个被调用的位置被分别内联。内联函数的代码会被拷贝到使用它的两个不同位置,并把不同的Lambda替换到其中
-
非局部返回
在Kotlin中,无限定符的return语句只能用来退出有名称的函数或匿名函数。要退出一个Lambda表达式,我们必须使用一个标签,无标签的return在Lambda表达式内是禁止使用的,因为Lambda表达式不允许强制包含它的函数返回,还是拿刚才的例子来看,我们对照一下
fun i1(funValue: (Int) -> Unit) {
println("i fun")
funValue(3)
}
i1 {
println("${it+100}")
return@i1
}
这里return后面跟着@i1的标签
如果Lambda表达式被传递去的函数类型的变量是内联函数,那么return语句就可以内联,因此无标签的return 在这种情况下是允许的
inline fun i(funValue: (Int) -> Unit) {
println("i fun")
funValue(3)
}
i {
println("${it+100}")
return
}
有些内联函数可能并不在自己的函数体内直接调用传递给它的Lambda表达式参数,而是通过另一个执行环境来调用,比如通过一个局部对象或者一个嵌套函数。这种情况下,在Lambda表达式内,非局部的控制流同样是禁止的。为了标识这一点,Lambda表达式参数需要添加crossinline修饰符
inline fun j(crossinline runFun : () -> Unit) {
val runnable = object : Runnable {
override fun run() = runFun()
}
}
-
具体化的类型参数
有时候我们需要访问参数传给我们的类型,这个在Java中貌似只能一个个的instanceOf去判断了,Kotlin给我们带来一个简单的解决办法,举一个不是很好的例子,意思到了即可
inline fun <reified T> k(value1: T) {
println(value1)
val a="123"
if (a is T) {
println("String")
}
else{
println("UNKNOWN")
}
}
我们给类型参数添加了reified修饰符,这样就可以直接访问T的类型了。
普通的函数(未标记为内联函数的)不能有具体化参数。不具有运行时表示的类型(例如非具体化的类型参数或者类似于Nothing的虚构类型) 不能用作具体化的类型参数的实参。
-
Lambda表达式和匿名函数
一个Lambda表达式或匿名函数是一个"函数字面值",即一个未声明的函数,但立即作为表达式传递
-
函数类型
对于接收另一个函数作为参数的函数,我们必须为该参数指定函数类型,例如之前filter的参数predicate类型就是 (T) -> Boolean
-
Lambda表达式语法
大体上就是之前说得几点,有一个注意点还得说一下。我们之前在“返回”中也提及过了,要想在Lambda表达式中返回一个值,就得给它显式得加上标签以限制return进行返回,否则将隐式得返回最后一个表达式的值。对比一下这2个函数,他们是等价的
val filterList = list.filter { it>2 }
val filterListTemp = list.filter { return@filter it>2 }
或者就是使用匿名函数
val filterListTemp2 = list.filter(fun(value: Int) = value>2)
-
匿名函数
匿名函数看起来非常像一个常规函数声明,除了其名称被省略了。其函数体可以像上面那种表达式或者下面这种代码块
val filterListTemp2 = list.filter(fun(value: Int) : Boolean {return value>2})
参数和返回类型的指定方式与常规函数相同,如果能够从上下文推断出则可以省略不写。更进一步说,如果是表达式函数体,则将自动推断返回类型;如果是代码块函数体,则必须显式指定返回类型
请注意,匿名函数参数总是在括号内传递,只有Lambda表达式才是大括号
最后,一个不带标签的return语句,Lambda表达式内的return将会从包含这个Lambda表达式的函数中返回,匿名函数内的return只会从匿名函数本身返回
-
闭包
一段程序代码通常由常量、变量和表达式组成,然后使用一对花括号“{}”来表示闭合,并包裹着这些代码,由这对花括号包裹着的代码块就是一个闭包
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
fun justCount():() -> Unit {
var count = 0
return {
println(count++)
}
}
我们来调用这个闭包
val count = justCount()
count()
count()
count()
看看结果
闭包执行结果
广义上来说,在Kotlin语言之中,函数、条件语句、控制流语句、花括号逻辑块、Lambda表达式都可以称之为闭包,但通常情况下,我们所指的闭包都是在说Lambda表达式。
-
带接收者的函数字面值
Kotlin提供了使用指定的接收者对象调用函数字面值的功能。在函数字面值的函数体中,可以调用该接收者对象上的函数而无需任何额外的限定符。 这有点类似于扩展函数,它允许你在函数体内访问接收者对象的成员
这样的函数字面值的类型就是一个带有接收者的函数类型
sum: Int.(other: Int) -> Int
使用的时候是这样的
fun h(leftValue: Int, rightValue: Int, sum: Int.(other: Int) -> Int) {
val value=leftValue.sum(rightValue)
println("$value")
}
可以使用匿名函数或者Lambda表达式语法进行调用
val funH= (fun Int.(other: Int): Int {
return this+other
})
h(1, 2, funH)
h(1, 2) {
other -> this+other
}
这里的this代表调用函数时点号左侧传递的接收者参数,也就是这里的leftValue
网友评论