kotlin 双冒号到底是什么
2023-12-23
双冒号 ::method 到底是什么?
123 如果你上网搜,你会看到这个双冒号的写法叫做函数引用 Function Reference,这是 Kotlin 官方的说法。但是这又表示什么意思?表示它指向上面的函数?那既然都是一个东西,为什么不直接写函数名,而要加两个冒号呢?
因为加了两个冒号,这个函数才变成了一个对象。
什么意思?
Kotlin 里「函数可以作为参数」这件事的本质,是函数在 Kotlin 里可以作为对象存在——因为只有对象才能被作为参数传递啊。赋值也是一样道理,只有对象才能被赋值给变量啊。但 Kotlin 的函数本身的性质又决定了它没办法被当做一个对象。那怎么办呢?Kotlin 的选择是,那就创建一个和函数具有相同功能的对象。怎么创建?使用双冒号。
在 Kotlin 里,一个函数名的左边加上双冒号,它就不表示这个函数本身了,而表示一个对象,或者说一个指向对象的引用,但,这个对象可不是函数本身,而是一个和这个函数具有相同功能的对象。
怎么个相同法呢?你可以怎么用函数,就能怎么用这个加了双冒号的对象:
a(::b)
val d = ::b
b(1) // 调用函数
d(1) // 用对象 a 后面加上括号来实现 b() 的等价操作
(::b)(1) // 用对象 :b 后面加上括号来实现 b() 的等价操作
但我再说一遍,这个双冒号的这个东西,它不是一个函数,而是一个对象,一个函数类型的对象。
对象是不能加个括号来调用的,对吧?但是函数类型的对象可以。为什么?因为这其实是个假的调用,它是 Kotlin 的语法糖,实际上你对一个函数类型的对象加括号、加参数,它真正调用的是这个对象的 invoke() 函数:
d(1) // 实际上会调用 d.invoke(1)
(::b)(1) // 实际上会调用 (::b).invoke(1)
所以你可以对一个函数类型的对象调用 invoke(),但不能对一个函数这么做:
b.invoke(1) // 报错
为什么?因为只有函数类型的对象有这个自带的 invoke() 可以用,而函数,不是函数类型的对象。那它是什么类型的?它什么类型也不是。函数不是对象,它也没有类型,函数就是函数,它和对象是两个维度的东西。
包括双冒号加上函数名的这个写法,它是一个指向对象的引用,但并不是指向函数本身,而是指向一个我们在代码里看不见的对象。这个对象复制了原函数的功能,但它并不是原函数。
这个……是底层的逻辑,但我知道这个有什么用呢?
这个知识能帮你解开 Kotlin 的高阶函数以及接下来我马上要讲的匿名函数、Lambda 相关的大部分迷惑。 比如我在代码里有这么几行:
fun b(param: Int): String {
return param.toString()
}
val d = ::b
那我如果想把 d 赋值给一个新的变量 e:
val e = d
我等号右边的 d,应该加双冒号还是不加呢?
不用试,也不用搜,想一想:这是个赋值操作对吧?赋值操作的右边是个对象对吧?d 是对象吗?当然是了,b 不是对象是因为它来自函数名,但 d 已经是个对象了,所以直接写就行了。
Kotlin 里匿名函数和 Lambda 表达式的本质
我们先看匿名函数。它可以作为参数传递,也可以赋值给变量,对吧? 但是我们刚才也说过了函数是不能作为参数传递,也不能赋值给变量的,对吧? 那为什么匿名函数就这么特殊呢? 因为 Kotlin 的匿名函数不——是——函——数。它是个对象。匿名函数虽然名字里有「函数」两个字,包括英文的原名也是 Anonymous Function,但它其实不是函数,而是一个对象,一个函数类型的对象。它和双冒号加函数名是一类东西,和函数不是。 所以,你才可以直接把它当做函数的参数来传递以及赋值给变量:
a(fun (param: Int): String {
return param.toString()
});
val a = fun (param: Int): String {
return param.toString()
}
Kotlin
同理,Lambda 其实也是一个函数类型的对象而已。你能怎么使用双冒号加函数名,就能怎么使用匿名函数,以及怎么使用 Lambda 表达式。
这,就是 Kotlin 的匿名函数和 Lambda 表达式的本质,它们都是函数类型的对象。Kotlin 的 Lambda 跟 Java 8 的 Lambda 是不一样的,Java 8 的 Lambda 只是一种便捷写法,本质上并没有功能上的突破,而 Kotlin 的 Lambda 是实实在在的对象。
在你知道了在 Kotlin 里「函数并不能传递,传递的是对象」和「匿名函数和 Lambda 表达式其实都是对象」这些本质之后,你以后去写 Kotlin 的高阶函数会非常轻松非常舒畅。
Kotlin 官方文档里对于双冒号加函数名的写法叫 Function Reference 函数引用,故意引导大家认为这个引用是指向原函数的,这是为了简化事情的逻辑,让大家更好上手 Kotlin;但这种逻辑是有毒的,一旦你信了它,你对于匿名函数和 Lambda 就怎么也搞不清楚了。
总结
好,这就是 Kotlin 的高阶函数、匿名函数和 Lambda。简单总结一下:
- 在 Kotlin 里,有一类 Java 中不存在的类型,叫做「函数类型」,这一类类型的对象在可以当函数来用的同时,还 能作为函数的参数、函数的返回值以及赋值给变量;
- 创建一个函数类型的对象有三种方式:双冒号加函数名、匿名函数和 Lambda;
- 一定要记住:双冒号加函数名、匿名函数和 Lambda 本质上都是函数类型的对象。在 Kotlin 里,匿名函数不是函数, Lambda 也不是什么玄学的所谓「它只是个代码块,没法归类」,Kotlin 的 Lambda 可以归类,它属于函数类型的对象。