跳至主要內容

函数式编程

LincZero大约 20 分钟

函数式编程

函数的声明、定义、使用

Java & C++

在类外定义方法

  • Java / C#
    • Java函数都在类中
    • 这是纯面向对象语言的通用标准,C#也是如此: 不允许在类外定义变量、方法、事件等,强调一切皆是对象的思想。 即便是主函数的main,也必须定义在某个类里面。
  • C++
    • 允许在类外定义变量、方法、事件等

Python & C++

自定义函数写法

  • C语言(类型)函数名() {},有主函数
  • Pythondef 函数名():,无需主函数/用init
  • Js:无需主函数/用init
    • 普通形式:function 函数名() {}
    • 匿名函数:function () {},立即执行:;(function () {})()
    • 箭头函数:()=>{},立即执行:;(()=>{})()

是否声明形参类型

  • C、Java、Ts:声明形参类型
  • Python、Js:不声明形参类型

是否声明返回值

  • C、Java、Ts:需定义函数时声明:是否有返回值、和返回值类型
  • Python、Js:无需定义函数时声明:是否有返回值、和返回值类型

预声明函数原型

  • C、C++:需要先声明函数原型(参数类型+返回值类型)
    • 函数原型声明(Function Prototype)原理
      • 让编译器知道函数的存在,以及存在的形式,即使函数暂时没有定义,编译器也知道如何使用它
      • 有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。
  • Python、Js:不需要先声明函数原型

函数定义位置

  • C、C++:函数定义在前还是在后都无所谓,但函数原型声明需要放在使用前

  • Python:解释型语言函数定义必须放在前面,解释型语言不需要声明函数原型

  • Js:解释型语言函数定义必须放在前面,,解释型语言不需要声明函数原型,变量提升例外

    • 变量提升

      • 函数字面量声明式function f(){}的可以在函数定义前调用
      • 定义变量式的var f = function (){}var f = new Function()则不能
      • var定义的方式也是变量提升,但提升的是var而不是function。提前使用则是声明但未赋值的状态
    • 原理

      • JavaScript 会提升变量声明(Hoisting),即变量提升
      • 意味着 var 表达式和 function 声明都将会被提升到当前作用域的顶部(var只声明不定义,提前使用时是undefined
    • 变量提升 / 函数提升原理

      • js是从上往下的顺序执行代码的。在一个作用域中,js会先进入“进入执行上下文阶段”。这个阶段分两步
      • 第一步是创建用var关键字创建的变量(只是创建,不做其它操作)
      • 第二步创建函数声明,也就是function f() {}
      • 之后js会进入“执行代码阶段”,也就是执行函数的计算或赋值等操作。所以function f() {}会在执行前就定义好了。

定义顺序、与向前声明问题

  • 函数/类 是否区分声明与定义、是否考虑顺序:

    • C/C++
      • 可分离
      • 考虑顺序,可以向前声明
    • Rust
      • 不可分离
      • 考虑顺序,可以向前声明
    • Swift
      • 不可分离
      • 考虑顺序,可以向前声明。但在同一个作用域内 (同一个类或结构体中),则无需考虑顺序
    • Java
      • 仅在接口 (interface) 中可仅声明,类中不可分离
      • 不考虑顺序,原理类似于全部默认向前声明
    • JavaScript/TypeScript
      • 不可分离
      • 不考虑顺序,有 函数提升(hoisting)
    • Go
      • 不可分离
      • 不考虑顺序,编译时会先解析所有包和函数,原理类似于全部默认向前声明
    • Python
      • 不可分离
      • 不考虑顺序,执行前会编译并解析一遍

    GPT4:

    不可单独声明的语言,是否意味着不能发布真正闭源的链接库?


    一个语言是否允许单独声明而不定义函数或类,并不直接决定该语言能否发布闭源的链接库。主要依赖于编译和链接过程,以及你是否使用了混淆或其他保护源代码的工具。

    有的语言需要通过一些方法实现一定程度的闭源:

    • C/C++可以发布闭源库
    • Python通常分发源代码而难以闭源。但可以将你的Python代码编译为C扩展,或者使用工具将你的代码编译为二进制格式
    • Java编译成字节码后,可以对这些字节码进行混淆,一定程度闭源。

    1. 但你说的那些只是一定程度的闭源,如果我需要非常高安全性的完全闭源呢?
    2. Java可以分离接口,不能完全闭源吗?

    1. 对于需要非常高安全性的完全闭源,通常的做法是使用编译到机器码的语言(如C、C++、Go、Rust等),并且发布的是编译后的二进制文件。这样,虽然理论上仍然可以通过反汇编和逆向工程来查看代码,但这需要相当高的技术水平,并且得到的代码通常很难理解。然而,需要注意的是,没有任何方法可以100%防止代码被反编译或逆向工程,因此,除了尽可能地保护你的代码外,还需要使用其他的安全措施,如加密、混淆、代码签名等。
    2. 对于Java,虽然可以将接口和实现分离,但是Java的字节码可以被反编译。这意味着,即使你只发布包含接口的.class文件或.jar文件,别人仍然可以通过反编译得到你的代码。然而,你可以使用混淆工具(如ProGuard)来混淆你的字节码,使得反编译的结果难以理解。这在一定程度上可以实现闭源。然而,和上面一样,没有任何方法可以100%防止代码被反编译或逆向工程。

参数与返回值

可变参数

  • Java

    • 可变参数类型,有时称为“变参”方法

      public static void main(String... args) {}							// 本质是数组
      public PrintStream printf(String fmt, Object... arg) {}
      
  • TypeScript

    • 叫剩余参数,参考 https://www.tslang.cn/docs/handbook/functions.html

      function buildName(firstName: string, ...restOfName: string[]) {	// 本质是数组
        return firstName + " " + restOfName.join(" ");
      }
      
      let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
      
  • Python

    • Python的可变参数有两种,一种是列表类型,一种是字典类型

      # a接受一个常规参数,*args 接收的是一个tuple,**kwargs 接收一个dict。本质分别是元组(只读)和字典
      def ff(a, *b, **c):
          print(a)  # 1232
          print(b)  # (4, 5, 6, 7, 8)
          print(c)  # {'ss': 'sadf', 'xx': 'fff', 'ww': 'asdf'}
      
      if __name__ == '__main__':
          ff(1232, 4, 5, 6, 7, 8, ss="sadf", xx="fff", ww="asdf")
      
  • C++

    • 参考

      • https://blog.csdn.net/qychjj/article/details/98532841(含原理)
      • https://www.bilibili.com/read/cv13120050
    • 可变参数函数 - ...,也叫 VA函数(variable argument function)

      #include <stdio.h>
      
      // 定义一个接受可变参数的函数
      void printNumbers(int count, ...) {		// 预处理器,机制可能稍复杂些。在运行时动态处理参数的机制,它依赖于栈帧、va_list和相关宏
          va_list args; // 定义一个va_list类型的变量,用于遍历可变参数列表
          va_start(args, count); // 初始化va_list变量,count是固定参数的个数
      
          int i;
          for (i = 0; i < count; i++) {
              int number = va_arg(args, int); // 从可变参数列表中获取参数
              printf("%d\n", number);
          }
      
          va_end(args); // 清理va_list变量
      }
      
      int main() {
          // 调用函数,传入不同数量的参数
          printNumbers(3, 1, 2, 3);
          printNumbers(4, 1, 2, 3, 4);
          return 0;
      }
      
    • 可变参数宏 - __VA_ARGS__

      // 略
      // __VA_ARGS__ 不是C语言标准的一部分。在C语言中,预处理器宏通常用于在编译时执行文本替换和条件编译等操作
      
    • 可变参数模板 - initializer_list

      #include <iostream>
      #include <initializer_list>
      
      // 定义一个接受可变参数的函数
      void printNumbers(std::initializer_list<int> numbers) {	// 本质是一种特殊的容器类型
          for (int number : numbers) {
              std::cout << number << std::endl;
          }
      }
      
      int main() {
          // 调用函数,传入不同数量的参数
          printNumbers({1, 2, 3, 4, 5});
      }
      
  • Go

    • 数量可变

      func test (args ...int) {			// 这里的args本质是切片(slice)
          for i:=0; i<len(args); i++ {
              fmt.Println(args[i])
          }
      }
      
      func main() {
      	test(1, 3, 5, 7, 9)
      }
      
    • 类型可变

      func myFunction(args ...interface{}) {
          for _, arg := range args {
              fmt.Println(arg)
          }
      }
      
      func main() {
          // 调用函数,传入不同数量和类型的参数
          myFunction(1, 2, 3, "a", "b", "c")
      }
      

多返回值

  • Go可以多返回值,例如:

    // 原型,双返回值
    func ParseInt(s string, base int, bitSize int) (i int64, err error)
    // 使用,可以忽略返回值
    n1, _ = strconv.ParseInt(s1, 10, 64)
    
  • 有点类似于Python的返回元组,特别是Python也有元组的解引用写法

  • 而C/C++就做不到这点。传统的C/C++在这方面,错误处理都比较烦

    • 要么通过抛出异常。但这需要每次调个函数都要try-catch
    • 要么返回类型为int的函数直接返回错误码
    • 而返回其他类型的函数还可以通过全局的errno。但这需要为函数设计一个线程安全的errno

Lambda、闭包

Lambda表达式【区别还挺大的】

普通函数、匿名函数、闭包、Lambda 关系

各语言的闭包概念和作用基本是一样的。普通函数、匿名函数、闭包、Lambda 关系:

  • 普通函数/具名函数 & 匿名函数

    • 使用区别/表层区别:有无名字,但匿名函数用函数指针保存的话还是和具名函数有区别的

    • 底层区别:

      • 具名函数:具名函数的地址是固定的
      • 匿名函数:每次使用时都需要重新创建,在捕获外部变量的情况下内存管理复杂,可能会影响性能 (现代编译器和解释器也会针对此进行很多优化)
    • 本质区别:

      • 具名函数:作用域明确。可以作为闭包使用,但是它们的封装性和封装数据的能力不如匿名函数
      • 匿名函数:作用域受限于上下文。经常用于创建闭包
  • Lambda表达式 & 匿名函数:前者是后者的一种简洁表示方法 (有的函数有匿名函数,但不是以Lambda形式表示的)

  • 匿名函数/Lambda表达式 & 闭包

    • 前者若捕获外部作用域中的变量时,构成闭包
    • 前者若不捕获外部作用域中的变量时,则不构成闭包,就只是普通的匿名函数
  • “捕获” 的具体要求:按值而非按引用捕获也算闭包,捕获的是全局变量而非外部的局部变量也算闭包

是否延长捕获值的生命周期

  • 用途上
    • 捕获外部作用域变量
    • 如果是引用捕获,则一些语言附带 “延长其生命周期” 的效果,否则 “不延长其生命周期”
    • 如果是值捕获,则捕获值可以 “看作是一个在内存持久保存” 的一个值,通常 “延长其生命周期”
  • 引用捕获是否延长生命周期
    • 对于各种语言的闭包函数,捕获外部作用域变量后,并不意味着一定延长其生命周期。对于有GC的语言及对应变量类型,通常延长,反之。
    • 有GC语言:如 JavaScript5、Java、C#等。只要闭包本身还被引用,变量就不会被GC
    • 无GC语言:如 C、C++等。如果捕获的变量生命周期结束且并释放,可能内存泄露
    • 特殊语言:如 Rust。有GC机制,有所有权和借用的概念来帮助管理内存,闭包捕获变量时需要明确指定生命周期。 如果闭包尝试捕获一个作用域内的临时变量,即有内存泄露/悬挂指针等风险,编译器会报错。不让你那样写。

角度:函数对象化

闭包这个东西可以从很多角度去分析、解构,这里以一种实现方式为例。

一些闭包的实现说白了就是 “函数对象化”,伪代码:

class 闭包类 {
  m_闭包环境变量
  init(闭包环境变量):m_闭包环境变量(闭包环境变量) {}
  闭包函数() {这里可以使用m_闭包环境变量}
}
闭包对象 = new 闭包类(闭包环境变量)
// 语法糖:做点操作,使调用闭包对象时,自动调用该对象的闭包函数

传递代码段的方式

  • Java
    • Java很难传递代码段
    • 在Java中传递一个代码段并不容易,不能直接传递代码段。 Java是一种纯面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
    • 问题已经不是是否增强Java来支持函数式编程,而是要如何做到这一点。 设计者们做了多年的尝试,终于找到一种适合Java的设计 —— Lambda表达式(Java SE 8引入)
  • 其他语言
    • 在其他语言中可以直接处理代码块。
    • Java设计者很长时间以来一直拒绝增加这个特性 (传递代码段)。Java非常重视其简单性和一致性。 他们认为如果只要一个特性能够让代码稍简洁一些,就把这个特性增加到语言中,这个语言很快就会变得一团糟,无法管理。

不同点:

  • Java
    • lambda表达式所能做的也只是能转换为函数式接口。函数式接口这个概念其他语言也似乎是没有的
  • 其他语言
    • (这里指其他支持函数字面量的程序设计语言)
    • 可以声明函数类型(如(String, String) -> int)、声明这些类型的变量,还可以使用变量保存函数表达式。 不过,Java设计者还是决定保持我们熟悉的接口概念,没有为Java语言增加函数类型

写法上

  • C

    特别地,像C语言那种在语言内定义Static变量的行为,是否属于 “闭包”?不属于

  • Java

    // 这里用了类型自动推导。另外,最重要的一点是:
    // 这里表示可以用来代替函数式接口来使用,而不是真的赋值、保存Lambda表达式
    Comparator<String> comp = (first, second) ->
    {
        first.length() - second.length();
    }
    
    // 用BiFunction倒是可以保存一个Lambda表达式到BiFunction变量中,但这没有什么用
    // 例如:没有哪个Arrays.sort方法想要接收一个BiFunction
    BiFunction<String, Sting, Integer> comp
        = (first, second) -> first.length() - second.length();
    
    
  • C++

    C++没有原生的闭包支持,但可以通过lambda表达式和对象来模拟闭包行为。

    /**
     * Lambda表达式语法
     *
     * @detail:
     * - 捕获列表:捕获外部变量,捕获的变量可以在函数体中使用,可以省略,即不捕获外部变量。
     * - 形参列表:和普通函数的形参列表一样。可省略,即无参数列表
     * - mutable:mutable 关键字,如果有,则表示在函数体中可以修改捕获变量,根据具体需求决定是否需要省略。
     * - 异常列表:noexcept / throw(...),和普通函数的异常列表一样,可省略,即代表可能抛出任何类型的异常。
     * - 返回类型:和函数的返回类型一样。可省略,如省略,编译器将自动推导返回类型。
     * - 函数体:代码实现。可省略,但是没意义。
     */
    [捕获列表] (形参列表) mutable 异常列表-> 返回类型
    {
        函数体
    }
    
    // 闭包函数
    auto l = [](int x) -> bool {
    	// ...  
    };
    auto lambda = [a, b](int x, int y)mutable throw() -> bool
    {
        return a + b > x + y;
    };
    
    // 用闭包函数 捕获一个超出作用域的变量
    #include <iostream>
    #include <functional>
    std::string createGreeting(const std::string& greeting) {
        return [greeting](const std::string& name) -> std::string {
            return greeting + ", " + name + "!";
        };
    }
    int main() {
        auto sayHello = createGreeting("Hello");
        std::cout << sayHello("Kimi") << std::endl; // 输出: Hello, Kimi!
        return 0;
    }
    
  • Python

    # 闭包函数
    self.aboutAct = QAction("关于", self, statusTip="关于界面"
                            , triggered=lambda: QMessageBox.about(self, "About MDI", "WWWWWWWWWWWWW")
                            )  # def about(self);
    
    # 用闭包函数 捕获一个超出作用域的变量
    def create_greeting(greeting):
        def greeting_function(name):
            return f"{greeting}, {name}!"
        return greeting_function
    say_hello = create_greeting("Hello")
    print(say_hello("Kimi"))  # 输出: Hello, Kimi!
    
  • JavaScript

    // 闭包函数
    let max = (a,b) => a>b?a:b;	// 也可用es6的箭头写
    let max = function (a,b){
    	return a>b?a:b;
    }
    
    // 用闭包函数 捕获一个超出作用域的变量
    function createGreeting(greeting) {
      return function(name) {
        return `${greeting}, ${name}!`;
      };
    }
    const sayHello = createGreeting("Hello");
    console.log(sayHello("Kimi")); // 输出: Hello, Kimi!
    
  • Go

    // 用闭包函数 捕获一个超出作用域的变量
    package main
    import "fmt"
    func createGreeting(greeting string) func(string) string {
        return func(name string) string {
            return greeting + ", " + name + "!"
        }
    }
    func main() {
        sayHello := createGreeting("Hello")
        fmt.Println(sayHello("Kimi")) // 输出: Hello, Kimi!
    }
    

概念2,闭包与变量捕获

首先,**闭包 (closure)**是什么?

参考:

百度百科解释:

**(功能上)**闭包就是能够读取其他函数内部变量的函数。

**(使用上)**例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。

**(本质上)**在本质上,闭包是将函数内部和函数外部连接起来的桥梁。


mozilla解释:

**(组成上)**闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。

**(功能上)**换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。

**(使用上)**在 JavaScript 中,闭包会随着函数的创建而被同时创建。


Wiki:

(概念上)在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)函数闭包(function closures)。 是在支持头等函数的编程语言中实现词法绑定的一种技术。

(组成/实现上)闭包是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。 环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。

**(语言上)**最早实现闭包的程序语言是Scheme。 之后,闭包被广泛使用于函数式编程语言 (如ML语言和LISP),很多命令式程序语言也开始支持闭包。

**(区分闭包和 函数 )**它们最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。 捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

**(区分闭包和匿名函数)**它们经常被用作同义词。但严格来说,匿名函数就是字面意义上没有被赋予名称的函数,而闭包则实际上是一个函数的实例,也就是说它是存在于内存里的某个结构体。

各种语言中(类似)闭包的结构

  • C语言

    • C语言的回调函数

      在C语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的void*指针用以保存用户数据。 这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包,但语法上有所不同。

    • gcc对C语言的扩展

      gcc编译器对C语言实现了一种闭包的程序特性。

    • C语言扩展:Blocks

  • C++函数对象

    • 早期标准

      早期标准允许通过重载operator()来定义函数对象。这种对象的行为在某种程度上与函数式编程语言中的函数类似。

      它们可以在运行时动态创建、保存状态,但是不能如闭包一般方便地隐式获取局部变量,并且有“专物专用”的繁琐问题——对于每一段闭包代码都要单独写一个函数对象类。

    • C++11

      // C++11标准已经支持了闭包,这是一种特殊的函数对象,由特殊的语言结构——lambda表达式自动构建
      void foo(string myname) {
      	typedef vector<string> names;
      	int y;
      	names n;
      	// ...
      	names::iterator i =
      	 find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;});	
      	// 'i' 现在是'n.end()'或指向'n'中第一个
      	// 不等于'myname'且长度大于'y'的字符串
      }
      
  • Java

    • Java SE 8 引入Lambda,可以用Lambda来实现闭包

    • 注意事项

      • ① 只能引用值不会改变的变量
      • ② 不能引用可能在外部被改变的变量
      • ③ 不能声明同名变量
      • ④ this的含义
    • 代码举例

      public static void repeatMessage(String text, int delay)
      {
          ActionListener listener = event ->
          {
              // 这里捕获了text变量。不怕函数结束时该变量被销毁
              System.out.println(text);
              Toolkit.getDefaultToolkit().beep();
          };
          new Timer(delay, listener).start();
      }
      
      // 调用
      repeatMessage("Hello", 1000); // 打印:Hello every 1,000 milliseconds
      
  • JavaScript

    • JavaScript 在闭包上要简单许多 Javascript 语言的特殊之处,就在于函数内部可以直接读取全局变量 JavaScript 变量可以是局部变量或全局变量。私有变量可以用到闭包。

      // 实例1,a 是一个 局部 变量
      function myFunction() {
          var a = 4;
          return a * a;
      }
      
      // 实例2,a 是一个 全局 变量
      var a = 4;
      function myFunction() {
          return a * a;
      }
      

资源释放

不同语言的类似处理:

  • goto方法

  • C语言

    • Setjmp/Longjmp

      可以用来实现异常处理和资源释放。setjmp用于捕获当前环境的上下文,而longjmp则用于从setjmp返回,并可以选择性地执行资源释放代码。

      这种方法可以在异常发生时跳转到函数的末尾执行清理代码,而不需要使用大量的if语句。

  • C++

    • RAII (Resource Acquisition Is Initialization)

      与智能指针(如std::unique_ptr或std::shared_ptr)一起使用。当对象被创建时,它会获取资源;当对象被销毁时,它会自动释放资源。

  • Java的 try-with-resources/try-catch-finally

    FileWriter writer = null;
    try {
        writer = new FileWriter("example.txt");
        // 使用writer对象
        // ...
    } catch (IOException e) {
        // 处理异常
        // ...
    } finally {
        if (writer != null) {
            try {
                writer.close();
            } catch (IOException e) {
                // 处理关闭资源时的异常
                // ...
            }
        }
    }
    
  • Python

    上下文管理器 __exit____enter__with

    __exit__方法定义了退出with语句块时的行为,通常用于释放资源

    class MyResource:
        def __enter__(self):
            # 获取资源
            return self
    
        def __exit__(self, exc_type, exc_value, traceback):
            # 释放资源
            if exc_type:
                # 如果有异常发生,可以选择处理或忽略
                print(f"An exception occurred: {exc_type}, {exc_value}")
            # 总是执行资源释放
            # ...
    
    with MyResource() as resource:
        # 使用资源
        # ...
        
    # 上面是自己定义的情况。一般用得比较多的是打开文件的 open with,无需显式调用 close() 方法
    
  • C#

    • using语句,类似于Java的try-with-resources
  • JavaScript

    • Promise和async/await
  • Go语言

    • defer关键字:可以在函数返回前执行一段代码,通常用于资源释放。

      结合try-defer模式,可以在发生异常时自动释放资源,而无需在每个判断点重复编写释放代码