深入浅出 Rust笔记 Series 3

系列导航

这是我在阅读范长春的《深入浅出 Rust》时做的笔记,绝大部分内容源自此书,另有小部分内容源自 Rust 圣经。这两份资料是我入门 Rust 的主要材料。

第三部分 高级抽象

chap 22 闭包

  • 闭包(closure)是一种匿名函数,具有“捕获”外部变量的能力。闭包有时候也被称作 lambda 表达式。它有两个特点:(1)可以像函数一样被调用;(2)可以捕获当前环境中的变量。

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let add = | a :i32, b:i32 | -> i32 { return a + b; } ;
    // 闭包的参数和返回值类型都是可以省略的
    // let add = |a, b| { a + b };
    let x = add(1,2);
    println!("result is {}", x);
    }
  • 变量捕获:closure 的原理与 C++11 的 lambda 非常相似。当一个 closure 创建的时候,编译器帮我们生成了一个匿名 struct 类型,通过自动分析 closure 的内部逻辑,来决定该结构体包括哪些数据,以及这些数据该如何初始化。

    • 在保证能编译通过的情况下,编译器会自动选择一种对外部影响最小的类型存储。对于被捕获的类型为 T 的外部变量,在匿名结构体中的存储方式选择为:尽可能先选择&T 类型,其次选择&mut T 类型,最后选择 T 类型。
  • move 关键字:闭包前加上 move 关键字,所有的变量捕获全部使用 by value 的方式,所有被捕获的外部变量所有权一律转移进闭包。一般用于闭包需要传递到函数外部(escaping closure)的情况。

  • Fn/FnMut/FnOnce:闭包被调用的时候,不需要执行某个成员函数,而是采用类似函数调用的语法来执行。这是因为它自动实现了编译器提供的几个特殊的 trait,Fn 或者 FnMut 或者 FnOnce。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
    }

    pub trait FnMut<Args> : FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
    }

    pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
    }

    这几个 trait 的主要区别在于,被调用的时候 self 参数的类型。FnOnce 被调用的时候,self 是通过 move 的方式传递的,因此它被调用之后,这个闭包的生命周期就已经结束了,它只能被调用一次;FnMut 被调用的时候,self 是&mut Self 类型,有能力修改当前闭包本身的成员,甚至可能通过成员中的引用,修改外部的环境变量;Fn 被调用的时候,self 是&Self 类型,只有读取环境变量的能力。

    • 对于一个闭包,编译器是如何选择 impl 哪个 trait 呢?答案是,编译器会都尝试一遍,实现能让程序编译通过的那几个。闭包调用的时候,会尽可能先选择调用 ​fn call(&self,args:Args)函数,其次尝试选择 fn call_mut(&self,args:Args)​​​ 函数,最后尝试使用 ​fn call_once(self,args:Args) ​函数。
    • 为自定义类型实现 Fn trait 要使用 nightly 版本的 Rust
  • 每个闭包,编译器都会为它生成一个匿名结构体类型;即使两个闭包的参数和返回值一致,它们也是完全不同的两个类型,只是都实现了同一个 trait 而已。

  • 闭包与泛型约束

    1
    2
    3
    4
    fn call_with_closure<F>(some_closure: F) -> i32
    where F : Fn(i32) -> i32 {
    some_closure(1)
    }
    • 其中泛型参数 F 的约束条件是 F:Fn(i32)->i32。这里 Fn(i32)-> i32 是针对闭包设计的专门的语法,而不是像普通 trait 那样使用 Fn<i32,i32> 来写。这样设计为了让它们看起来跟普通函数类型 fn(i32)->i32 更相似。除了语法之外,Fn FnMut FnOnce 其他方面都跟普通的泛型一致。
  • 向函数中传递闭包的两种方式(闭包作为函数参数)

    • 通过泛型的方式。这种方式会为不同的闭包参数类型生成不同版本的函数,实现静态分派。

      1
      2
      3
      4
      5
      6
      // 这里是泛型参数。对于每个不同类型的参数,编译器将会生成不同版本的函数
      fn static_dispatch<F>(closure: &F)
      where F: Fn(i32) -> i32
      {
      println!("static dispatch {}", closure(42));
      }
    • 通过 trait object 的方式。这种方式会将闭包装箱进入堆内存中,向函数传递一个胖指针,实现运行期动态分派。

      1
      2
      3
      4
      5
      // 这里是 `trait object``Box<Fn(i32)->i32>`也算`trait object`
      fn dynamic_dispatch(closure: &Fn(i32)->i32)
      {
      println!("dynamic dispatch {}", closure(42));
      }
  • 闭包作为函数的返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    // 如果我们希望一个闭包作为函数的返回值,那么就不能使用泛型的方式
    // 了。因为如果泛型类型不在参数中出现,而仅在返回类型中出现的话,会要
    // 求在调用的时候显式指定类型,编译器才能完成类型推导。可是调用方根本
    // 无法指定具体类型,因为闭包类型是匿名类型,用户无法显式指定。所以下
    // 面这样的写法是编译不过的:

    fn test<F>() -> F
    where F: Fn(i32)->i32
    {
    return | i | i * 2;
    }

    // 但下面这种是可以的
    fn test<F>(arg: F) -> F
    where F: Copy
    {
    arg
    }
    • 静态分派。我们可以用一种新的语法 fn test() -> impl Fn(i32)-> i32​ 来实现。
    • 动态分派。就是把闭包装箱进入堆内存中,使用 Box<dyn Fn(i32) -> i32>​ 这种 trait object 类型返回

chap 23 动态分派和静态分派

  • Rust 可以同时支持“静态分派”(static dispatch)和“动态分派”(dynamic dispatch)。所谓“静态分派”,是指具体调用哪个函数,在编译阶段就确定下来了。Rust 中的“静态分派”靠泛型以及 impl trait 来完成。对于不同的泛型类型参数,编译器会生成不同版本的函数,在编译阶段就确定好了应该调用哪个函数。所谓“动态分派”,是指具体调用哪个函数,在执行阶段才能确定。Rust 中的“动态分派”靠 Trait Object 来完成。Trait Object 本质上是指针,它可以指向不同的类型;指向的具体类型不同,调用的方法也就不同。

  • Traitobject:向 Trait 的指针。&dyn Trait、&mut dyn Trait、Box、const dyn Trait*、*mut dyn Trait 以及 Rc 等等都是 Trait Object。

    1
    2
    3
    4
    pub struct TraitObject {
    pub data: *mut (),
    pub vtable: *mut (),
    }
    • 自己在想:Trait Object 中指向实际数据的指针有什么用?
      answer:访问 trait 中方法的 self 参数
    • Rust 的动态分派和 C++ 的动态分派,内存布局有所不同。在 C++ 里,如果一个类型里面有虚函数,那么每一个这种类型的变量内部都包含一个指向虚函数表的地址。而在 Rust 里面,对象本身不包含指向虚函数表的指针,这个指针是存在于 trait object 指针里面的。如果一个类型实现了多个 trait,那么不同的 trait object 指向的虚函数表也不一样。
  • object safe。有以下条件之一则不是 object safe,不能创建 trait object

    • 当 trait 有 Self:Sized 约束时。如果不希望一个 trait 通过 trait object 的方式使用,可以为它加上 Self:Sized 约束。同理,如果我们想阻止一个函数在虚函数表中出现,可以专门为该函数加上 Self:Sized 约束。
    • 当函数中有 Self 类型作为参数(除了 self)或者返回类型时。Rust 规定,如果函数中除了 self 这个参数之外,还在其他参数或者返回值中用到了 Self 类型,那么这个函数就不是 object safe 的。这样的函数是不能使用 trait object 来调用的。这样的方法是不能在虚函数表中存在的。
    • 当函数第一个参数不是 self 时。如果有“静态方法”,那这个“静态方法”是不满足 object safe 条件的。这个条件几乎是显然的,编译器没有办法把静态方法加入到虚函数表中。如果一个 trait 中存在静态方法,而又希望通过 trait object 来调用其他的方法,那么我们需要在这个静态方法后面加上 Self:Sized 约束,将它从虚函数表中剔除。
    • 当函数有泛型参数时。通过 trait object 调用成员的方法是通过 vtable 虚函数表来进行查找并调用。现在需要被查找的函数成了泛型函数,而泛型函数在 Rust 中是编译阶段自动展开的。这里有一个根本性的冲突问题。Rust 选择的解决方案是,禁止使用 trait object 来调用泛型函数,泛型函数是从虚函数表中剔除了的。这个行为跟 C++ 是一样的。C++ 中同样规定了类的虚成员函数不可以是 template 方法。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!