深入了解Rust中函数与闭包的使用

目录
  • 闭包
  • 高阶函数
  • 发散函数

闭包

Rust 的闭包由一个匿名函数加上外层的作用域组成,举个例子:

fn main() {
    let closure = |n: u32| -> u32 {
        n * 2
    };
    println!("n * 2 = {}", closure(12));
    // n * 2 = 24
}

闭包可以被保存在一个变量中,然后我们注意一下它的语法,参数定义、返回值定义都和普通函数一样,但闭包使用的是两个竖线。我们对比一下两者的区别:

// 普通函数定义
fn func1(a: u32, b: u32) -> String {
    // 函数体
}
/* 如果换成闭包的话,那么等价于
let func1 = |a: u32, b: u32| -> String {
    // 函数体
}
*/

所以两者在语法上没有什么本质的区别,但这个时候可能有人好奇了,我们能不能把闭包中的匿名函数换成普通函数呢?来试一下。

fn main() {
    let closure1 = |n: u32| -> u32 {
        n * 2
    };

    fn closure2(n: u32) -> u32 {
        n * 2
    }

    println!("n * 2 = {}", closure1(12));
    println!("n * 2 = {}", closure2(12));
    /*
    n * 2 = 24
    n * 2 = 24
    */
}

从表面上来看是可以的,但其实还存在问题,因为 closure2 只是一个在函数里定义的函数而已。而闭包除了要包含函数之外,还要包含函数所在的外层作用域,什么意思呢?我们举例说明:

你看到了什么?没错,在函数 closure2 内部无法使用外层作用域中的变量 a,因此它只是定义在 main 函数里的函数而已,而不是闭包,因为它不包含外层函数(main)的作用域。

而 Rust 提示我们使用 || { ... },那么 closure1 显然是闭包,因为它除了包含一个函数(匿名),还包含了外层作用域,我们将这个闭包赋值给了 closure1。

此外闭包还有一个重要的用途,就是在多线程编程时,可以将主线程的变量移动到子线程内部。

关于多线程后续会详细说,这里只是举个例子。

// 导入线程模块
use std::thread;

fn main() {
    let s = String::from("hello world");
    // 必须在 || 的前面加上 move
    // 它的含义就是将值从主线程移动到子线程
    let closure1 = move || {
        println!("{}", s);
    };
    // 开启一个子线程
    thread::spawn(closure1).join();
    /*
    hello world
    */
}

打印是发生在主线程当中的,而不是子线程,以上就是闭包相关的内容。

高阶函数

了解完闭包之后,再来看看高阶函数,在数学和计算机中,高阶函数是至少满足下列一个条件的函数:

  • 接收一个或多个函数作为输入;
  • 输出一个函数;

在数学中它们也叫算子或者泛函,高阶函数是函数式编程中非常重要的一个概念。

先来看看如何定义一个接收函数作为参数的函数:

// calc 接收三个参数,返回一个 i32
// 参数一:接收两个 i32 返回一个 i32 的函数
// 参数二 和 参数三均是一个 i32
fn calc(method: fn(i32, i32) -> i32,
        a: i32, b: i32) -> i32 {
    method(a, b)
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}
fn main() {
    println!("a + b = {}", calc(add, 12, 33));
    /*
    a + b = 45
    */

    // 也可以传递一个匿名函数,但它不能引用外层作用域的变量
    // 因为 calc 第一个参数接收的是函数,不是闭包
    let sub = |a: i32, b: i32| -> i32 {
        a - b
    };

    println!("a - b = {}", calc(sub, 12, 33));
    /*
    a - b = -21
    */
}

以函数作为参数,在类型声明中我们不需要写函数名以及参数名,只需要指明参数类型、数量和返回值类型即可。

然后再观察一下函数 calc 的定义,由于第一个参数 method 接收一个函数,所以它的定义特别的长,我们能不能简化一下呢?

// 相当于给类型起了一个别名
type Method = fn(i32, i32) -> i32;

fn calc(method: Method,
        a: i32, b: i32) -> i32 {
    method(a, b)
}

这种做法也是可以的。

看完了接收函数作为参数,再来看看如何将函数作为返回值。

type Method = fn(i32, i32) -> i32;

// 想要接收字符串的话
// 应该使用引用 &String 或切片 &str
// 当然我们前面说过,更推荐切片
fn calc(op: &str) -> Method {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    let sub = |a: i32, b: i32| -> i32 { a - b };

    // 使用 if else 也是可以的
    match op {
        "add" => add,
        "sub" => sub,
        // 内置的宏,会抛出一个错误,表示方法没有实现
        _ => unimplemented!(),
    }  // 注意:此处不可以加分号,因为要作为表达式返回
}

fn main() {
    let (a, b) = (11, 33);
    println!("a + b = {}", calc("add")(a, b));
    println!("a - b = {}", calc("sub")(a, b));
    /*
    a + b = 44
    a - b = -22
    */
}

以上就是高阶函数,还是很好理解的,和 Python 比较类似。你可以基于这个特性,实现一个装饰器,只是 Rust 里面没有 @ 这个语法糖罢了。这里我们简单地实现一下吧,加深一遍印象。

enum Result {
    Text(String),
    Func(fn() -> String),
}

fn index() -> String {
    String::from("欢迎来到古明地觉的编程教室")
}

fn login_required(username: &str, password: &str) -> Result {
    if !(username == "satori" && password == "123") {
        return Result::Text(String::from("请先登录"));
    } else {
        return Result::Func(index);
    }
}

fn main() {
    let res1 = login_required("xxx", "yyy");
    let res2 = login_required("satori", "123");
    // 如果后续还要使用 res1 和 res2,那么就使用引用
    // 也就是 [&res1, &res2]
    // 但这里我们不用了,所以是 [res1, res2],此时会转移所有权
    for item in [res1, res2] {
        match item {
            Result::Text(error) => println!("{}", error),
            Result::Func(index) => println!("{}", index()),
        }
    }
    /*
    请先登录
    欢迎来到古明地觉的编程教室
    */
}

是不是很有趣呢?这里再次看到了枚举类型的威力,我们有可能返回字符串,也有可能返回函数,那么应该怎么办呢?很简单,将它们放到枚举里面即可,这样它们都是枚举类型。至于到底是哪一个成员,再基于 match 分别处理即可。

还记得 match 吗?match 可以有任意多个分支,每一个分支都应该返回相同的类型,并且只有一个分支会执行成功,然后该分支的返回值会作为整个 match 表达式的返回值。

发散函数

最后再来看看发散函数,这个概念在其它语言里面应该很少听到。在 Rust 里面,发散函数永远不会返回,它的返回值被标记为 !,表示这是一个空类型。

// 发散函数的返回值类型是一个感叹号
// 它表示这个函数执行时会报错
fn foo() -> ! {
    panic!("这个函数执行时会报错")
}

fn main() {
    // 调用发散函数时,可以将其结果赋值给任意类型的变量
    let res1: u32 = foo();
    let res2: f64 = foo();
}

所以这个发散函数没啥卵用,你在实际开发中估计一辈子也用不上,因为它在执行的时候会 panic 掉。所以这段代码编译的时候是没有问题的,但执行时会触发 panic。既然执行时会报错,那么当然可以赋值给任意类型的变量。

因此当返回值类型为 ! 时,我们需要通过 panic 宏让函数在执行的过程中报错。但要注意的是,发散函数和不指定返回值的函数是不一样的,举个例子:

// 发散函数的返回值类型是一个感叹号
// 它表示这个函数执行时会报错
fn foo() -> ! {
    panic!("这个函数执行时会报错");
}

// 不指定返回值,默认返回 ()
// 所以以下等价于 fn bar() -> () {}
// 但很明显 bar 函数是有返回值的,会返回空元组
fn bar() {

}

总的来说发散函数没啥卵用,在工作中也不建议使用,只要知道有这么个东西就行。

到此这篇关于深入了解Rust中函数与闭包的使用的文章就介绍到这了,更多相关Rust函数 闭包内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入了解Rust 结构体的使用

    目录 楔子 定义并实例化结构体 简化版的实例化方式 基于已有结构体实例创建 元组结构体 没有字段的空结构体 结构体数据的所有权 使用结构体的示例程序 楔子 结构体是一种自定义的数据类型,它允许我们将多个不同的类型组合成一个整体.下面我们就来学习如何定义和使用结构体,并对比元组与结构体之间的异同.后续我们还会讨论如何定义方法和关联函数,它们可以指定那些与结构体数据相关的行为. 定义并实例化结构体 结构体与我们之前讨论过的元组有些相似,和元组一样,结构体中的数据可以拥有不同的类型.而和元组不一样的是

  • 深入了解Rust中泛型的使用

    目录 楔子 函数中的泛型 结构体中的泛型 枚举中的泛型 方法中的泛型 楔子 所有的编程语言都致力于将重复的任务简单化,并为此提供各种各样的工具.在 Rust 中,泛型(generics)就是这样一种工具,它是具体类型或其它属性的抽象替代.在编写代码时,我们可以直接描述泛型的行为,以及与其它泛型产生的联系,而无须知晓它在编译和运行代码时采用的具体类型. 总结一下泛型就是,提高代码的复用能力,处理重复代码.泛型是具体类型或者其它属性的抽象代替,编写的泛型代码不是最终的代码,而是一些模板,里面有一些占

  • 深入了解Rust的切片使用

    目录 为什么要有切片 字符串切片 其它类型的切片 为什么要有切片 除了引用,Rust 还有另外一种不持有所有权的数据类型:切片(slice),切片允许我们引用集合中某一段连续的元素序列,而不是整个集合. 考虑这样一个小问题:编写一个搜索函数,它接收字符串作为参数,并将字符串中的首个单词作为结果返回.如果字符串中不存在空格,那么就意味着整个字符串是一个单词,直接返回整个字符串作为结果即可. 让我们来看一下这个函数的签名应该如何设计: fn first_word(s: &String) -> ?

  • Rust指南之泛型与特性详解

    目录 前言 1.泛型 1.1.在函数中定义泛型 1.2.结构体中的泛型 1.3.枚举类中的泛型 1.4.方法中的泛型 2.特性 2.1.默认特性 2.2.特性做参数 2.3.特性做返回值 前言 在上篇Rust 文章中涉及到了泛型的知识,那么今天就来详细介绍一下Rust 中的泛型与特性.泛型是一个编程语言不可或缺的机制,例如在C++ 语言中用模板来实现泛型.泛型机制是编程语言用于表达类型抽象的机制,一般用于功能确定.数据类型待定的类,如链表.映射表等. 1.泛型 泛型是具体类型或其他属性的抽象代替

  • 深入了解Rust中引用与借用的用法

    目录 楔子 什么是引用 可变引用 悬空引用 小结 楔子 好久没更新 Rust 了,上一篇文章中我们介绍了 Rust 的所有权,并且最后定义了一个 get_length 函数,但调用时会导致 String 移动到函数体内部,而我们又希望在调用完毕后能继续使用该 String,所以不得不使用元组将 String 也作为元素一块返回. // 该函数计算一个字符串的长度 fn get_length(s: String) -> (String, usize) {     // 因为这里的 s 会获取变量的

  • 深入了解Rust中函数与闭包的使用

    目录 闭包 高阶函数 发散函数 闭包 Rust 的闭包由一个匿名函数加上外层的作用域组成,举个例子: fn main() {     let closure = |n: u32| -> u32 {         n * 2     };     println!("n * 2 = {}", closure(12));     // n * 2 = 24 } 闭包可以被保存在一个变量中,然后我们注意一下它的语法,参数定义.返回值定义都和普通函数一样,但闭包使用的是两个竖线.我们对

  • 举例讲解Go语言中函数的闭包使用

    和变量的声明不同,Go语言不能在函数里声明另外一个函数.所以在Go的源文件里,函数声明都是出现在最外层的. "声明"就是把一种类型的变量和一个名字联系起来. Go里有函数类型的变量,这样,虽然不能在一个函数里直接声明另一个函数,但是可以在一个函数中声明一个函数类型的变量,此时的函数称为闭包(closure). 例: 复制代码 代码如下: packagemain   import"fmt"   funcmain(){     add:=func(baseint)fun

  • 谈谈JavaScript中的函数与闭包

    闭包这东西,说难也难,说不难也不难,下面我就以自己的理解来说一下闭包 一.闭包的解释说明 对于函数式语言来说,函数可以保存内部的数据状态.对于像C#这种编译型命令式语言来说,由于代码总是在代码段中执行,而代码段是只读的,因此函数中的数据只能是静态数据.函数内部的局部变量存放在栈上,在函数执行结束以后,所占用的栈被释放,因此局部变量是不能保存的. Javascript采用词法作用域,函数的执行依赖于变量作用域,这个作用域是在定义函数时确定的.因此Javascript中函数对象不仅保存代码逻辑,还必

  • python中函数总结之装饰器闭包详解

    1.前言 函数也是一个对象,从而可以增加属性,使用句点来表示属性. 如果内部函数的定义包含了在外部函数中定义的对象的引用(外部对象可以是在外部函数之外),那么内部函数被称之为闭包. 2.装饰器 装饰器就是包装原来的函数,从而在不需要修改原来代码的基础之上,可以做更多的事情. 装饰器语法如下: @deco2 @deco1 def func(arg1,arg2...): pass 这个表示了有两个装饰器的函数,那么表示的含义为:func = deco2(deco1(func)) 无参装饰器语法如下:

  • 详谈JavaScript 匿名函数及闭包

    1.匿名函数 函数是JavaScript中最灵活的一种对象,这里只是讲解其匿名函数的用途.匿名函数:就是没有函数名的函数. 1.1 函数的定义,首先简单介绍一下函数的定义,大致可分为三种方式 第一种:这也是最常规的一种 复制代码 代码如下: function double(x){     return 2 * x;   } 第二种:这种方法使用了Function构造函数,把参数列表和函数体都作为字符串,很不方便,不建议使用. 复制代码 代码如下: var double = new Functio

  • JavaScript基础篇(6)之函数表达式闭包

    其实js支持函数闭包的主要原因是因为js需要函数能够保存数据.这里的保存数据是只函数在运行结束以后函数内变量的值也会进行保存.至于为什么js需要在函数内可以保存数据,那就是js是一种函数式语言.在函数内保存数据是函数式语言的一大特征. 回顾前面介绍过的三种定义函数方式 functiosu(numnumreturnunum//函数声明语法定义 vasufunction(numnum)returnunum}//函数表达式定义 vasuneFunction("num""num&qu

  • JavaScript 匿名函数和闭包介绍

    匿名函数:没有名字的函数; 闭包:可访问一个函数作用域里的变量的函数; 一 匿名函数 // 普通函数 function box(){ // 函数名是box; return 'Lee'; } box(); // =>Lee; 调用函数; // 匿名函数 function(){ // 匿名函数,会报错; return 'Lee'; } // 通过表达式自我执行 (function(name){ console.log(name); // =>Lee; })("Lee"); //

  • 浅谈JS封闭函数、闭包、内置对象

    一.变量作用域指的是变量的作用范围,javascript中的变量分为全局变量和局部变量 1.全局变量:在函数之外定义的变量,为整个页面公用,函数的内部外部都可以访问. 2.局部变量:在函数内部定义的变量,只能在定义该变量的函数内部访问,外部无法访问.函数内部访问变量时,先在内部查找是否有此变量,如果有,就使用内部,如果没有,就去外部查找 二.封闭函数封闭函数是javascript中匿名函数的另外一种写法,创建一个一开始就执行而不用命名的函数. 1.一般函数的定义和执行函数 2.封闭函数的定义和执

  • 详解 Python中LEGB和闭包及装饰器

    详解 Python中LEGB和闭包及装饰器 LEGB L>E>G?B L:local函数内部作用域 E:enclosing函数内部与内嵌函数之间 G:global全局作用域 B:build-in内置作用域 python 闭包 1.Closure:内部函数中对enclosing作用域变量的引用 2.函数实质与属性 函数是一个对象 函数执行完成后内部变量回收 函数属性 函数返回值 passline = 60 def func(val): if val >= passline: print (

  • javascript笔记之匿名函数和闭包

    本文介绍了js匿名函数和闭包的相关内容,供大家参考,具体内容如下 匿名函数 <script type="text/javascript"> //function(){}//会报错 var fun = function(){};//将匿名函数赋值给变量 (function(){})();//匿名函数自执行 function(){ return function(){};//函数里的匿名函数 } </script> 闭包 闭包是指有权访问另一个函数作用域中的变量的函

随机推荐