专栏首页sickworm5. 函数声明与闭包(Closure)

5. 函数声明与闭包(Closure)

今天介绍闭包。闭包也不是新东西了。其实 Kotlin 就基本没有新东西,不,是新语言都基本没有新东西。新语言都是把近些年好用的特性组装起来,再加点自己的见解,因地制宜 一下。

0. 闭包概念介绍

闭包我第一次接触是在 JavaScript 上,函数当作“一等公民”的编程语言都有这个概念。函数是“一等公民”的意思是,函数和变量一样,它是某种类型的实例,可以被赋值,可以被引用。当然函数还可以被调用。变量类型是某个声明的类,函数类型就是规定了入参个数,类型和返回值类型(不规定名字。函数名就和变量名一样,随便起)。如我要声明 Kotlin 一个函数类型,它的入参是两个整数,出参是一个整数,那应该这样写:val add: (Int, Int) -> Int。箭头左边括号内表示入参,括号不可省略。箭头右边表示返回值。

wiki 上闭包的定义是:引用了自由变量的函数,这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。从定义来说,对闭包的理解,是基于普通函数之上的。一般的函数,能处理的只有入参和全局变量,然后返回一个结果。闭包比普通函数更多一点能力,它还捕获了当前环境的局部变量。当然了,捕获局部变量的前提是可以在局部环境里声明一个函数,这只有把函数当作“一等公民”才可以做到。

1. 闭包与匿名类比较

在函数不是“一等公民”的 Java 老大哥这里,匿名类其实就是代替闭包而存在的。只不过 Java 严格要求所有函数都需要在类里面,所以巧妙的把“声明一个函数”这样的行为变成了“声明一个接口”或“重写一个方法”。匿名类也可以捕获当前环境的 final 局部变量。但和闭包不一样的是,匿名类无法修改捕获的局部变量(final 不可修改)。

而匿名类能引用 final 的局部变量,是因为在编译阶段,会把该局部变量作为匿名类的构造参数传入。因为匿名类修改的变量不是真正的局部变量,而是自己的构造参数,外部局部变量并没有被修改。所以 Java 编译器不允许匿名类引用非 final 变量。

Java8 lambda 是进一步接近闭包的特性,lambda 的 JVM 实现是类似函数指针的东西。但注意: Java7 的 lambda 语法糖兼容不是真正的 lambda,它只是简化了匿名类的书写。同样的 lambda 也只能引用 final 变量。

2. 闭包使用

我们来看一个闭包的例子:

fun returnFun(): () -> Int {
    var count = 0
    return { count++ }
}

fun main() {
    val function = returnFun()
    val function2 = returnFun()
    println(function()) // 0
    println(function()) // 1
    println(function()) // 2

    println(function2()) // 0
    println(function2()) // 1
    println(function2()) // 2
}

分析上面的代码,returnFun返回了一个函数,这个函数没有入参,返回值是Int。我们可以用变量接收它,还可以调用它。functionfunction2分别是我创建的两个函数实例。

可以看到,我每调用一次function()count都会加一,说明countfunction持有了而且可以被修改。而function2functioncount是独立的,不是共享的。

而我们通过 jadx 反编译可以看到:

public final class ClosureKt {
    @NotNull
    public static final Function0<Integer> returnFun() {
        IntRef intRef = new IntRef();
        intRef.element = 0;
        return (Function0) new 1<>(intRef);
    }

    public static final void main() {
        Function0 function = returnFun();
        Function0 function2 = returnFun();
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
    }
}

被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。这个 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。Ref 家族源码:https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/Ref.java

在 Java 中,我们如果想要匿名类也可以操作外部变量,一般做法是把这个变量放入一个 final 数组中。这和 Kotlin 的做法本质上是一样的,即通过持有该变量的引用来使得两个类可以修改同一个变量。

3. 总结

根据上面分析,我们可以了解到:

  • 闭包不是新东西,是把函数作为“一等公民”的编程语言的特性;
  • 匿名类是 Java 世界里的闭包,但有局限性,即只能读 final 变量,不能写任何变量;
  • Kotlin 的闭包可以捕获上下文的局部变量,并修改它。实现办法是 Kotlin 编译器给引用的局部变量封装了一层引用。

版权所有,转载请注明出处: https://sickworm.com/?p=1786

共享此文章:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 12. Kotlin 作用域函数(scope function)

    学习 Kotlin 一定绕不开 run/let/apply/also 这四兄弟,它们是 Kotlin 使用频率最高的扩展方法(扩展方法在之前文章有介绍),它们也...

    sickworm
  • 实现比特币BTC交易重发(Opt-In Replace-by-Fee,Opt-In RBF)

    当你的交易因为交易费用过低而迟迟不能被节点确认,而又没有被节点抛弃的时候,你可能需要交易重发这个功能。而交易重发实际上就是,将保存在节点交易内存池中的你的交易(...

    sickworm
  • 1. hello world!与函数声明

    非常熟悉。可在我漫长的编程生涯中,我大概是第 5 次在 Google 搜索了“Java Hello world”之后,才能独自完整的默写出来。。因为他有些“不太...

    sickworm
  • JavaScript 的闭包用于什么场景

    本文翻译自 MDN ( Mozilla Developer Network ): 原文地址:MDN 译文地址:shixinzhang 的博客 词法作用域 考虑如...

    张拭心 shixinzhang
  • 从 redux 的纯函数到函数式编程

    在做业务时我们用 react + redux 框架,其中 redux 的 reducers 是用的纯函数。这里什么是纯函数?为什么要用纯函数?纯函数的好处是什么...

    腾讯IVWEB团队
  • 2015.12.17 HTML5真题练习

    HTML5学堂:每天一道题,强壮程序员!今日主要涉及12.16日关于函数返回值的题目解答,以及一道涉及闭包的题目。 HTML5真题【2015.12.16】答案解...

    HTML5学堂
  • 火星探测器安装AI软件,AEGIS 实现好奇号自主探索

    【新智元导读】去年 5 月,NASA 的工程师为火星探测器好奇号安装了一套 AI 软件 AEGIS,使得好奇号实现了从自动向自主的飞跃。上周,开发者在 Scie...

    新智元
  • 代价函数

    代价函数,度量【假设集】的准确性。 机器学习中常用的代价函数,总结如下: 1 误差平方和函数 ? 说明:yi 是模型预测值,oi是样本实际值 2 交叉熵函数...

    陆勤_数据人网
  • 如何编写高质量的 JS 函数(3) --函数式编程[理论篇]

    《如何编写高质量的 JS 函数(1) -- 敲山震虎篇》介绍了函数的执行机制,此篇将会从函数的命名、注释和鲁棒性方面,阐述如何通过 JavaScript 编写高...

    2020labs小助手
  • 第5章 函数与函数式编程第5章 函数与函数式编程

    函数式编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以传入函数参数,也可以返回一个函数。函数式编程 (简称FP) 是一种编程...

    一个会写诗的程序员

扫码关注云+社区

领取腾讯云代金券