【Rust 笔记】08-枚举与模式

08 - 枚举与模式

8.1 - 枚举

  • 枚举的定义:

    enum Ordering {
      Less,
      Equal,
      Greater
    }
    
    • 枚举的值被称为变体,或者构造式(constructor):

      • Ordering::Less
      • Ordering::Equal
      • Ordering::Greater
    • 这个枚举是标准库中定义的,可以在代码中直接导入它本身:【推荐】

      use std::cmp::Ordering;
      
    • 或者,导入它的所有构造式:除非可以让代码更好理解,否则不要直接导入构造式。

      use std::cmp::Ordering::*;
      
  • 如果要导入当前模块中声明的枚举的构造式,要使用 self 参数:

    enum Pet {
      Orca,
      Giraffe
    }
    
    use self::Pet::*;
    
  • Rust 允许把枚举值转换为整数,但不能直接把整数转换为枚举值。

  • Rust 要保证枚举值一定是在 enum 声明中定义的。

  • 枚举和结构体一样,支持使用#[derive] 属性,来标记共有特型。

  • 枚举和结构体一样,也可以通过 impl 块定义方法。

8.1.1 - 包含数据的枚举

  • 枚举包含什么数据,就称为什么变体:

    #[derive(Copy, Clone, Debug, PartialEq)]
    enum RoughTime {
      InThePast(TimeUnit, u32),
      JustNow,
      InTheFuture(TimeUnit, u32)
    }
    
    • InThePastInTheFuture 称为元组变体(tuple variant)
    • 枚举值也可以是结构体变体
    • RoughTme 的每个构造式占用 8 个字节。
  • Rust 有三种枚举变体:

    • 类基元变体:没有数据的变体。

      enum EnumName;
      
    • 元组变体:

      enum EnumName (...);
      
    • 结构体变体:

      enum EnumName {
        ...
      }
      
    • 一个枚举中可以同时包含这三种变体数据:

      enum RelationshipStatus {
        Single,
        InARelationship,
        ItsComplicated(Option<String>),
        ItsExtremelyComplicated {
          car: DifferentialEquation,
          cdr: EarlyModernistPoem
        }
      }
      
  • 公有枚举的所有构造式和字段自动都是公有的。

8.1.2 - 枚举的内存布局

  • 整数标签是 Rust 内部使用的字段,通过它可以知道是哪个构造式创建了当前的值,以及当前的值包含哪些字段。
  • Rust 对某些枚举会直接优化掉标签字段。如泛型枚举。

8.1.3 - 使用枚举的富数据结构

  • 枚举可以用来快速实现类似树的数据结构。

  • Rust 实现 Json 枚举:

    enum Json {
      Null,
      Boolean(bool),
      Number(f64),
      String(String),
      Array(Vec<Json>),
      Object(Box<HashMap<String, Json>>)
    }
    
    • JSON 标准规定了可以出现在 JSON 文档中的不同数据类型有:null、布尔值、数值、字符串、JSON 值数组、以字符串为键以 JSON 值为值的对象。
    • Box 包装 HashMap 来表示 Object,可以让所有 JSON 值更简洁。
    • JSON 类型的值占 4 个机器字。
      • StringVec 值占 3 个机器字
      • Box 占 1 个机器字

8.1.4 - 泛型枚举

  • 常见的泛型枚举:

    enum Option<T> {
      None,
      Some(T)
    }
    
    enum Result<T, E> {
      Ok(T),
      Err(E)
    }
    
  • 基于泛型的数据结构举例:实现一个二叉树

    // 创建T类型值的有序集合
    enum BinaryTree<T> {         // BinaryTree的值只占1个机器字
      Empty,                     // 不包含任何数据
      NonEmpty(Box<TreeNode<T>>) // 包含一个Box,它是指向位于堆内存的TreeNode的指针
    }
    
    // BinaryTree的节点
    struct TreeNode<T> {
      element: T,           // 实际的元素
      left: BinaryTree<T>,  // 左子树
      right: BinaryTree<T>  // 右子树
    }
    
    • 创建这个树的任何特定节点:

      use self::BinaryTree::*
      let jupiter_tree = NonEmpty(Box::new(TreeNode {
        element: "Jupiter",
        left: Empty,
        right: Empty
      }));
      
    • 大一点的树可以基于小一点的树创建

      // 将jupiter_node和mercury_node的所有权,通过赋值转移给新的父节点mars_tree
      let mars_tree = NonEmpty(Box::new(TreeNode {
        element: "Mars",
        left: jupiter_tree,
        right: mercury_tree
      }));
      
    • 根节点也使用相同的方式创建:

      let tree = NonEmpty(Box::new(TreeNode {
        element: "Saturn",
        left: mars_tree,
        right: uranus_tree
      }));
      
    • 假如这个树有一个 add 方法,那么可以通过这样调用这个树:

      let mut tree = BinaryTree::Empty;
      for planet in planets {
        tree.add(planet);
      }
      

8.2 - 模式

  • 访问枚举数据的唯一安全方式:模式匹配。

    • 定义一个 RoughTme 枚举类型:

      enum RoughTime {
        InThePast(TimeUnit, u32),
        JustNow,
        InTheFuture(TimeUnit, u32)
      }
      
    • 使用 match 表达式访问枚举的数据:

      fn rough_time_to_english(rt: RoughTime) -> String {
        match rt {
          RoughTime::InThePast(units, count) =>
              format!("{} {} ago", count, units.plural()),
          RoughTime::JustNow =>
              format!("just now"),
          RoughTime::InTheFuture(units, count) =>
              format!("{} {} from now", count, units.plural())
        }
      }
      
  • 枚举、结构体或元组在匹配模式时,会从左到右堆模式的每个组件,依次检查当前值是否与之匹配。如果不匹配,就会进入到下一个模式。

  • 模式的特型

  • Rust 模式本身就是一个迷你语言:

    模式类型 示例 说明
    字面量 100
    “name”
    匹配确切的值;const 声明的名字也可以
    范围 0 … 100
    ‘a’ … ‘k’
    匹配范围中的任意值,包括最终值
    通配符 _ 匹配任意值并忽略该值
    变量 name
    mut count
    类似_,但会把匹配的值转移或复制到新的局部变量
    ref 变量 ref field
    ref mut field
    不转移或复制匹配的值,而是借用匹配值的引用
    子模式绑定 val @ 0 … 99
    ref circle @
    Shape::Circle { .. }
    匹配 @右侧的模式,使用左侧的变量名
    枚举模式 Some(value)
    None
    Pet::Orca
    元组模式 (Key, value)
    结构体模式 Color(r, g, b)
    Point { x, y }
    Card { suit: Clubs, rank: n }
    Account { id, name, … }
    引用 &value
    &(k, v)
    只匹配引用值
    多个模式 'a' 'A'
    护具表达式 x if x * x <= r2 仅限 match(不能在 let 等中使用)

8.2.1 - 模式中的字面量、变量和通配符

  • 0、1 等整数值可以作为模式使用:

    match meadow.count_rabbits() {
      0 => {},
      1 => println!("A rabbit is nosing around in the clover."),
      n => println!("There are {} rabbits hopping about in the meadow", n)
    }
    
  • 其他类型的字面量也可以用作模式,包括布尔值、字符,甚至字符串:

    let calendar = 
        match settings.get_string("calendar") {
          "gregorian" => Calendar::Gregorian,
          "chinese" => Calendar::Chinese,
          "ethiopian" => Calendar::Ethiopian,
          other => return parse_error("calendar", other)
    };
    
  • 可以使用通配符_作为模式,以匹配任意值,但不保存匹配的值:

    let caption = 
        match photo.tagged_pet() {
          Pet::Tyrannosaur => "RRRAAAAAHHHHHH",
          Pet::Samoyed => "*dog thoughts*",
          _ => "I'm cute, love me"  // 通用标题,任何宠物都适用
    };
    
  • 每个 match 表达式最后都会有一个通配符,即使非常确定其他情况不会发生,也必须至少加上一个后备的诧异分支:

    // 有很多形状(shape),但只支持“选择”某些文本,
    // 或者一个矩形区域中的所有内容,不能选择椭圆或梯形。
    match document.selection() {
      Shape::TextSpan(start, end) => paint_text_selection(start, end),
      Shape::Rectangle(rect) => paint_rect_selection(rect),
      _ => panic!("unexpected selection type")
    }
    
  • 为了避免最后一个分支无法运行到,可以结合 if 表达式实现模式:

    fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
      match point_to_hex(click) {
        None => Err("That's not a game space"),
        Some(hex) =>
            if hex == current_hex {
              Err("You are already there! You must click somewhere else")
            } else {
              Ok(hex)
            }
      }
    }
    

8.2.2 - 元组与结构体模式

  • 元组模式匹配元组,适合在一个 match 表达式中同时匹配多个数据:

    fn describe_point(x: i32, y: i32) -> &'static str {
      use std::cmp::Ordering::*;
      match (x.cmp(&0), y.cmp(&0)) {
        (Equal, Equal) => "at the orgin",
        (_, Equal) => "on the x axis",
        (Equal, _) => "on the y axis",
        (Greater, Greater) => "in the first quadrant",
        (Less, Greater) => "in the second quadrant",
        _ => "somewhere else"
      }
    }
    
  • 结构体模式使用花括号,类似结构体表达式,其中的每个字段都是一个子模式:

    match balloon.location {
      Point {
        x: 0,
        y: height
      } => println!("straight up {} meters", height),
      Point {
        x: x,
        y: y
      } => println!("at ({}m, {}m)", x, y)
    }
    
  • 对于复杂的结构体,为了使代码简洁,可以使用.. 表示不关心其他字段:

    match get_account(id) {
      ...
      Some(Account {
        name,
        language,
        ..
      }) => language.show_custom_greeting(name)
    }
    

8.2.3 - 模式的引用

  • 对于引用,Rust 支持两种模式:

    • ref 模式:借用匹配值的元素
    • & 模式:匹配引用
  • 一般情况下,匹配不可复制的值会转移值:

    match account {
      Account {
        name, language, ..
      } => {
        ui.greet(&name, &language);
        ui.show_settings(&account);  // 错误:使用了转移的值account
      }
    }
    
  • 使用 ref 模式,可以借用匹配的值,而不转移它:

    match account {
      Account {
        ref name,
        ref language,
        ..
      } => {
        ui.greet(name, language);
        ui.show_settings(&account);
      }
    }
    
  • 还可以用 ref mut 借用 mut 引用:

    match line_result {
      Err(ref err) => log_error(err),  // err是&Error(共享的ref)
      // 模式Ok(ref mut line)可以匹配任何成功的结果,并借用该结果中存储的值的mut引用
      Ok(ref mut line) => {            // line是&mut String(可修改的ref)
        trim_comments(line);           // 就地修改字符串
        handle(line);
      }
    }
    
  • ref 模式对应的是 & 模式。以 & 开头的模式匹配引用:

    match sphere.center() {
      &Point3d {x, y, z} => ...
    }
    
  • 匹配引用遵循引用的规则:

    • 生命期;
    • 不能对共享引用采取 mut 操作;
    • 不能从引用(包括 mut 引用)中转移出值。

8.2.4 - 匹配的多种可能性

  • 竖线 | 可用于在一个 match 分支中组合多个模式:

    let at_end = 
        match chars.peek() {
          Some(&'\r') | Some(&'\n') | None => true,
          _ => false
    };
    
  • 使用... 可以匹配某个范围中的值。范围模式包含起点值和终点值,即'0' ... '9' 匹配所有 ASCII 数字:

    match next_char {
      '0' ... '9' => self.read_number(),
      'a' ... 'z' | 'A' ... 'Z' => self.read_word(),
      ' ' | '\t' | '\n' => self.skip_whitespace(),
      _ => self.handle_punctuation()
    }
    
  • 全纳(inclusive)范围...:对模式匹配比较适用。

  • 互斥范围..:对循环和片段比较适用。

8.2.5 - 模式护具

  • 使用 if 关键字可以为 match 分支添加护具。只有在护具求值为 true 时匹配才成功,如果护具求值为 false,那么 Rust 会继续匹配下一个模式。

    match robot.last_known_location() {
      Some(point) if self.distance_to(point) < 10 =>
          short_distance_strategy(point),
      Some(point) =>
          long_distance_strategy(point),
      None =>
          searching_strategy()
    }
    

8.2.6-@模式

  • x @ pattern 可以匹配给定的 pattern,可以把匹配值整个转移或复制到一个变量 x 中:

    • 创建如下的模式:

      match self.get_selection() {
        Shape::Rect(top_left, bottom_right) =>
            optimized_paint(&Shape::Rect(top_left, bottom_right)),
        other_shape =>
            paint_outline(other_shape.get_outline()),
      }
      
    • Shape::Rect 分支拆解出值后,可以再重新创建一个相同的值,所以可以用 @模式重写:

      rect @ Shape::Rect(..) => Optimized_paint(&rect),
      
    • @模式支持匹配范围值:

      match chars.next() {
        Some(digit @ '0' ... '9') => read_number(digit, chars),
        ...
      }
      

8.2.7 - 模式的使用场景

  • 通过模式匹配可以实现拆解值,而不是仅仅把值保存在一个变量中:

    • 用在 match 表达式中

    • 用来代替标识符

    • 用于将结构体拆解为 3 个新的局部变量:

      let Track { album, track_number, title, .. } = song;
      
    • 用于拆解作为函数参数的元组:

      fn distance_to((x, y): (f64, f64)) -> f64 { ... }
      
    • 用于迭代 HashMap 的键和值:

      for (id, document) in &cache_map {
        println!("Document #{}: {}", id, document.title);
      }
      
    • 用于自动对传给闭包的参数解引用:

      let sum = numbers.fold(0, |a, &num| a + num);
      
  • 上述例子,在 JavaScript 中叫解构(restructuring),Python 中叫解包(unpacking)。

  • 不可驳模式(irrefutable pattern):始终都可以匹配的模式。可用于:

    • let 后面。
    • 函数参数中。
    • for 后面。
    • 闭包参数中。
  • 可驳模式(refutable pattern):可能不会匹配的模式。可用于:

    • match 表达式。

    • if let 表达式。

    • while let 表达式。

    • 只处理一种特定的枚举变体:

      if let RoughTime::InTheFuture(_, _) = user.date_of_birth() {
        user.set_time_traveler(true);
      }
      
    • 只在查表成功时运行某些代码:

      if let Some(document) = cache_map.get(&id) {
        return send_cached_response(document);
      }
      
    • 不成功则重复做一些事:

      while let Err(err) = present_cheesy_anti_robot_task() {
        log_robot_attempt(err);
      }
      
    • 手工便利一个迭代器:

      while let Some(_) = lines.peek() {
        read_paragraph(&mut lines);
      }
      

8.2.8 - 填充二叉树

实现 BinaryTree::add() 方法,用于向 BinaryTree 中添加相同类型的子节点:

enum BinaryTree<T> {
  Empty,
  NonEmpty(Box<TreeNode<T>>)
}

struct TreeNode<T> {
  element: T,
  left: BinaryTree<T>,
  right: BinaryTree<T>
}

impl<T: Ord> BinaryTree<T> {
  fn add(&mut self, value: T) {
    match *self {
      BinaryTree::Empty =>
          *self = BinaryTree::NonEmpty(Box::new(TreeNode {
            element: value,
            left: BinaryTree::Empty,
            right: BinaryTree::Empty
          })),
      BinaryTree::NonEmpty(ref mut node) =>
          if value <= node.element {
            node.left.add(value);
          } else {
            node.right.add(value);
          }
    }
  }
}
  • *self 为空,那么运行 BinaryTree::Empty 模式,把 Empty 树改成 NonEmpty 树。

  • *self 不为空,那么运行 BinaryTree::NonEmpty(ref mut node) 模式,可以访问并修改该树节点中的数据。

  • 调用这个 add 方法:

    let mut tree = BinaryTree::Empty;
    tree.add("Mercury");
    tree.add("Venus");
    ...
    

8.3 - 模式的设计

  • Rust 的枚举,也被称为代数数据类型(algebraic data type)。与下述特点密切相关:
    • 变体(variant)
    • 引用
    • 可变性(mutability)
    • 内存安全
  • 函数变成语言不需要可变性。
  • C 的 union 联合体同时支持变体、指针(引用)和可变性,但它不是内存安全的。
  • 枚举是特定数据形态的设计工具:
    • 一个值可能是一个值;
    • 也可能是另外一个值;
    • 还有可能什么也不是
  • 枚举不支持扩展枚举:
    • 如果要添加一个新变体,那么只能修改枚举的声明
    • 修改了枚举的声明,必须重新检查匹配中每个变体的每个 match 表达式,因为需要给他们都添加一个新分支以处理新变体。
  • Rust 的特型比枚举更具有灵活性。

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十章
原文地址

你可能感兴趣的:(rust,rust,开发语言,后端)