Kotlin匿名函数和函数类型

本文最后更新于:2022年11月30日 晚上

匿名函数和函数类型

匿名函数

定义时不取名字的函数,我们称之为匿名函数,匿名函数通常整体传递给其他函数,或者从其他函数返回。

匿名函数对Kotlin来说很重要,有了它,我们能够根据需要制定特殊规则,轻松定制标准库里的内置函数。

fun main() {
    val total = "MISSSSSSSYOU".count()
    val count = "MISSSSSSSYOU".count({ letter ->
        letter == 'S'
    })
    println(total)
    println(count)
}

函数类型与隐式返回

匿名函数也有类型,可以直接赋值给函数类型变量。

匿名函数一般不需要return,会隐式或自动返回函数体最后一行语句的结果。

// 定义变量,类型为返回值为String的匿名函数
val blessingFun:()->String = {
    val holiday = "National Day"
    // 自动返回函数体最后一行语句的结果
    "Happy $holiday"
}
// 也可用先声明,后赋值
val blessingFun:()->String
blessingFun = {
    val holiday = "National Day"
    "Happy $holiday"
}

函数参数

匿名函数需要带参数时,参数的类型放在匿名函数的参数定义中,参数名(形参)放在函数定义中。

//               参数类型   返回值类型  参数名(形参)
val blessingFun: (String) -> String = { name ->
    val holiday = "New Year"
    "$name,Happy $holiday!"
}
println(blessingFun("Yorick"))
// 输出
// Yorick,Happy New Year!

it关键字

定义只有一个参数的匿名函数时,可以用it关键字表示参数名。

val blessingFun: (String) -> String = {
    val holiday = "New Year"
    // it关键字表示参数名
    "$it,Happy $holiday"
}
println(blessingFun("Yorick"))
// 输出
// Yorick,Happy New Year!

类型推断

定义变量时,如果已把匿名函数作为变量赋值,就不需要显式指明变量类型。

val blessingFun = {
    val holiday = "National Day"
    "Happy $holiday"
}
println(blessingFun())
// 输出
// Happy National Day

当匿名函数有参数时,也可以省略变量类型,但是在匿名函数內要定义参数类型。

  • 不省略变量类型
    val blessingFun:(String,Int)->String={name,year->
        val holiday = "New Year"
        "$name,Happy $holiday $year!"
    }
    println(blessingFun("Yorick",2022))
    // 输出
    // Yorick,Happy New Year 2022!
  • 省略变量类型
    val blessingFun = { name: String, year: Int ->
        val holiday = "New Year"
        "$name,Happy $holiday $year!"
    }
    println(blessingFun("Yorick",2022))
    // 输出
    // Yorick,Happy New Year 2022!

什么是lambda

我们将匿名函数称为lambda,将它的定义称为lambda表达式,它返回的数据称为lambda结果。

定义参数是函数的函数

函数的参数是另一个函数

fun main() {
    val getRefreshWords = { name: String, sec: Int ->
        "${name}还有${sec}秒刷新!"
    }
    showOnBoard("主宰", getRefreshWords)
}

fun showOnBoard(name: String, getRefreshWords: (String, Int) -> String) {
    val sec = (1..60).shuffled().last()
    println(getRefreshWords(name, sec))
}
// 输出示例
// 主宰还有18秒刷新!

可怕的是,它还有省略写法!

如果一个函数的lambda参数排在最后,或者是唯一的参数,那么括住lambda值参的一对圆括号就可以稍等。

fun main() {
    showOnBoard("主宰") { name: String, sec: Int ->
        "${name}还有${sec}秒刷新!"
    }
}

private fun showOnBoard(name: String, getRefreshWords: (String, Int) -> String) {
    val sec = (1..60).shuffled().last()
    println(getRefreshWords(name, sec))
}
// 输出示例
// 主宰还有18秒刷新!

函数內联

lambda使用方便灵活,但是灵活是有代价的。
在JVM上,你定义的lambda会以对象的实例形式存在,JVM会为所有同lambda打交道的变量分配内存,这就产生了内存开销,这会带来严重的性能问题。幸运的是,kotlin有一种优化机制叫内联,有了内联,JVM就不需要使用lambda对象实例了,因而避免了变量内存分配。哪里需要使用lambda,编译器就会将函数体复制粘贴到哪里。

没有使用内联的Kotlin代码

fun main() {
    val getRefreshWords = { name: String, sec: Int ->
        "${name}还有${sec}秒刷新!"
    }
    showOnBoard("主宰", getRefreshWords)
}

private fun showOnBoard(name: String, getRefreshWords: (String, Int) -> String) {
    val sec = (1..60).shuffled().last()
    println(getRefreshWords(name, sec))
}

反编译成Java的部分代码

public final class AnonymousFunc2Kt {
   public static final void main() {
      Function2 getRefreshWords = (Function2)null.INSTANCE;
      showOnBoard("主宰", getRefreshWords);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   private static final void showOnBoard(String name, Function2 getRefreshWords) {
      byte var3 = 1;
      int sec = ((Number)CollectionsKt.last(CollectionsKt.shuffled((Iterable)(new IntRange(var3, 60))))).intValue();
      Object var4 = getRefreshWords.invoke(name, sec);
      System.out.println(var4);
   }
}

我们发现main函数里只有两行代码,需要调用实例化后的lambda函数才能运行。

在函数showOnBoard的private后加入inline

...
private inline fun showOnBoard(name: String, getRefreshWords: (String, Int) -> String)
...

此时反编译后的Java代码的主方法:

public static final void main() {
    Function2 getRefreshWords = (Function2)null.INSTANCE;
    String name$iv = "主宰";
    int $i$f$showOnBoard = false;
    byte var3 = 1;
    int sec$iv = ((Number)CollectionsKt.last(CollectionsKt.shuffled((Iterable)(new IntRange(var3, 60))))).intValue();
    Object var5 = getRefreshWords.invoke(name$iv, sec$iv);
    System.out.println(var5);
}

我们发现主方法多了很多,这是因为他复制了lambda函数体里的代码,直接运行,不再去实例化lambda函数。

但是,使用lambda递归函数无法内联,因为会导致复制粘贴无限循环,编译会发出警告。

例如,这里的递归函数就不能使用内联。

fun main() {
    add(1, 2, show)
}

var sum = 0
val show: (Int) -> (String) = {
    println(it)
    it.toString()
}

fun add(num1: Int, num2: Int, show: (Int) -> (String)) {
    sum = num1 + num2
    if (sum < 100) {
        show(sum)
        // 递归
        add(num2, sum, show)
    }
}

函数引用

要把函数作为参数传给其他函数使用,除了传lambda表达式,kotlin还提供了其他方法,传递函数引用,函数引用可以把一个具名函数转换成一个值参,可以使用lambda表达式的地方,都可以使用函数引用。

fun main() {
    // ::表示把一个方法当做一个参数,传递到另一个方法中进行使用,通俗的来讲就是引用一个方法。
    showOnBoard("主宰", ::getRefreshWords)
}

fun getRefreshWords(name: String, sec: Int): String {
    return "${name}还有${sec}秒刷新!"
}

private inline fun showOnBoard(name: String, getRefreshWords: (String, Int) -> String) {
    val sec = (1..60).shuffled().last()
    println(getRefreshWords(name, sec))
}

函数类型作为返回值类型

fun main() {
    val getRefreshWords = configRefreshWords()
    println(getRefreshWords("主宰"))
}

fun configRefreshWords(): (String) -> String {
    val sec = (1..60).shuffled().last()
    return { name: String ->
        "${name}还有${sec}秒刷新!"
    }
}
// 输出示例
// 主宰还54有秒刷新!

lambda与闭包

闭包:能够读取其他函数内部变量的函数

在Kotlin中,匿名函数能修改并引用定义在自己的作用域之外的变量。lambda就是匿名函数,所以Kotlin中的lambda就是闭包。

lambda与内部匿名类

我们知道Kotlin可以把函数作为另一个函数的参数,那如何用Java实现呢?

这就要使用Java里的接口和内部类

import java.util.Random;

public class JavaAnonymousClass {
    public static void main(String[] args) {
        showOnBoard("主宰", new MyRefreshWords());
    }

    public interface RefreshWords {
        String getRefreshWords(String name, int sec);
    }

    public static void showOnBoard(String name, RefreshWords refreshWords) {
        int sec = new Random().nextInt(60);
        System.out.println(refreshWords.getRefreshWords(name, sec));
    }

    static class MyRefreshWords implements RefreshWords {
        @Override
        public String getRefreshWords(String name, int sec) {
            return name + "还有" + sec + "秒刷新!";
        }
    }
}

或者使用匿名内部类

import java.util.Random;

public class JavaAnonymousClass {
    public static void main(String[] args) {
        showOnBoard("主宰", new RefreshWords() {
            @Override
            public String getRefreshWords(String name, int sec) {
                return name + "还有" + sec + "秒刷新!";
            }
        });
    }

    public interface RefreshWords {
        String getRefreshWords(String name, int sec);
    }

    public static void showOnBoard(String name, RefreshWords refreshWords) {
        int sec = new Random().nextInt(60);
        System.out.println(refreshWords.getRefreshWords(name, sec));
    }
}

Java8 以后支持lambda表达式,那么main方法可以写成:

public static void main(String[] args) {
    showOnBoard("主宰", (name, sec) -> name + "还有" + sec + "秒刷新!");
}

可以看出,还是kotlin的简洁,函数类型可以让开发者少写模式代码,写出更灵活的代码。


Kotlin匿名函数和函数类型
https://yorick-ryu.github.io/Kotlin/Kotlin匿名函数和函数类型/
作者
Yorick
发布于
2022年9月5日
许可协议