Kotlin第七讲---函数定义

内容简介

个人认为 Kotlin 函数的设计属于改动最多,也最为惊艳的。尤其是函数可以以参数形式传递,为后续带来了更多的发挥空间。以及巧妙的运用 Lambda (其实就是个对象)表达式,将 Java 中的很多不可能变成了可能。接下来我们来揭开函数的面纱。

函数定义

我们要如何定义函数呢?在 Kotlin 中函数定义,不再和 Java 一样。而是通过 fun 修饰符修饰。并且定义过程中有很多的新的功能。

简单的定义函数&调用


   
   
  1. /**

  2. * 定义函数

  3. */

  4. fun function(){

  5. println("调用")

  6. }

  7. fun main() {

  8. // 函数调用

  9. function()

  10. }

函数定义行参&返回值

定义一个函数,若需要行参使用 fun函数昵称(行参1:类型,行参2:类型) 声明即可。若需要返回值要在函数后方增加 :返回类型 即可。


   
   
  1. /**

  2. * 定义一个 call 方法,接收 value 行参. 返回 int 类型

  3. */

  4. fun call(value: String): Int {

  5. return value.toInt()

  6. }

函数默认参数

记得类的定义讲过的默认值功能吗?我们定义的主构造方法的时候,可以为成员变量添加默认值。 Kotlin 中也可以为函数的行参增加默认值。


   
   
  1. /**

  2. * 定义一个 call 方法 参数默认值为 100,若不传值默认值为 100

  3. */

  4. fun call(value: String = "100"): Int {

  5. return value.toInt()

  6. }

补充一点,函数默认值功能只能在 Kotlin 中使用,若 Java 调用 Kotlin是无效的(老老实实传参吧)。

可变参数

在 Java 中也具有可变参数,只需要在定义行参的时候,使用 类型...变量昵称 定义即可(本质是数组)。
在 Kotlin 也具有可变参数,只需要对行参通过 vararg 修饰即可。


   
   
  1. /**

  2. * 定义一个可变参数 args

  3. */

  4. fun call(name: String, vararg args: String) {

  5. }

接下来我们看看 Kotlin 中可变参数的本质是什么东东。我尝试传入数组能成功吗?经过本人尝试,我分别传了数组&集合都在编译器就直接报错了,奇怪难道 Kotlin 的可变参数本质难道不是数组?

我尝试用 Java 的代码调用 Kotlin 的可变参数方法,发现可以调用成功,并且查看了反编译后的代码,发现本质还是数组。


   
   
  1. public class Demo {

  2. public static void main(String[] args) {

  3. String[] arrays = {"1", "2", "3"};

  4. // 没有报错

  5. KotKt.call("阿文",arrays);

  6. }

  7. }

上面的问题一直困扰我很久,由于这个可变参数用处不多,我也就没有在意。直到有一天我用 Kotlin 编写动态代理的时候,遇到了这样的问题。

请看下方代码:


   
   
  1. /**

  2. * 定义接口

  3. */

  4. interface CallI {

  5. fun call(value: String): Int

  6. }

  7. fun main() {

  8. // 动态代理

  9. val call =

  10. Proxy.newProxyInstance(

  11. ClassLoader.getSystemClassLoader()

  12. , arrayOf(CallI::class.java)

  13. , object : InvocationHandler {

  14. override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {

  15. // 拦截 call 方法,定义代码.其它方法都调用 InvocationHandler 的方法

  16. return if (method?.name == "call") {

  17. (args!![0] as String).toInt()

  18. } else method?.invoke(this, args)

  19. }

  20. }) as CallI

  21. // 我调用 toString 方法

  22. call.toString()

  23. }

报错内容:


   
   
  1. Exception in thread "main" java.lang.IllegalArgumentException: wrong number of arguments

我尝试看了下 method?.invoke(this,args) 的 invoke 方法第二个参数的确是可变参数,我传入的是 Kotlin 的数组类型,最终报错了。
最终找到了问题 Kotlin 中对可变数组的类型限制还是挺严格的,虽然本质上它是数组,但是 Kotlin 就是不让你直接传递数组(应该是为了代码上的歧义吧)。我们只需要在数组的前面加入一个 * 即可,将数组变成可变数组。


   
   
  1. /**

  2. * 定义一个可变参数 args

  3. */

  4. fun call(name: String, vararg args: String) {

  5. }

  6. fun main() {

  7. val array = arrayOf("1", "2")

  8. // 注意我前面加了一个 * 哦,理解为:将数组变成一个可变数组吧

  9. call("阿文", *array)

  10. }

具名参数

前面讲了 可变参数 与 函数默认参数 ,其实也是为具名参数做了铺垫。
回想下在 Java 中的可变参数,有一个硬性规定可变参数只能定义在最后一个(想想也是,不然怎么区分哪些参数属于可变的)。在 Kotlin 中也有这种问题,并且默认函数默认值也会出现这种问题,例如我将函数的中间的一个参数定义成有默认值的,当使用中间函数的默认值功能,就不知道该如何传值了。
为了解决这个问题 Kotlin 提供了一种具名参数的功能,我们只需要在传参的时候,使用 参数昵称=参数值 即可。


   
   
  1. /**

  2. * 可以看到我第一个参数具有默认值,我们如何调用呢?

  3. */

  4. fun call(str: String = "", msg: String): Int {

  5. return str.toInt()

  6. }

  7. /**

  8. * 可变参数和java的概念一致,本质也是数组,但是java中可变参数只能放在形参末尾

  9. * 由于kotlin有具名参数的概念,kotlin的可变参数就没必要放在末尾了

  10. */

  11. fun call2(vararg values: Int, msg: String) {

  12. println("values:${values.size} msg")

  13. }

  14. fun main() {

  15. // 通过指定名字进行赋值

  16. call(msg = "啊哈哈")

  17. call2(1, 2, 3, msg = "msg消息")

  18. }

函数嵌套

看完上面的内容,我想大家应该可以定义一些 Kotlin 的函数了吧。接下来我们来看看 Kotlin 比较重要的特性以及原理。在 Kotlin 中可以函数中嵌套定义函数,在 Java 中这是不可能实现的。


   
   
  1. /**

  2. * 定义函数嵌套

  3. */

  4. fun test(n: Int) {

  5. fun print(value: String) {

  6. println(value)

  7. }

  8. // 调用嵌套函数

  9. print(n.toString())

  10. }

  11. fun main() {

  12. test(18)

  13. }

啥原理呢?其实在编译器编译的时候,会将函数的内部函数。编译生成一个实现 FunctionN 类,并且有一个重写了 invoke 方法,创建了一个静态对象,然后 invoke 方法就是嵌套函数的实现。
别小看这一改动,这一改动是 Kotlin 中函数的精髓, Kotlin 的 Lambda 表达式丶函数式编程丶函数以参数传递,以及后续的 DSL 都和这个有一定关系。

反编译源码:


   
   
  1. public static final void test(int n) {

  2. // 看到没 内部的嵌套的函数编译成了一个 DemoKt$test$1 类

  3. DemoKt$test$1.INSTANCE.invoke(String.valueOf(n));

  4. }

  5. public static final void main() {

  6. test(18);

  7. }

DemoKt$test$1源码:


   
   
  1. // 只有这里为啥是 Function1 这个 1 又是什么含义。在 Lambda 篇章中会详细讲解

  2. final class DemoKt$test$1 extends Lambda implements Function1<String, Unit> {

  3. 。。。。不重要的删除掉了

  4. // 创建了一个静态对象

  5. public static final DemoKt$test$1 INSTANCE = new DemoKt$test$1();

  6. // 嵌套方法的函数体

  7. public final void invoke(@NotNull String value) {

  8. Intrinsics.checkParameterIsNotNull(value, "value");

  9. System.out.println(value);

  10. }

  11. }

这里讲到一个 FunctionN,当且大家就需要知道是一个接口,接口中定义了一个方法 invoke。后续的 Lambda 会详细讲解

函数当参数传递

在 C & C++ 中有函数指针的概念,由于 Java 中去除了复杂的指针的概念,因此 Java 中我们的定义的函数不可以以参数传递的,只能出传递对象(所有的 CallBack 回调都是这样的形式)。

Kotlin 中没有这种约束,如果要传递一个定义好的 fun 函数,可以通过 :: 关键词,就能取 fun 函数的函数指针(函数指针,我是这么叫的啦,嘿嘿!),这个其实在高阶函数中经常使用。


   
   
  1. /**

  2. * 定义一个函数

  3. * 这里用类一个缩写,只要函数只有执行一句代码,就可以这样写

  4. */

  5. fun funPrint(msg: String) = println(msg)

  6. /**

  7. * 高潮部分,函数传递

  8. * (这里说了提前预备了下Lambda的知识,先不要考虑Function1是个什么东东,只管是个函数类型的对象吧)

  9. *

  10. */

  11. fun funCall(tag: String, funParameter: Function1<String, Unit>) {

  12. // 调用传入的函数

  13. funParameter("tag:$tag")

  14. }

  15. fun main() {

  16. /**

  17. * 通过::获取函数的指针

  18. */

  19. val f = ::funPrint

  20. /**

  21. * 传递函数

  22. */

  23. funCall("阿文", f)

  24. }

接下来我们用 Java 的角度分析下,是如何实现的呢?
我们定义 funCall 方法的第二个参数类型是 Function1 它本质是一个接口,在后续的 Lambda 中会详细讲解这里接口。我们先来看看 main 方法。


   
   
  1. public static final void main() {

  2. // 第二个参数 DemoKt$main$f$1.INSTANCE 他是一个什么呢?

  3. funCall("阿文", DemoKt$main$f$1.INSTANCE);

  4. }

  5. public static final void funCall(@NotNull String tag, @NotNull Function1<? super String, Unit> funParameter) {

  6. sb.append("tag:");

  7. sb.append(tag);

  8. // 调用了 funCall invoke 方法

  9. funParameter.invoke(sb.toString());

  10. }


   
   
  1. final /* synthetic */ class DemoKt$main$f$1 extends FunctionReference implements Function1<String, Unit> {

  2. ...我删除了部分源码

  3. // 是个静态单例

  4. public static final DemoKt$main$f$1 INSTANCE = new DemoKt$main$f$1();

  5. // 对应的执行方法,

  6. public final void invoke(@NotNull String p1) {

  7. Intrinsics.checkParameterIsNotNull(p1, "p1");

  8. // 注意这里就是传入的需要调用的函数

  9. DemoKt.funPrint(p1);

  10. }

  11. }

通过上面分析可知,其实将函数变为参数传递,本质是新生成了一个实现 FunctionN 的类,并且重写了 invoke 方法。而 invoke 的函数体做的就是传入函数的函数调用。(是不是和函数嵌套有一些类似啊?只是函数体不同) 所以本质上函数嵌套与函数参数传递,都是传递了一个实现 FunctionN 对象(后续的 Lambda)也是一样的哦。

推荐阅读

--END--

识别二维码,关注我们

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值