Kotlin学习 - 高阶函数和内联函数

这篇具有很好参考价值的文章主要介绍了Kotlin学习 - 高阶函数和内联函数。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

高阶函数定义

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。

以前开发的时候,入参或返回值都是基本类型或者对象,但是在Kotlin中出现了一个新的类型:函数类型。就是函数也可以像String这种类型一样做入参或者返回值。

函数类型

语句结构:

(String, Int) -> Unit 

->左边的部分是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。

->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit,相当于Java 中的void

实例

前面铺垫了这么久,先来个例子学习下高阶函数

1、高阶函数实例

fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
    return operationFunc(num1, num2)
}

fun plus(num1: Int, num2: Int): Int {
    return num1 + num2
}

fun minus(num1: Int, num2: Int): Int {
    return num1 - num2
}

上面声明了一个对数字操作的函数numOperation,传入了一个函数类型的入参(Int, Int) -> Int对前面输入的数字进行操作。后面plusminus是定义了两个对数字操作的函数,看下使用。

fun main() {
    val plusResult = numOperation(1, 3, ::plus)
    val minusResult = numOperation(7, 2, ::minus)
    println("plusResult:$plusResult | minusResult:$minusResult")
}

//运行结果
plusResult:4 | minusResult:5

::plus::minus这种是一种函数引用方式的写法,通过传入的函数类型参数来决定具体的运算逻辑。

2、Lambda表达式

上面实例中,每次调用高阶函数的时候都还得先定义一个与其函数类型参数相匹配的函数,太复杂了。Kotlin还支持其他多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员引用等。让我们换成Lambda表达式看看。

fun main() {
    val plusResult1 = numOperation(2, 3) { n1, n2 -> n1 + n2 }
    val plusResult2 = numOperation(5, 2) { n1, n2 -> n1 - n2 }
    println("plusResult1:$plusResult1 | plusResult2:$plusResult2")
}
//运行结果
plusResult1:5 | plusResult2:3

换成Lambda表达式就简单多了。我们都知道,Kotlin 的代码最终还是要编译成Java 字节码的,但Java 中并没有高阶函数的概念。所以高阶函数是怎么实现的呢?让我们看看字节码反编译后的Java代码:

public static final void main() {
	int plusResult1 = numOperation(2, 3, (Function2)null.INSTANCE);
	int plusResult2 = numOperation(5, 2, (Function2)null.INSTANCE);
}

public static final int numOperation(int num1, int num2, @NotNull Function2 operationFunc) {
	Intrinsics.checkNotNullParameter(operationFunc, "operationFunc");
	return ((Number)operationFunc.invoke(num1, num2)).intValue();
}

首先,先解释下这个Function2,Kotlin内部在Function.kt文件中定义了一系列FunctionX接口,开发者定义的Lambda表达式(匿名函数)在Kotlin底层会通过实现FunctionX接口的方式实现。

确定用哪个FunctionX接口是根据Lambda中定义的入参和返回值个数来决定的。因为我们是两个入参一个返回值,所以是Function2。

public interface Function1<in P1, out R> : Function<R> {
	public operator fun invoke(p1: P1): R
}

public interface Function2<in P1, in P2, out R> : Function<R> {
    public operator fun invoke(p1: P1, p2: P2): R
}

再来看下调用的时候传入的值(Function2)null.INSTANCE,其实是Function2接口类型的匿名内部类的实例。

JVM中,Lambda表达式是以对象实例(匿名内部类)的形式存在,JVM会为所有同Lambda打交道的变量分配内存。因此Lambda表达式虽然用起来简单,但是实现上对内存和性能并不友好。

为了解决这个问题,Kotlin 提供了内联函数的功能。

内联函数inline

内联函数的工作原理:Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方。

使用方式,在高阶函数前面加上关键字inline

inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
    return operationFunc(num1, num2)
}

再次看下字节码反编译后的Java代码:

public static final void main() {
     byte num1$iv = 2;
     int num1$iv = 3;
     int $i$f$numOperation = false;
     int var6 = false;
     //plus函数替换
     int plusResult1 = num1$iv + num1$iv;
     num1$iv = 5;
     int num2$iv = 2;
     int $i$f$numOperation = false;
     int var7 = false;
     //minus函数替换
     int plusResult2 = num1$iv - num2$iv;
     String var9 = "plusResult1:" + plusResult1 + " | plusResult2:" + plusResult2;
     $i$f$numOperation = false;
     System.out.println(var9);
}

直接把代码替换到了引用的地方,这样就是解决了高阶函数Lambda 表达式带来的运行时开销。

注意:使用Lambda的递归函数是无法内联的,会导致复制黏贴无限循环,编译会发出警告。

关键字noinline

高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了inline关键字,那么Kotlin 编译器会自动将所有引用的Lambda 表达式全部进行内联。

如果我们只想内联其中的一个Lambda 表达式的话,就可以使用noinline关键字了,语法格式所示:

inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
} 

这样只会对block1参数所引用的Lambda 表达式进行内联。

1、关键字noinline出现的意义

话说前面已经学习了内联函数的好处,那么为什么Kotlin 还要提供一个noinline关键字来排除内联功能呢?

因为内联的函数类型参数在编译的时候会被代码替换,因此它没有真正的参数属性。内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

上面这段话摘至郭霖大神的《第一行代码》第三版。讲真的我第一次读完,没明白啥意思,写代码试了下才知道,接下来我们先上代码:

fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
    return operationFunc(num1, num2)
}

fun func1(operationFunc: (Int, Int) -> Int) {
    var a = 3;
    var b = 2;
    println("func1 result: " + operationFunc(a, b))
}

我又定义了一个高阶函数func1,入参也是函数类型的,直接复制的numOperation函数中的入参operationFunc,然后我们在numOperation函数中去调用func1试试

fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
 	func1(operationFunc)
    return operationFunc(num1, num2)
}

fun func1(operationFunc: (Int, Int) -> Int) {
    var a = 3;
    var b = 2;
    println("func1 result: " + operationFunc(a, b))
}

上面代码在numOperation函数中去调用func1,将函数类型的入参又转手传给了func1,运行可正常出结果。那如果我们想优化下给numOperation函数加上inline试试

inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
     //func1调用报红
 	func1(operationFunc)
    return operationFunc(num1, num2)
}

fun func1(operationFunc: (Int, Int) -> Int) {
    var a = 3;
    var b = 2;
    println("func1 result: " + operationFunc(a, b))
}

加上inline关键字后,func1报红,并提示错误:Illegal usage of inline-parameter ‘operationFunc’ in ‘public inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int defined in com.jane.kotlinlearning.test in file Test3.kt’. Add ‘noinline’ modifier to the parameter declaration。提示需要使用noinline关键字单独修饰。

先不管这个提示,我们把func1加上inline关键字试试:

inline fun numOperation(num1: Int, num2: Int, operationFunc: (Int, Int) -> Int): Int {
 	func1(operationFunc)
    return operationFunc(num1, num2)
}

inline fun func1(operationFunc: (Int, Int) -> Int) {
    var a = 3;
    var b = 2;
    println("func1 result: " + operationFunc(a, b))
}

果然两个高阶函数同时变为内联函数后报错消失,可正常运行。好,现在我们在回头看看那段话,为方便看我复制过来了。

因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

总结下:

1、非内联的函数它的函数型的入参可以随意再作为参数传递给内部引用到的其他的高阶函数。

2、内联的函数不能随意传递函数型的入参,除非内部引用到的高阶函数全部变为内联函数,或者函数型的入参使用noinline关键字修饰。

上面报错提示我们要用这个关键字,那让我们加上试试

inline fun numOperation(num1: Int, num2: Int, noinline operationFunc: (Int, Int) -> Int
						, print: (Int, Int) -> Unit): Int {
    print(num1,num2)
 	func1(operationFunc)
    return operationFunc(num1, num2)
}

fun func1(operationFunc: (Int, Int) -> Int) {
    var a = 3;
    var b = 2;
    println("func1 result: " + operationFunc(a, b))
}

为了代码更合理,我又加了一个函数类型的入参print,这样operationFunc可以当参数随意传递,print函数进行内联提高运行效率。

noinline关键字的作用就是实现局部内联及函数型参数的传递。

2、return返回区别

上面啰里吧嗦讲了一堆noinline存在的意义。下面再看个内联函数和非内联函数return返回区别。

非内联函数中的Lambda表达式实际就是个匿名内部类,因此在Lambda表达式中return,只能退出Lambda这个闭包,真正调用Lambda的主体函数还是会继续跑。

在内联函数中,相当于将Lambda表达式中的代码复制了一份到主体函数中,变成了主体函数的中一部分,因此直接return的话,主体函数也就直接停掉返回了。

下面看下代码:

fun printContent(content: String, printFunc: (String) -> Unit) {
    println("printContent start")
    printFunc(content)
    println("printContent end")
}

fun main() {
    println("main start")
    var param = ""
    printContent(param) { s ->
        println("lambda start")
        if (s.isEmpty()) return@printContent
        println(s)
        println("lambda end")
    }
    println("main end")
}

Kotlin中,在Lambda表达式中无法直接return,会报错:‘return’ is not allowed here,因此上面代码中使用return@printString表示在Lambda表达式中局部返回。@后面是调用的函数名称,注意return和@之前没有空格。当前例子中的高阶函数是非内联的,打印结果:

main start
printContent start
lambda start
printContent end
main end

从结果看到,Lambda表达式中return后,会返回到printContent函数继续执行。

修改下代码加上内联关键字

inline fun printContent(content: String, printFunc: (String) -> Unit) {
    println("printContent start")
    printFunc(content)
    println("printContent end")
}

fun main() {
    println("main start")
    var param = ""
    printContent(param) { s ->
        println("lambda start")
        if (s.isEmpty()) return
        println(s)
        println("lambda end")
    }
    println("main end")
}

当高阶函数有inline关键字修饰后,可直接调用return返回,在看下执行结果:

main start
printContent start
lambda start

从结果看到,Lambda表达式中return后,整个主函数main完全退出。看下字节码反编译后的Java代码会更清楚:

public static final void main() {
      String param = "main start";
      boolean $i$f$printContent = false;
      System.out.println(param);
      //入参赋值为空串
      param = "";
      $i$f$printContent = false;
      String var2 = "printContent start";
      boolean var3 = false;
      System.out.println(var2);
      int var5 = false;
      String var6 = "lambda start";
      boolean var7 = false;
      System.out.println(var6);
      //var10 变量也是空字符串
      CharSequence var10 = (CharSequence)param;
      var7 = false;
      //判断无法进入
      if (var10.length() != 0) {
         boolean var11 = false;
         System.out.println(param);
         var6 = "lambda end";
         var7 = false;
         System.out.println(var6);
         var2 = "printContent end";
         var3 = false;
         System.out.println(var2);
         String var8 = "main end";
         boolean var9 = false;  
         System.out.println(var8);
     }
}   

因为param = ""所以if语句无法进入,主函数main直接退出了。

关键字crossinline

先看个代码:

fun main() {
	var param = ""
    printContent(param) { s ->
	    if (s.isEmpty()) return
	    println(s)
    }
}

inline fun printContent(content: String, printFunc: (String) -> Unit) {
	val runnable = Runnable {
	    //下面这句代码报红
        printFunc(content)
    }
}

我把上面例子中的打印都给去掉了,这样看代码更清晰。这里我修改下,在内联函数中先新建个匿名内部类Runnable对象,然后printFunc放在对象中执行。

上面代码编译器提示错误:Can’t inline ‘printFunc’ here: it may contain non-local returns. Add ‘crossinline’ modifier to parameter declaration ‘printFunc’。

内联函数所引用的Lambda 表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类Runnable中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回。这样返回是容易引发错误的,Kotlin编译器会提前识别并提示错误,防止开发者没有意识到这个问题。

那你可以会说,我把Lambda 表达式入参中的return去掉呢?也是一样的,编译器会报红,因为无论Lambda 表达式中有无return,Kotlin编译器检查到在内联函数中的匿名内部类中引用了函数型入参就会报错,有潜在的风险。

这里我们锊一下:

1、Lambda表达式不允许使用return,除非这个Lambda表达式是内联函数的参数。

2、在内联函数中的匿名类中不允许调用函数类型参数。

那要是这样的话,是不是就没办法使用内联函数了?那怎么可能,内联函数这么优秀,所以出现了上面编译器中提示的关键字crossinline。加上试试:

fun main() {
	var param = "aaa"
    printContent(param) { s ->
	    //if (s.isEmpty()) return
	    println(s)
    }
}

inline fun printContent(content: String, crossinline printFunc: (String) -> Unit) {
	val runnable = Runnable {	  
        printFunc(content)
    }
}

上面的代码我改了两个地方,第一,把return语句去掉了,第二,在函数型入参前面加上了crossinline关键字。

加上crossinline关键字后,编译器允许你在内联函数的匿名内部类中引用函数型入参,但是,要确保函数型入参中没有return语句。crossinline关键字就像一个契约,它用于保证在内联函数的Lambda 表达式中一定不会使用return关键字。

那要是这样Lambda 表达式想局部退出咋办呢?我们可以使用@printContent的写法进行局部返回。这样想要的操作都有了。文章来源地址https://www.toymoban.com/news/detail-527149.html

到了这里,关于Kotlin学习 - 高阶函数和内联函数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Kotlin Lambda和高阶函数

    本文链接: 1、lambda的由来 单词\\\"lambda\\\"源于希腊字母λ(小写lambda) \\\"Lambda\\\"一词最早起源于数学中的λ演算(lambda calculus),它是一种函数定义和函数应用的形式系统,由逻辑学家阿隆佐·邱奇(Alonzo Church)在20世纪30年代发明。 邱奇使用λ作为演算中的一个操作符,用于表示匿

    2024年02月12日
    浏览(31)
  • Kotlin(十六) 高阶函数的简单应用

    高阶函数非常适用于简化各种API的调用,一些API的原有用法在使用高阶函数简化之后,不管是在易用性还是可读性方面,都可能会有很大的提升。 所以我们可以通过高阶函数来使一些API变得更简单更易读。在我们APP存储数据时,通常会用到SharedPreferences 这个API,那么我们现在

    2024年02月04日
    浏览(31)
  • 【Kotlin】探索回调机制:函数类型、高阶函数和接口

    当在 Kotlin 中开发应用程序时,经常会遇到需要使用回调机制的情况。回调是一种常见的编程模式,用于在异步操作完成后通知调用方,并处理相应的结果或事件。在 Kotlin 中,有几种不同的方法可以实现回调,包括使用函数类型、高阶函数和接口。每种方法都有其优点和适用

    2024年02月02日
    浏览(34)
  • Android kotlin高阶函数与Java lambda表达式介绍与实战

            目前在Java JDK版本的不断升高,新的表达式已开始出现,但是在Android混淆开发中,kotlin的语言与Java的语言是紧密贴合的。所以Java lambda表达式在kotlin中以新的身份出现:高阶函数与lambda表达式特别类似。接下来我讲会先讲Java的lambda,再介绍kotlin的高阶函数。 2.1

    2024年02月15日
    浏览(27)
  • 【Kotlin】函数式编程 ① ( 函数式编程简介 | 高阶函数 | 函数类别 | Transform 变换函数 | 过滤函数 | 合并函数 | map 变换函数 | flatMap 变换函数 )

    编程范式 指的是 使用某种编程语言的 编程套路 或 编程习惯 ; 使用 Java 等高级语言进行的编程 , 编程范式 一般都是 面向对象编程 ; 与 面向对象编程 同等级的另外一种 编程范式 是 函数式编程 , 函数式编程 不依赖于 指定的语言 , 所有的编程语言都可以使用 函数式编程范式

    2024年01月18日
    浏览(34)
  • kotlin基础--快速上手kotlin语言开发

    1.1 变量 var表示可变变量,val表示不可变变量,注意并不是常量。变量名写在前面,类型写在后面,编译器如果能推断出你的类型,那么类型是不用声明的 。 编译器自动推断类型。 空安全类型编译器报错 如果还是想给赋初始化值的话 注意:String和String?是两个完全不同的类

    2024年02月15日
    浏览(36)
  • 【第三阶段】kotlin语言的内置函数takeif

    执行结果

    2024年02月11日
    浏览(27)
  • 学习Kotlin~函数式编程

    高阶函数:以函数为参数或返回函数 函数式编程范式主要依赖于高阶函数返回的数据,这些高阶函数专用于处理各种集合 这些高阶函数可方便的联合多个同类函数构建链式操作以创建复杂的计算行为 函数类别 一个函数式应用通常由三大类构成:变换transform、过滤filter、合并

    2024年02月12日
    浏览(30)
  • 学习Kotlin~函数

    函数参数 不打算传入值参,可以预先给参数指定默认值 有名的函数值参,如果使用命名值参,可以不用管值参的顺序 参数的顺序相反了,也不会影响最终结果。 Unit函数 Kotlin中没有返回值的函数叫Unit函数。 Nothing类型 TODO函数的任务就是抛出异常,就是永远别指望它能运行成

    2024年02月11日
    浏览(23)
  • 23:kotlin类和对象 -- 内联值类(Inline value classes)

    有时,将一个值包装在一个类中可以创建一个更具领域特定类型的类。然而,由于额外的堆分配,这会引入运行时开销。此外,如果包装的类型是原始类型,性能损失是显著的,因为原始类型通常由运行时进行了大量优化,而它们的包装类没有得到任何特殊处理。 为了解决这

    2024年02月03日
    浏览(26)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包