Kotlin 函数

声明

kotlin 中使用 fun 关键字声明函数。我们可以通过可见性修饰符/访问控制符(public 、protected 、internal 、private)来控制函数的使用范围。如果没有显示的使用 可见性修饰符/访问控制符,则默认为 public 的。

普通的函数定义方式如下:

  1. [访问控制符] fun 函数名([参数列表])[: 返回值类型]{
  2. 函数体
  3. }

方括号中的内容都是可以省略的,可以在需要的时候定义。

在 kotlin 的函数中,所有的函数都是有返回值的,但是当我们不需要返回内容的时候,可以给它设置 Unit 作为返回值,当一个函数的返回值是 Unit 的时候,是可以省略的。比如:

  1. fun reutunNull(): Unit {
  2. println("Hello World")
  3. }
  4. // 或者
  5. fun reutunNull() {
  6. println("Hello World")
  7. }

参数

kotlin 函数的参数,采用 name:type 定义方式,参数之间使用逗号分开,每个参数必须有显示的类型,如下:

  1. fun add(one: Int, other:Int):Int {
  2. return one + other
  3. }

默认参数

函数的参数可以有默认值。比如:

  1. fun sayHello(name: String = "World"){
  2. println("Hello , $name")
  3. }

上面的 sayHello 函数就有一个默认的 name 参数,其类型为 String ,默认值为 World 。在调用的时候,如果只想输出 Hello ,World ,只需要直接调用,只有在需要更改 name 值得时候才需要,设置参数内容。

  1. fun main() {
  2. // 直接输出 Hello ,World
  3. sayHello()
  4. // 输出 Hello ,Ricky
  5. sayHello("Ricky")
  6. }

需要注意的是:

父类中有默认参数的函数,如果子类需要重写这个函数,则其重写的参数不能有默认值。

具名参数

在调用函数的时候,我们可以通过特定参数的名字进行传值。

  1. fun sayHello(name1: String, name2: String, name3: String) {
  2. println("Hello ! $name1 、$name2 and $name3")
  3. }
  4. fun main() {
  5. sayHello(name1 = "Ricky", name2 = "Jim", name3 = "Jack")
  6. }

上面的代码就是在调用 sayHello() 函数时,通过指定参数的名称的方式传值。

需要注意:

  1. 如果在函数中使用具名的参数,参数的顺序是可以更改的;

    1. 由于在上面一条规定的前提下,如果函数中有一个使用具名的参数,其它的所有的参数都必须使用具名参数,否者编译器会无法分辨参数顺序,因为它不知道函数的顺序是否在调用的时候发生了改变;
    2. 如果一个函数有默认参数,且参数数量大于等于2个,在不更改默认参数的前提下,调用时只能通过具名参数的方式调用该参数。具体看代码:
  1. fun main() {
  2. // 如果不更改默认默认参数,则必须使用具名参数
  3. sayHello(name2 = "Jim", name3 = "Jack")
  4. // 如果给每个参数都传值,则不需要关心默认值
  5. sayHello("Ricky","Jim","Jack")
  6. }
  7. fun sayHello(name1: String = "World", name2: String, name3: String) {
  8. println("Hello ! $name1 、$name2 and $name3")
  9. }
  1. 当函数调用时,普通的位置参数与具名参数混用时,所有的具名参数都要放到位置参数后面,具体看代码: ```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”) }

  1. <a name="708af124"></a>
  2. ### 可变参数
  3. kotlin 函数支持可变的同类型的参数的传入。使用 `vararg` 修饰 。
  4. ```kotlin
  5. fun main() {
  6. sayHello("Ricky", "Jim", "Jack", "Michael")
  7. }
  8. fun sayHello(vararg names: String) {
  9. names.forEach {
  10. print(it)
  11. }
  12. }

在函数内部,vararg 修饰参数,会转变成特定类型的数组,比如上面的代码,names 其实就是一个 String 类型的数组。

虽然可变参数可以看成一个数组,但是我们不能直接传入一个数组,需要做一个转化。我们可以在参数前面加 * ,这样既可以传入一个数组了。

  1. fun main() {
  2. val names = arrayOf("Ricky", "Jim", "Jack")
  3. sayHello(*names)
  4. }
  5. fun sayHello(vararg names: String) {
  6. names.forEach {
  7. print(it)
  8. }
  9. }

一个函数如果有普通的参数和可变参数,可变参数一定要放到最后。

嵌套函数

嵌套函数即函数内部再定义一个函数,也叫作局部函数。

  1. fun compare(x: Int, y: Int): Boolean {
  2. fun power(y: Int): Int {
  3. return y * y
  4. }
  5. val power = power(y)
  6. return x > power
  7. }
  8. fun main() {
  9. println(compare(4, 2))
  10. // 下面的这行会报错,无法直接调用嵌套函数
  11. // power(2)
  12. }

上面的函数就是典型的嵌套函数。比较 x 跟 y 的平方哪个值大。

嵌套函数有个特点:

嵌套函数默认对外界是隐藏的,但它的外层函数可以调用和使用它。以上面的代码为例, power 是嵌套函数,compare 是它的外层函数,在compare 中可以调用 power ,但是在其它地方不可以调用 power

函数小结:

  • 函数声明是 fun 关键字

  • 函数可以通过可见性修饰符/访问控制符(public 、protected 、internal 、private)来控制函数的使用范围

  • 如果没有显示的使用 可见性修饰符/访问控制符,则默认为 public

  • 函数的参数可以在定义的时候,给其赋默认值;

  • 调用函数的时候,我们可以通过具名参数的方式进行传值

    • 如果在函数中使用具名的参数,参数的顺序是可以更改的;

    • 由于在上面一条规定的前提下,如果函数中有一个使用具名的参数,其它的所有的参数都必须使用具名参数,否者编译器会无法分辨参数顺序,因为它不知道函数的顺序是否在调用的时候发生了改变;

    • 如果一个函数有默认参数,且参数数量大于等于2个,在不更改默认参数的前提下,调用时只能通过具名参数的方式调用该参数;

  • 函数的可以接收可变参数,使用vararg 修饰参数即可

  • 可变参数可以看成是一个数组,使用 * 可以将一个数组传入带有可变参数的函数中

  • 一个函数如果有普通的参数和可变参数,可变参数一定要放到最后

  • 函数内部再定义一个函数,叫做嵌套函数,也叫作局部函数。

Kotlin 高阶函数

kotlin 的函数都是头等公民。即函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

在kotlin中一切皆对象,函数类型其实也是一种对象,即函数类型的对象。

高阶函数是将函数用作参数或返回值的函数。

函数的类型

每个函数都有自己的类型,由函数的参数和返回类型组成。我们看下面的代码:

  1. fun add(one: Int, other:Int):Int {
  2. return one + other
  3. }

上面的函数传入的两个参数都是 Int 类型,返回值也是 Int 类型,所以这个函数的类型是 (one: Int, other:Int)->Int 。其中括号里的是参数的类型,-> 后面的是返回值得类型。因为函数的参数名在定义时没有实际意义,我们也可以把参数名称省略,直接写成 (Int, Int)->Int

如果函数没有参数和返回值其类型为 ()->Unit

函数类型可以有一个额外的接收者,即 我们可以有一个对象来接收定义的函数类型,就好像我们给一个已知的对象,在不修改其内容的情况下,给它添加一个额外的函数;就类似扩展函数一样。接收者类型需要在 点之前指定,定义的模式如下: 接收者类型.(参数类型)->返回类型 。比如:A.(B)->C

函数类型的使用

我们定义了函数的类型,但是如何使用呢?前面说过,函数可以作为参数,也可以作为范围值。

首先我们需要确定的是,函数类型也是一种类型跟 IntFloat 类似,我们首先需要声明出来,实例化后才可以使用。

比我们有一个函数类型为 ()->Unit 参数为空,返回值也是空的,然后我们类比一下 Int 的 声明过程:

  1. val num: Int

我们声明了一个 Int 对象,同样的我们给函数类型 (Int)->Unit 也声明一下:

  1. val fly: (Int) -> Unit

这样我们声明了一个 (Int) -> Unit 的函数类型,其名称为 fly

既然声明函数类型,接下来我们需要给其赋值:

  1. val fly: (Int) -> Unit = {
  2. println("I can fly at 100 kilometers per hour")
  3. }

= 后面我们使用了 lambda 表达式,将 {} 内的代码赋值给 函数类型为 (Int) -> Unit,名为 fly 的变量 。这里有没有很像给对象的实例化呢?上面我们说过 在kotlin中一切皆对象,函数类型其实也是一种对象,即函数类型的对象。 这里正好印证了函数也是对象的说法

函数类型实例化,我们可以把它看做是名为 fly 的函数:

  1. fun fly(speed: Int) {
  2. println("I can fly at 100 kilometers per hour")
  3. }

既然是函数,我们就可以像下面这样调用:

  1. fun main() {
  2. val fly: (Int) -> Unit = {
  3. println("I can fly at $it kilometers per hour")
  4. }
  5. //
  6. fly(100)
  7. }

带有接收者的函数类型

前面说到函数类型可以有一个额外的接收者,就像扩展函数一样。我们可以像如下一样定义函数类型: 接收者类型.(参数类型)->返回类型 。比如:A.(B)->C

现在我们有一个 Airplane 的类型,我们让它作为接收者:

  1. class Airplane
  1. // Airplane作为接收者,函数类型为 () -> Unit
  2. val fly: Airplane.() -> Unit = {
  3. println("This plane flies can fly")
  4. }

带有接收者的函数类型,调用稍微有些区别,我们需要先实例化Airplane ,然后把它的引用作为接收者。

  1. fun main() {
  2. val airPlane = AirPlane()
  3. airPlane.fly()
  4. }

调用已有的函数类型

有些时候,我们可能会用到已经定义的好的函数,可能会作为参数,也有可能作为返回值。

首先,我们先看一下,我们如何查看一个函数的类型。如果要获取函数的类型,那么就需要用到双冒号 ::

  1. fun fly(speed: Int) {
  2. println("I can fly at $speed kilometers per hour")
  3. }
  4. fun main() {
  5. val fly = ::fly
  6. println(fly)
  7. }

输出:

  1. fun fly(kotlin.Int): kotlin.Unit

在使用 :: 调用函数之后,其实是将函数提升为对象。这样我们可以很容的获取其类型,也可以作为参数传入其它的函数。在提升为对象之后,它会有一个父接口 KCallable 。官方给出的注释是,表示可调用的实体,如函数或属性。 它还有一个泛型的 <out R> 表示返回的类型。

函数作为参数

函数作为参数时,形参传入的其实是函数的类型,也就是说传入的是一个对象,而不是单纯的函数,如果要传入一个已知的函数,我们需要添加 :: 将函数提升为对象 ,比如:

  1. fun add(x: Int, y: Int): Int {
  2. return x + y
  3. }
  4. // 传入函数add的类型 (Int, Int) -> Int
  5. fun calculate(multi: Int, add: (Int, Int) -> Int): Int {
  6. // 计算 multi * (1 + 2)
  7. return multi * add(1, 2)
  8. }
  9. fun main() {
  10. // 使用 :: 提升add函数为对象,用于作为参数传入
  11. val result = calculate(3, ::add)
  12. println(result)
  13. }

高阶函数小结:

  • 高阶函数是将函数用作参数或返回值的函数。
  • 每个函数都有自己的类型,由函数的参数和返回类型组成
  • 函数类型是一个对象
  • 函数类型可以有一个接收者
  • 可以使用 :: 获取一个已有函数的 函数类型

Lambda 表达式

lambda 表达式本质上其实就是匿名函数。我们可以把 lambda 理解成简化后的匿名函数。

匿名函数

匿名函数就是没有名字的函数,我们在定义函数的时候,不给它指定名字就会得到一个匿名函数。

  1. fun main() {
  2. // 这里声明了一个函数,但是没有指定名字,这个就是匿名函数
  3. val sum = fun(x:Int,y:Int):Int{
  4. return x+y
  5. }
  6. println(sum(1,2))
  7. }

既然 lambda 是用来简化匿名函数的,那么上的代码可以如何简化呢?

多个参数匿名函数转化为 lambda

函数的三个要素:方法名、参数列表、返回值。那么我们可以简化哪些内容呢?首先我们的是用来简化的是匿名函数,那么方法名可定不用了,同时我们还可以把 fun 关键字简化掉。

那么我们就会有这样一段代码:

  1. val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

先对上面的代码做一些说明:

  • 其中 {} 内的内容就是 lambda 表达式 ;
  • = 前面,: 后面是 函数类型 ,关于这个在上面的函数类型中提到过;
  • lambda 表达式的返回值是最后一行表达式的结果;
  • sumlambda 变量,我们通过 sum 就可以调用该 lambda 表达式 ;
  • {} 中的 x: Int, y: Intlambda 参数以及其类型。

上面就是一段将匿名函数转化为 lambda 表达式的标准形式。

Kotlin 的变量有类型推断的特性,在这里 Kotlin 同样有类型推断的特性。Kotlin 可以通过匿名函数的参数列表和返回值推断出其函数类型,那么我们就可以把 (Int, Int) -> Int 这个函数类型省略掉,于是就成了下面这样:

  1. val sum = { x: Int, y: Int -> x + y }

其中 {}lambda -> 前面的内容为匿名函数的参数,后面的为具体的需要执行的代码。

还有一种情况,lambda 变量声明了函数类型,那么 lambda 的参数部分的类型就可以省略了:

  1. // {} 中只有参数的名称,并没有参数类型,因为我们在lambda变量中已经声明了参数类型,在 lambda 表达式中就可以可省略了
  2. val sum2: (Int, Int) -> Int = { x, y -> x + y }

无参数匿名函数转化为 lambda

上面是带有参数的匿名函数转化为 lambda 的最简化方式了,那么没有参数的匿名函数呢?比如下面的代码:

  1. val echo = fun(){
  2. println("Hello World")
  3. }

首先我们将其转换为 lambda 的标准形式:

  1. val echo:()->Unit = { println("Hello World") }

然后简化掉函数类型:

  1. 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个参数。

我们以下面的代码为例:

  1. val echo = { println("Hello World") }
  2. val sum = { x: Int, y: Int -> x + y }
  3. echo()
  4. println(sum(2,2))

将其反编译成 Java代码(其中删除一些多余不影响逻辑的代码):

  1. // 将没有参数的 echo 转换成 Function0
  2. Function0 echo = (Function0)null.INSTANCE;
  3. // 执行Function0 的 invoke() ,来执行 println()
  4. echo.invoke();
  5. // 将两个参数的 sum 转换成 Function2
  6. Function2 sum = (Function2)null.INSTANCE;
  7. // 执行Function2 的 invoke 方法来执行 x+y 的操作
  8. int var2 = ((Number)sum.invoke(2, 2)).intValue();
  9. System.out.println(var2);

我们简单看一下 Function 的源码:

  1. package kotlin.jvm.functions
  2. /** A function that takes 0 arguments. */
  3. public interface Function0<out R> : Function<R> {
  4. /** Invokes the function. */
  5. public operator fun invoke(): R
  6. }
  7. /** A function that takes 1 argument. */
  8. public interface Function1<in P1, out R> : Function<R> {
  9. /** Invokes the function with the specified argument. */
  10. public operator fun invoke(p1: P1): R
  11. }
  12. /** A function that takes 2 arguments. */
  13. public interface Function2<in P1, in P2, out R> : Function<R> {
  14. /** Invokes the function with the specified arguments. */
  15. public operator fun invoke(p1: P1, p2: P2): R
  16. }
  17. /** A function that takes 3 arguments. */
  18. public interface Function3<in P1, in P2, in P3, out R> : Function<R> {
  19. /** Invokes the function with the specified arguments. */
  20. public operator fun invoke(p1: P1, p2: P2, p3: P3): R
  21. }
  22. ....................
  23. /** A function that takes 21 arguments. */
  24. 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> {
  25. /** Invokes the function with the specified arguments. */
  26. 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
  27. }
  28. /** A function that takes 22 arguments. */
  29. 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> {
  30. /** Invokes the function with the specified arguments. */
  31. 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
  32. }

其中省略了一部分,我们可以看到每个 Function 都有一个 invoke 方法,它负责执行具体的代码。。其实在我们调用 lambda 表达式的时候是需要调用其 invoke 方法的,只是kotlin 帮我们省略掉了,我们也可显式的调用 invoke 方法,其效果也是一样的。

  1. val echo = { println("Hello World") }
  2. val sum = { x: Int, y: Int -> x + y }
  3. echo.invoke()
  4. println(sum.invoke(2,2))

lambda 小结:

  • lambda 表达式本质上其实就是匿名函数
  • lambda 在Java层会转换成Function类型,lambda执行时其实执行的是 Function的invoke方法

闭包

一段程序代码通常由常亮、变量、和表达式组成,然后使用一对{} 来表示闭合,并包裹这些代码。由这对 {} 包裹的代码如果访问了外部的环境变量则被称为一个闭包。全局和嵌套函数就是一种特殊的闭包。Kotlin语言中有三种闭包形式:全局函数、自嵌套函数、匿名函数体参考自 北漂周 “什么是闭包”

一个闭包可以直接使用也可以被当做参数使用

闭包可以简单看成能够读取其他函数内部变量的函数

闭包有两大用途:

  • 读取函数内部的变量;
  • 让这些变量的值始终保持在内存中。

我们知道变量的作用域只有两种:全局变量和局部变量。闭包可以读取函数内部的变量,并不是所有的函数都可以读取其它函数内部的变量。比较典型的是 自嵌套函数、匿名函数(lambda) 。

  1. fun justCount(): () -> Unit {
  2. var count = 0
  3. // 闭包,相当于返回了一个函数,其类型为 () -> Unit
  4. return {
  5. println(count++)
  6. }
  7. }
  8. fun main() {
  9. val count = justCount()
  10. count() // 输出 0
  11. count() // 输出 1
  12. count() // 输出 2
  13. count() // 输出 3
  14. }

上面的代码采用 lambda 表达式的方式创建了闭包。我们知道 lambda 表达式实质上还是一个匿名函数,我们在其内部访问了外部函数类型的变量 count 。其中 count 值并不会在执行完一次之后被回收,而是始终保持在内存中,每当我们调用时都会进行累加。

闭包在函数创建的时候创建,它有一个私有的作用域,并且能访问其外层的函数的作用域。我们可以将每个模块拆分到不同的函数里。不同的函数里的变量保持相互调用的可能性,并且彼此相互独立互不影响。

我们将上面的代码反编译成 java 代码看一下:

  1. public static final class IntRef implements Serializable {
  2. public int element;
  3. @Override
  4. public String toString() {
  5. return String.valueOf(element);
  6. }
  7. }
  8. @NotNull
  9. public static final Function0 justCount() {
  10. final IntRef count = new IntRef();
  11. count.element = 0;
  12. return (Function0)(new Function0() {
  13. // $FF: synthetic method
  14. // $FF: bridge method
  15. public Object invoke() {
  16. this.invoke();
  17. return Unit.INSTANCE;
  18. }
  19. public final void invoke() {
  20. IntRef var10000 = count;
  21. int var1;
  22. var10000.element = (var1 = var10000.element) + 1;
  23. boolean var2 = false;
  24. System.out.println(var1);
  25. }
  26. });
  27. }
  28. public static final void main() {
  29. Function0 count = justCount();
  30. count.invoke();
  31. count.invoke();
  32. count.invoke();
  33. count.invoke();
  34. }

这里又出现了Function 类型,它本身是个接口。我们可以看到,这里通过 Function0 接口的匿名实现类实现了闭包的功能。将count变量的值始终保持在 IntRef 类中,我们每次调用闭包其实是调用其匿名内部类的 invoke 方法。

总结:

  • kotlin 中使用 fun 关键字声明函数,函数是头等的,可以作为返回值也可以做参数
  • 函数接受默认参数,具名参赛,可变参数
  • 函数在定义的时候可以嵌套
  • 每个函数都是有类型的,函数类型是一个对象
  • 函数类型可以有接收者
  • lambda 表达式是对匿名函数的简化操作
  • 闭包最重要的特点是:可以访问其它函数中的变量