Kotlin 函数
声明
kotlin 中使用 fun
关键字声明函数。我们可以通过可见性修饰符/访问控制符(public 、protected 、internal 、private)
来控制函数的使用范围。如果没有显示的使用 可见性修饰符/访问控制符,则默认为 public
的。
普通的函数定义方式如下:
[访问控制符] fun 函数名([参数列表])[: 返回值类型]{
函数体
}
方括号中的内容都是可以省略的,可以在需要的时候定义。
在 kotlin 的函数中,所有的函数都是有返回值的,但是当我们不需要返回内容的时候,可以给它设置 Unit
作为返回值,当一个函数的返回值是 Unit
的时候,是可以省略的。比如:
fun reutunNull(): Unit {
println("Hello World")
}
// 或者
fun reutunNull() {
println("Hello World")
}
参数
kotlin 函数的参数,采用 name:type
定义方式,参数之间使用逗号分开,每个参数必须有显示的类型,如下:
fun add(one: Int, other:Int):Int {
return one + other
}
默认参数
函数的参数可以有默认值。比如:
fun sayHello(name: String = "World"){
println("Hello , $name")
}
上面的 sayHello
函数就有一个默认的 name
参数,其类型为 String
,默认值为 World
。在调用的时候,如果只想输出 Hello ,World
,只需要直接调用,只有在需要更改 name
值得时候才需要,设置参数内容。
fun main() {
// 直接输出 Hello ,World
sayHello()
// 输出 Hello ,Ricky
sayHello("Ricky")
}
需要注意的是:
父类中有默认参数的函数,如果子类需要重写这个函数,则其重写的参数不能有默认值。
具名参数
在调用函数的时候,我们可以通过特定参数的名字进行传值。
fun sayHello(name1: String, name2: String, name3: String) {
println("Hello ! $name1 、$name2 and $name3")
}
fun main() {
sayHello(name1 = "Ricky", name2 = "Jim", name3 = "Jack")
}
上面的代码就是在调用 sayHello()
函数时,通过指定参数的名称的方式传值。
需要注意:
如果在函数中使用具名的参数,参数的顺序是可以更改的;
- 由于在上面一条规定的前提下,如果函数中有一个使用具名的参数,其它的所有的参数都必须使用具名参数,否者编译器会无法分辨参数顺序,因为它不知道函数的顺序是否在调用的时候发生了改变;
- 如果一个函数有默认参数,且参数数量大于等于2个,在不更改默认参数的前提下,调用时只能通过具名参数的方式调用该参数。具体看代码:
fun main() {
// 如果不更改默认默认参数,则必须使用具名参数
sayHello(name2 = "Jim", name3 = "Jack")
// 如果给每个参数都传值,则不需要关心默认值
sayHello("Ricky","Jim","Jack")
}
fun sayHello(name1: String = "World", name2: String, name3: String) {
println("Hello ! $name1 、$name2 and $name3")
}
- 当函数调用时,普通的位置参数与具名参数混用时,所有的具名参数都要放到位置参数后面,具体看代码: ```kotlin fun main() { sayHello(“Ricky”, name1 = “Jim”, name2 = “Jack”) // 具名参数在前,会报错(Mixing named and positioned arguments is not allowed) sayHello(name1 = “Jim”, “Ricky”, name2 = “Jack”) }
fun sayHello(name: String, name1: String, name2: String = “Jack”) { println(“Hello ! $name 、$name1 and $name2”) }
<a name="708af124"></a>
### 可变参数
kotlin 函数支持可变的同类型的参数的传入。使用 `vararg` 修饰 。
```kotlin
fun main() {
sayHello("Ricky", "Jim", "Jack", "Michael")
}
fun sayHello(vararg names: String) {
names.forEach {
print(it)
}
}
在函数内部,vararg
修饰参数,会转变成特定类型的数组,比如上面的代码,names
其实就是一个 String
类型的数组。
虽然可变参数可以看成一个数组,但是我们不能直接传入一个数组,需要做一个转化。我们可以在参数前面加 *
,这样既可以传入一个数组了。
fun main() {
val names = arrayOf("Ricky", "Jim", "Jack")
sayHello(*names)
}
fun sayHello(vararg names: String) {
names.forEach {
print(it)
}
}
一个函数如果有普通的参数和可变参数,可变参数一定要放到最后。
嵌套函数
嵌套函数即函数内部再定义一个函数,也叫作局部函数。
fun compare(x: Int, y: Int): Boolean {
fun power(y: Int): Int {
return y * y
}
val power = power(y)
return x > power
}
fun main() {
println(compare(4, 2))
// 下面的这行会报错,无法直接调用嵌套函数
// power(2)
}
上面的函数就是典型的嵌套函数。比较 x 跟 y 的平方哪个值大。
嵌套函数有个特点:
嵌套函数默认对外界是隐藏的,但它的外层函数可以调用和使用它。以上面的代码为例, power
是嵌套函数,compare
是它的外层函数,在compare
中可以调用 power
,但是在其它地方不可以调用 power
。
函数小结:
函数声明是
fun
关键字函数可以通过
可见性修饰符/访问控制符(public 、protected 、internal 、private)
来控制函数的使用范围如果没有显示的使用 可见性修饰符/访问控制符,则默认为
public
的函数的参数可以在定义的时候,给其赋默认值;
调用函数的时候,我们可以通过具名参数的方式进行传值
如果在函数中使用具名的参数,参数的顺序是可以更改的;
由于在上面一条规定的前提下,如果函数中有一个使用具名的参数,其它的所有的参数都必须使用具名参数,否者编译器会无法分辨参数顺序,因为它不知道函数的顺序是否在调用的时候发生了改变;
如果一个函数有默认参数,且参数数量大于等于2个,在不更改默认参数的前提下,调用时只能通过具名参数的方式调用该参数;
函数的可以接收可变参数,使用
vararg
修饰参数即可可变参数可以看成是一个数组,使用
*
可以将一个数组传入带有可变参数的函数中一个函数如果有普通的参数和可变参数,可变参数一定要放到最后
函数内部再定义一个函数,叫做嵌套函数,也叫作局部函数。
Kotlin 高阶函数
kotlin 的函数都是头等公民。即函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
在kotlin中一切皆对象,函数类型其实也是一种对象,即函数类型的对象。
高阶函数是将函数用作参数或返回值的函数。
函数的类型
每个函数都有自己的类型,由函数的参数和返回类型组成。我们看下面的代码:
fun add(one: Int, other:Int):Int {
return one + other
}
上面的函数传入的两个参数都是 Int 类型,返回值也是 Int 类型,所以这个函数的类型是 (one: Int, other:Int)->Int
。其中括号里的是参数的类型,->
后面的是返回值得类型。因为函数的参数名在定义时没有实际意义,我们也可以把参数名称省略,直接写成 (Int, Int)->Int
。
如果函数没有参数和返回值其类型为 ()->Unit
。
函数类型可以有一个额外的接收者,即 我们可以有一个对象来接收定义的函数类型,就好像我们给一个已知的对象,在不修改其内容的情况下,给它添加一个额外的函数;就类似扩展函数一样。接收者类型需要在 点之前指定
,定义的模式如下: 接收者类型.(参数类型)->返回类型
。比如:A.(B)->C
。
函数类型的使用
我们定义了函数的类型,但是如何使用呢?前面说过,函数可以作为参数,也可以作为范围值。
首先我们需要确定的是,函数类型也是一种类型跟 Int
,Float
类似,我们首先需要声明出来,实例化后才可以使用。
比我们有一个函数类型为 ()->Unit
参数为空,返回值也是空的,然后我们类比一下 Int
的 声明过程:
val num: Int
我们声明了一个 Int
对象,同样的我们给函数类型 (Int)->Unit
也声明一下:
val fly: (Int) -> Unit
这样我们声明了一个 (Int) -> Unit
的函数类型,其名称为 fly
。
既然声明函数类型,接下来我们需要给其赋值:
val fly: (Int) -> Unit = {
println("I can fly at 100 kilometers per hour")
}
在 =
后面我们使用了 lambda
表达式,将 {}
内的代码赋值给 函数类型为 (Int) -> Unit,名为 fly 的变量
。这里有没有很像给对象的实例化呢?上面我们说过 在kotlin中一切皆对象,函数类型其实也是一种对象,即函数类型的对象。 这里正好印证了函数也是对象的说法
。
函数类型实例化,我们可以把它看做是名为 fly
的函数:
fun fly(speed: Int) {
println("I can fly at 100 kilometers per hour")
}
既然是函数,我们就可以像下面这样调用:
fun main() {
val fly: (Int) -> Unit = {
println("I can fly at $it kilometers per hour")
}
//
fly(100)
}
带有接收者的函数类型
前面说到函数类型可以有一个额外的接收者,就像扩展函数一样。我们可以像如下一样定义函数类型: 接收者类型.(参数类型)->返回类型
。比如:A.(B)->C
现在我们有一个 Airplane
的类型,我们让它作为接收者:
class Airplane
// Airplane作为接收者,函数类型为 () -> Unit
val fly: Airplane.() -> Unit = {
println("This plane flies can fly")
}
带有接收者的函数类型,调用稍微有些区别,我们需要先实例化Airplane
,然后把它的引用作为接收者。
fun main() {
val airPlane = AirPlane()
airPlane.fly()
}
调用已有的函数类型
有些时候,我们可能会用到已经定义的好的函数,可能会作为参数,也有可能作为返回值。
首先,我们先看一下,我们如何查看一个函数的类型。如果要获取函数的类型,那么就需要用到双冒号 ::
。
fun fly(speed: Int) {
println("I can fly at $speed kilometers per hour")
}
fun main() {
val fly = ::fly
println(fly)
}
输出:
fun fly(kotlin.Int): kotlin.Unit
在使用 ::
调用函数之后,其实是将函数提升为对象。这样我们可以很容的获取其类型,也可以作为参数传入其它的函数。在提升为对象之后,它会有一个父接口 KCallable
。官方给出的注释是,表示可调用的实体,如函数或属性。
它还有一个泛型的 <out R>
表示返回的类型。
函数作为参数
函数作为参数时,形参传入的其实是函数的类型,也就是说传入的是一个对象,而不是单纯的函数,如果要传入一个已知的函数,我们需要添加 ::
将函数提升为对象 ,比如:
fun add(x: Int, y: Int): Int {
return x + y
}
// 传入函数add的类型 (Int, Int) -> Int
fun calculate(multi: Int, add: (Int, Int) -> Int): Int {
// 计算 multi * (1 + 2)
return multi * add(1, 2)
}
fun main() {
// 使用 :: 提升add函数为对象,用于作为参数传入
val result = calculate(3, ::add)
println(result)
}
高阶函数小结:
- 高阶函数是将函数用作参数或返回值的函数。
- 每个函数都有自己的类型,由函数的参数和返回类型组成
- 函数类型是一个对象
- 函数类型可以有一个接收者
- 可以使用
::
获取一个已有函数的 函数类型
Lambda 表达式
lambda 表达式本质上其实就是匿名函数。我们可以把 lambda 理解成简化后的匿名函数。
匿名函数
匿名函数就是没有名字的函数,我们在定义函数的时候,不给它指定名字就会得到一个匿名函数。
fun main() {
// 这里声明了一个函数,但是没有指定名字,这个就是匿名函数
val sum = fun(x:Int,y:Int):Int{
return x+y
}
println(sum(1,2))
}
既然 lambda
是用来简化匿名函数的,那么上的代码可以如何简化呢?
多个参数匿名函数转化为 lambda
函数的三个要素:方法名、参数列表、返回值。那么我们可以简化哪些内容呢?首先我们的是用来简化的是匿名函数,那么方法名可定不用了,同时我们还可以把 fun
关键字简化掉。
那么我们就会有这样一段代码:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
先对上面的代码做一些说明:
- 其中
{}
内的内容就是lambda
表达式 ; =
前面,:
后面是 函数类型 ,关于这个在上面的函数类型中提到过;- 该
lambda
表达式的返回值是最后一行表达式的结果; sum
为lambda
变量,我们通过sum
就可以调用该lambda
表达式 ;{}
中的x: Int, y: Int
为lambda
参数以及其类型。
上面就是一段将匿名函数转化为 lambda
表达式的标准形式。
Kotlin 的变量有类型推断的特性,在这里 Kotlin 同样有类型推断的特性。Kotlin 可以通过匿名函数的参数列表和返回值推断出其函数类型,那么我们就可以把 (Int, Int) -> Int
这个函数类型省略掉,于是就成了下面这样:
val sum = { x: Int, y: Int -> x + y }
其中 {}
中 lambda
->
前面的内容为匿名函数的参数,后面的为具体的需要执行的代码。
还有一种情况,lambda
变量声明了函数类型,那么 lambda
的参数部分的类型就可以省略了:
// {} 中只有参数的名称,并没有参数类型,因为我们在lambda变量中已经声明了参数类型,在 lambda 表达式中就可以可省略了
val sum2: (Int, Int) -> Int = { x, y -> x + y }
无参数匿名函数转化为 lambda
上面是带有参数的匿名函数转化为 lambda
的最简化方式了,那么没有参数的匿名函数呢?比如下面的代码:
val echo = fun(){
println("Hello World")
}
首先我们将其转换为 lambda
的标准形式:
val echo:()->Unit = { println("Hello World") }
然后简化掉函数类型:
val echo = { println("Hello World") }
那么我们总结一下lambda 表达式的几个特点:
- lambda 表达式总是被
{}
包裹着; - 如果
lambda
声明了参数部分的类型,且返回值类型支持类型推导,那么lambda
变量就可以省略函数类型声明; - 如果
lambda
变量声明了函数类型,那么lambda
的参数部分的类型就可以省略。
注:
我们看到上面不管匿名函数
还是lambda
表达式,都是可以通过 =
赋值给变量的;类比一下 Int、Float
这些对象,就说明 匿名函数
还是lambda
表达式本质上其实是对象。
Function 类型
我们将上面的 lambda
表达式通过 Kotlin Bytecode
反编译一下,就会发现,lambda
表达式会被转换成 Function
类型,如果其带有没有参数则会被转换成 Function0
,带有一个参数则会被转换成 Function1
,带有两个参数则会被转换成 Function2
, 一直支持到 Function22
,也就是它支持到lambda
表达式有22个参数。
我们以下面的代码为例:
val echo = { println("Hello World") }
val sum = { x: Int, y: Int -> x + y }
echo()
println(sum(2,2))
将其反编译成 Java代码(其中删除一些多余不影响逻辑的代码):
// 将没有参数的 echo 转换成 Function0
Function0 echo = (Function0)null.INSTANCE;
// 执行Function0 的 invoke() ,来执行 println()
echo.invoke();
// 将两个参数的 sum 转换成 Function2
Function2 sum = (Function2)null.INSTANCE;
// 执行Function2 的 invoke 方法来执行 x+y 的操作
int var2 = ((Number)sum.invoke(2, 2)).intValue();
System.out.println(var2);
我们简单看一下 Function
的源码:
package kotlin.jvm.functions
/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}
/** A function that takes 3 arguments. */
public interface Function3<in P1, in P2, in P3, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3): R
}
....................
/** A function that takes 21 arguments. */
public interface Function21<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21): R
}
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}
其中省略了一部分,我们可以看到每个 Function
都有一个 invoke
方法,它负责执行具体的代码。。其实在我们调用 lambda
表达式的时候是需要调用其 invoke
方法的,只是kotlin
帮我们省略掉了,我们也可显式的调用 invoke
方法,其效果也是一样的。
val echo = { println("Hello World") }
val sum = { x: Int, y: Int -> x + y }
echo.invoke()
println(sum.invoke(2,2))
lambda 小结:
- lambda 表达式本质上其实就是匿名函数
- lambda 在Java层会转换成Function类型,lambda执行时其实执行的是 Function的invoke方法
闭包
一段程序代码通常由常亮、变量、和表达式组成,然后使用一对{}
来表示闭合,并包裹这些代码。由这对 {}
包裹的代码如果访问了外部的环境变量则被称为一个闭包。全局和嵌套函数就是一种特殊的闭包。Kotlin语言中有三种闭包形式:全局函数、自嵌套函数、匿名函数体。参考自 北漂周 “什么是闭包” 。
一个闭包可以直接使用也可以被当做参数使用
闭包可以简单看成能够读取其他函数内部变量的函数。
闭包有两大用途:
- 读取函数内部的变量;
- 让这些变量的值始终保持在内存中。
我们知道变量的作用域只有两种:全局变量和局部变量。闭包可以读取函数内部的变量,并不是所有的函数都可以读取其它函数内部的变量。比较典型的是 自嵌套函数、匿名函数(lambda) 。
fun justCount(): () -> Unit {
var count = 0
// 闭包,相当于返回了一个函数,其类型为 () -> Unit
return {
println(count++)
}
}
fun main() {
val count = justCount()
count() // 输出 0
count() // 输出 1
count() // 输出 2
count() // 输出 3
}
上面的代码采用 lambda 表达式的方式创建了闭包。我们知道 lambda 表达式实质上还是一个匿名函数,我们在其内部访问了外部函数类型的变量 count
。其中 count
值并不会在执行完一次之后被回收,而是始终保持在内存中,每当我们调用时都会进行累加。
闭包在函数创建的时候创建,它有一个私有的作用域,并且能访问其外层的函数的作用域。我们可以将每个模块拆分到不同的函数里。不同的函数里的变量保持相互调用的可能性,并且彼此相互独立互不影响。
我们将上面的代码反编译成 java 代码看一下:
public static final class IntRef implements Serializable {
public int element;
@Override
public String toString() {
return String.valueOf(element);
}
}
@NotNull
public static final Function0 justCount() {
final IntRef count = new IntRef();
count.element = 0;
return (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
IntRef var10000 = count;
int var1;
var10000.element = (var1 = var10000.element) + 1;
boolean var2 = false;
System.out.println(var1);
}
});
}
public static final void main() {
Function0 count = justCount();
count.invoke();
count.invoke();
count.invoke();
count.invoke();
}
这里又出现了Function
类型,它本身是个接口。我们可以看到,这里通过 Function0
接口的匿名实现类实现了闭包的功能。将count
变量的值始终保持在 IntRef 类中,我们每次调用闭包其实是调用其匿名内部类的 invoke
方法。
总结:
- kotlin 中使用
fun
关键字声明函数,函数是头等的,可以作为返回值也可以做参数 - 函数接受默认参数,具名参赛,可变参数
- 函数在定义的时候可以嵌套
- 每个函数都是有类型的,函数类型是一个对象
- 函数类型可以有接收者
- lambda 表达式是对匿名函数的简化操作
- 闭包最重要的特点是:可以访问其它函数中的变量