Kotlin高阶探索-Lambda

404 阅读9分钟

      在Kotlin中,函数类型如 () -> Unit(Int, String) -> Boolean 实际上是一些特殊的接口。它们定义了一个 invoke 方法。 举例来说,对于一个无参数和无返回值(Unit)的函数类型 () -> Unit,其实现的接口是这样的:

interface Function0<out R> : Function<R> {
    public operator fun invoke(): R
}

对于下边这段代码:

var method01 : (() -> Unit)?= null
fun main() {
    "Derry".shows()
    "2353453".shows()
    if (method01 != null) {
        method01()
    }
    method01?.invoke() // 调用函数
}

它反编译之后的内容是:

public final class Lambda00Kt {
   @Nullable
   private static Function0 method01;

   @Nullable
   public static final Function0 getMethod01() {
      return method01;
   }

   public static final void setMethod01(@Nullable Function0 var0) {
      method01 = var0;
   }

   public static final void main() {
      Lambda01Kt.shows("Derry");
      Lambda01Kt.shows("2353453");
      if (method01 != null) {
         method01.invoke();
      }

      Function0 var10000 = method01;
      if (var10000 != null) {
         Unit var0 = (Unit)var10000.invoke();
      }

   }

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

        可见无论是()还是invoke,最终的调用都是Function0的invoke函数。当你声明一个 () -> Unit 类型的变量并赋予一个函数或者lambda表达式,你实际上是创建了一个 Function0<Unit> 类型的对象,这个对象有一个 invoke 方法。
        在 Kotlin 中,invoke 是一个特殊的操作符函数。如果类型定义了 invoke 操作符函数,那么该类型的实例可以通过在实例名后面跟 () 来调用 invoke 函数。这就是为什么你可以通过 () 来 "调用" 函数类型的实例,例如 method01()
        这就是为什么 ()invoke() 在这种情况下等价的原因:它们实际上都是调用了 invoke 方法。 但是要注意,这只对那些定义了 invoke 操作符函数的类型适用。并不是所有的 Kotlin 类型都定义了 invoke 操作符函数,对于那些没有定义 invoke 操作符函数的类型,是不能通过 () 来 "调用" 实例的。
带有拓展函数的lambda

fun main() {
    "Derry".shows()
    "2353453".shows()
    val method20 : Int.(Int) -> String = { "两数相加的结果是:${this + it}" }
    println(1.method20(100))
    println(method20(1, 100))
    println(method20.invoke(10,200000))
}

反编译之后的代码:

public final class Lambda00Kt {
   public static final void main() {
      Lambda01Kt.shows("Derry");
      Lambda01Kt.shows("2353453");
      Function2 method20 = (Function2)null.INSTANCE;
      Object var1 = method20.invoke(1, 100);
      System.out.println(var1);
      var1 = method20.invoke(1, 100);
      System.out.println(var1);
      var1 = method20.invoke(10, 200000);
      System.out.println(var1);
   }

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

可见,最终的调用,还是通过invoke函数来进行实现。

函数返回一个函数

fun s04() : () -> Boolean = { true } // () -> Boolean 

        在 Kotlin 中,函数是不可变的。一旦函数被定义,其实现就不能改变。不能像修改变量那样去修改函数的实现。
        如果你想改变 s04 的实现,需要重新定义一个新的 s04 函数,覆盖原有的函数定义。在 Kotlin 中,这可以通过使用不同的上下文(例如不同的类或文件)来实现。然而,对于同一个上下文,不能有两个同名的函数,因此不能直接重写 s04
        但是,如果是在某个类中定义 s04,你可以使用 open/override 机制来实现重写。下面是一个例子:

open class A {
    open fun s04(): () -> Boolean = { true }
}

class B : A() {
    override fun s04(): () -> Boolean = { false } // 重写 s04 的实现
}

        在上面的例子中,B 类重写了 A 类中的 s04 函数。但请注意,需要使用 open 关键字来标记想要重写的函数,以明确表示该函数可以被重写。 如果只是想改变 s04 返回的函数,你可以将 s04 定义为一个变量,然后修改这个变量的值:

var s04: () -> Boolean = { true } // 初始化为返回 true 的函数
s04 = { false } // 然后修改为返回 false 的函数

        在上面的代码中,s04 是一个变量,其类型是 () -> Boolean。你可以修改 s04 的值,从而改变 s04 返回的函数。

函数调用函数

fun s04() : (CharSequence) -> Boolean = { true } // () -> Boolean 函数返回一个函数
println(s04()("11111"))
s04()?.invoke("11111") // 该方式也可以。

        在代码中,s04 是一个函数,这个函数返回另一个函数,这个返回的函数接受一个 CharSequence 参数,并返回一个 Boolean 值。
        fun s04() : (CharSequence) -> Boolean = { true } 这一行定义了 s04 函数。它没有参数,返回类型是 (CharSequence) -> Boolean,也就是一个函数类型,这个函数类型接受一个 CharSequence 参数并返回一个 Boolean 值。{ true } 是函数体,它返回的是一个lambda表达式,这个lambda表达式总是返回 true
        所以,当调用 s04() 时,会得到一个函数,这个函数接受一个 CharSequence 参数并返回一个 Boolean 值。
        然后,可以对这个函数再次调用,传入一个 CharSequence 参数。这就是为什么可以写 s04()("11111"),因为 s04() 返回的是一个函数,然后又调用了这个函数,传入了 "11111" 作为参数。
        println(s04()("11111")) 这一行的意思是:调用 s04 函数得到一个新的函数,然后调用这个新的函数,传入 "11111" 作为参数,并打印这个函数调用的结果。因为 s04 返回的函数总是返回 true,所以这行代码最终会打印 true

函数被定义之后就不能修改他的定义了,所以如果用以下的写法,是错误的:

fun s04() : (() -> Boolean ) ? = null// () -> Boolean 函数返回一个函数
s04 = { true }

但可以这么写:

var s04 : (() -> Boolean)? = null
或者这样写:// val s04:Function1<Unit, Boolean> ?= null
s04 = { true }

下边这个是一个错误的示例,这种方式很不好,不要这么写:

val methodX1 :(String)->Int = fun(str) : Int {
    return (str.length)
}

一些区别:

// fun aa{} 与 var aa={}有啥区别
fun aa() {}
var aa2 = {}
// aa就是一个函数,实打实的函数
// aa2 是接收一个匿名函数的变量而已,这个变量 可以执行这个匿名函数
// 共同点:他们现象是一样的
var aa3 = aa2 // 都属于函数引用的概念
var aa4 = ::aa // 实打实的函数 变成 函数引用 所以可以传递赋值 给另外一个变量

函数的函数的函数的函数的嵌套示例,这也是kotlin的lambda的精髓所在:
例1:

var k01 : ((String)->(CharSequence)->(Long)->(Short)->(Float)->Int)? = null
k01 = {
    {
        {
            {
                {
                    1
                }
            }
        }
    }
}

在看上变这个函数的调用的嵌套层级时,我们需要分段阅读,在kotlin中,是现有输入后有输出,与java想法,因此,我们可以这样,输入是var k01,输出是((String)->(CharSequence)->(Long)->(Short)->(Float)->Int),接着,我们以var k01 : ((String)为输入,(CharSequence)->(Long)->(Short)->(Float)->Int)为输出,依次进行。就可以得到答案。

例2:

fun show(n1:Int, lambda:(Int, Int) -> Unit, n2:Int) = lambda(n1, n2)

// 调用 show 函数,并传入一个 lambda,该 lambda 打印两个参数的和
show(5, { a, b -> println(a + b) }, 3)

学后测验

一、单选题(每题4分)

  1. 下列关于 () -> Unit 这种Kotlin函数类型的说法,正确的是?

    • A. 它本质上是个普通对象
    • B. 它实际上实现了 Function1 接口
    • C. 它实际上实现了 Function0 接口
    • D. 它只能通过 invoke 方法调用
    • 解析:无参数无返回值,底层实现 Function0 接口。
  2. 关于Kotlin中的invoke操作符,下面描述正确的是?

    • A. 只有顶级函数才能用invoke调用
    • B. 只要类型定义了operator fun invoke,就能用()`调用实例
    • C. 所有类型都自带invoke
    • D. 只有lambda类型才能用
    • 解析:任何自定义类型、函数类型,只要定义了invoke操作符,就能用()`调用。
  3. 下列关于拓展函数lambda类型Int.(Int) -> String的调用方式,哪种写法不合法?

    • A. method20(1, 100)
    • B. 1.method20(100)
    • C. method20.invoke(1, 100)
    • D. method20(100, "1")
    • 解析:第一个参数必须是receiver Int(1),第二个是 Int 参数。
  4. 关于Kotlin函数返回函数,下列哪句说法是正确的?

    • A. 可以直接定义函数返回lambda
    • B. 函数返回的lambda只能调用一次
    • C. 返回的lambda无法携带外部状态
    • D. 必须配合类来用
    • 解析:支持函数作为返回值,完全合法且常见。
  5. 下列关于函数引用 var aa4 = ::aa 的理解,正确的是?

    • A. 将已有函数转换成函数类型变量
    • B. 不能再调用
    • C. 只用于顶级函数
    • D. 只能传给lambda参数
    • 解析::aa可以转换为函数类型变量进行引用和传递。

二、判断题(每题3分)

  1. (√)Kotlin函数类型变量赋值后,变量本身可以被修改为指向别的lambda。

    • 解析:如 var f: ()->Unit = {},f可以被重新赋值。
  2. (×)所有Kotlin类型都能直接用 () 调用实例。

    • 解析:只有定义了 operator fun invoke() 的类型才可以。
  3. (√)method01()method01.invoke() 本质上完全等价。

    • 解析:都会调用 FunctionN 的 invoke。
  4. (√)带receiver的lambda(如 Int.(Int)->String)调用时,第一个参数是receiver,第二个是普通参数。

    • 解析:调用方式要分清 receiver 与参数。
  5. (×)Kotlin中lambda表达式返回类型可以自动推断,函数类型变量一定要写明返回类型。

    • 解析:lambda通常可以省略返回类型,但变量声明建议写清楚类型。

三、简答题(每题10分)

  1. Kotlin中为什么() -> Unitinvoke()是等价的?请简述其原理。

    • 答案要点:Kotlin的函数类型(如() -> Unit)底层是Function0接口,定义了operator fun invoke()。如果变量声明为函数类型,可以通过变量名+()`调用本质是调用invoke方法。Kotlin为带有invoke操作符的类型做了语法糖,a()等价于a.invoke()。
  2. Kotlin如何实现“函数返回函数”?实际使用中有什么优势?

    • 答案要点:直接声明函数返回值类型为函数类型即可,例如:fun foo(): ()->Boolean = { true }。优势:可组合、灵活,实现高阶函数、函数式编程范式,更适合抽象和扩展业务逻辑。
  3. 简述在Kotlin中拓展函数类型lambda的调用机制,并给出例子说明参数传递顺序。

    • 答案要点:如 Int.(Int)->String,调用时receiver.method20(param)method20(receiver, param),receiver作为调用者或第一个参数。例如:1.method20(100)method20(1, 100),第一个参数是receiver Int,第二个是Int参数。

四、代码理解题(每题8分)

  1. 阅读以下代码,写出输出结果并说明理由:
var k01 : ((String)->(CharSequence)->(Long)->(Short)->(Float)->Int)? = null
k01 = {
    {
        {
            {
                {
                    1
                }
            }
        }
    }
}
println(k01?.invoke("hello")?.invoke("world")?.invoke(123L)?.invoke(4)?.invoke(2.5f))
  • 答案:输出1。因为每一层都是返回下一个lambda,最后一层lambda返回1,所以多次调用后结果始终为1。

五、开放题(每题8分)

  1. 简要说明fun aa() {}var aa2 = {}的本质区别与使用场景。

    • 答案要点

      • fun aa() {} 是声明了一个顶级函数,函数体不能被替换,属于静态函数。
      • var aa2 = {} 是声明一个变量,类型为函数类型,可以被动态赋新值或更换实现,属于对象。
      • 使用场景:固定行为建议用fun,需要可变行为或函数作为参数/返回值/变量,建议用lambda变量。