【Rust 笔记】07-结构体

07 - 结构体

  • Rust 结构体,也称结构,等同于 C/C++ 中的 struct 类型、Python 中的类和 JavaScript 中的对象。
  • 结构体把各个类型的值聚集为一个值,以便作为一个整体(集合)来处理。
  • Rust 有 3 种结构体类型。
    • 命名字段(named-field)结构体:每个组件都有一个名字。
    • 类元组(tuple-like)结构体:以组件出现的次序标记它们。
    • 类基元(unit-like)结构体:根本没有组件。

7.1 - 命名字段结构体

  • 命名字段结构体的定义:

    /// 8位灰阶像素的矩形
    struct GrayscaleMap {
        pixels: Vec<u8>,
        size: (usize, usize)
    }
    
  • 结构体的命名约定:

    • 驼峰拼写法(CamelCase):每个单词的首字母要大写。
    • 蛇形拼写法(snake_case):字段和方法的名字要小写,单词间以下划线分割。
  • 结构体表达式(struct expression):用于创建结构体的值。

    let width = 1024;
    let height = 576;
    let image = GrayscaleMap {
      pixels: vec![0; width * height],
      size: (width, height)
    };
    
  • 访问结构体中的字段:使用. 操作符。

  • 结构体默认是私有的,只在声明它的模块中可见。结构体中的字段默认也是私有的。

  • 如果要让结构体对模块外部可见,需要在它的定义之前加上 pub 关键字。

  • 公有结构体,其字段仍然可以私有。

    • 其他模块可以使用这个结构体以及它的任何公有方法
    • 但是不能通过名字访问其私有字段
    • 也不能使用结构体表达式创建新的结构体值
  • 创建命名字段结构体的值:

    • 创建结构体值要求结构体的所有字段都必须是可见的。
    • 创建命名字段结构体值时,可以使用另一个相同类型的结构体来提供省略的字段值。
  • 在结构体表达式中,假设 EXPR 是结构体类型的值,如果命名字段结构体后面跟着..EXPR,那么任何没有出现的字段都将从 EXPR 中取得自己的值。

    struct Broom {
      name: String,
      height: u32,
      health: u32,
      position: (f32, f32, f32),
      intent: BroomIntent
    }
    
    // 扫帚(Broom)可以做copy和clone操作
    #[derive(Copy, Clone)]
    enum BroomIntent {
      FetchWater,
      DumpWater
    }
    
    // 按值接收Broom,取得所有权
    fn chop(b: Broom) -> (Broom, Broom) {
      // 基于b初始化broom1,只修改height。因为String不是可复制类型。
      // 所以broom1取得b名字的所有权
      let mut broom1 = Broom {
        height: b.height / 2,
        ..b
      };
    
      // 基于broom1初始化broom2。因为String不是可复制类型。
      // 所以必须显示地克隆name
      let mut broom2 = Broom {
        name: broom1.name.clone(),
        ..broom1
      };
    
      // broom1和broom2起不同的别名
      broom1.name.push_str(" I");
      broom2.name.push_str(" II");
    
      (broom1, broom2)
    }
    
    fn main() {
      let hokey = Broom {
        name: "Hokey".to_string(),
        height: 60,
        health: 100,
        position: (100.0, 200.0, 0.0),
        intent: BroomIntent::FetchWater
      };
    
      let (hokey1, hokey2) = chop(hokey);
    
      println!("{}", hokey1.name);      // "Hokey I"
      println!("{}", hokey1.health);    // 100
    
      println!("{}", hokey2.name);      // "Hokey II"
      println!("{}", hokey2.health);    // 100
    }
    

7.2 - 类元组结构体

  • 类元组结构体,类似于元组:

    struct Bounds (usize, usize);
    
  • 创建类元组结构体的值:

    let image_bounds = Bounds(1024, 768);
    
    // Bounds(1024, 768)类似于函数调用
    // 因为Rust会隐式地定义一个下面这样的函数:
    fn Bounds(elem0: usize, elem1: usize) -> Bounds { ... }
    
    • 类元组结构体的值称为元素,访问这些值与元组一样:

      assert_eq!(image_bounds.0 * image_bounds.1, 786432);
      
    • 类元组结构体中个别的元素可以是公有的,也可以不是。

      pub struct Bounds (pub usize, pub usize);
      
  • 命名字段结构体与类元组结构体的使用场景:

    • 命名字段结构体:如果要经常使用. 操作符取得值的组件,则建议使用命名字段结构体。
    • 类元组结构体:如果要经常使用模式匹配来查询元素,则建议使用类元组结构体。
  • 类元组结构体适合创建新类型(newtype):包含一个要经过严格类型检查的组件的结构体。

    struct Ascii(Vec<u8>);
    

7.3 - 类基元结构体

  • 类基元结构体是一种没有任何元素的结构体:

    struct Onesuch;
    
    • 这种类型只有一个值,就是它本身:

      let o = Onesuch;
      
    • 值不占内存。

  • 使用案例:

    • 表达式 3..5 是对结构值 Range {start: 3, end: 5} 的简写;
    • 表达式.. 是对类基元结构体值 RangeFull 的简写。

7.4 - 结构体的布局

  • Rust 不保证结构体的字段或元素在内存中会以某种顺序存储。

  • Rust 保证把字段的值直接存储在结构体的内存块中。

  • 对于下述结构体:

    struct GrayscaleMap {
      pixels: Vec<u8>,
      size: (usize, usize)
    }
    
    • Rust 直接把 pixelssize 放在 GrayscaleMap 值的内存里;
    • 只有 pixels 向量拥有自己分配在堆上的内存块。
  • 使用#[repr(C)] 属性,可以兼容 C/C++ 在内存中存储结构体的方式。

7.5-impl 定义方法

  • impl 块:为结构体定义方法,由一系列 fn 定义的关联函数(即方法)组成的集合。

    /// 实现一个后进先出的字符队列
    pub struct Queue {
      older: Vec<char>,   // 旧元素,最先进的在最后
      younger: Vec<char>  // 新元素,最新进的在最后
    }
    
    impl Queue {
      /// 把一个字符推到队列的后端
      pub fn push(&mut self, c: char) {
        self.younger.push(c);
      }
    
      /// 从队列前端取出一个字符,如果可以,取出字符返回Some(c),
      /// 否则,如果队列是空的,返回None
      pub fn pop(&mut self) -> Option<char> {
        if self.older.is_empty() {
          if self.younger.is_empty() {
            return None;
          }
    
          // 把younger中的元素转移到older中,
          // 保持对外承诺的顺序
          use std::mem::swap;
          swap(&mut self.older, &mut self.younger);
          self.order.reverse();
        }
    
        // 到这里,older肯定有元素。
        // Vec的pop方法已经返回Option
        self.older.pop()
      }
    }
    
    • self:作为第一个参数传入,并且要在其上调用这个方法的值。

      • self 代表:self: Queue
      • &self 代表:self: &Queue
      • &mut self 代表:self: &mut Queue
    • Rust 必须显示使用 self 来引用调用该方法时作为上下文的那个值。

    • 普通的方法调用语法时,会自动实现隐式调用。

      let mut q = Queue {  // 结构体实例化
        older: Vec::new(),
        younger: Vec::new()
      };
      
      q.push('0');
      q.push('1');
      assert_eq!(q.pop, Some('0'));
      
      q.push('f');
      assert_eq!(q.pop(), Some('1'));
      assert_eq!(q.pop(), Some('f'));
      assert_eq!(q.pop(), None);
      
    • 如果方法不需要修改其 self,那么可以让它接收一个共享引用

      impl Queue {
        pub fn is_empty(&self) -> bool {
          self.older.is_empyt() && self.younger.is_empty()
        }
      }
      
    • 方法调用表达式知道要借用哪种引用:

      assert!(q.is_empty());
      q.push('q');
      assert!(!q.is_empty);
      
    • 如果方法要取得 self 的所有权,则可以取得 self 的值:

      impl Queue {
        pub fn split(self) -> (Vec<char>, Vec<char>) {
          (self.older, self.younger)
        }
      }
      
      let mut q = Queue {
        older: Vec::new(),
        younger: Vec::new()
      };
      
      q.push('P');
      q.push('D');
      assert_eq!(q.pop(), Some('P'));
      q.push('X');
      
      let (older, younger) = q.split();
      // q变成未初始化状态
      assert_eq!(older, vec!['D']);
      assert_eq!(younger, vec!['X']);
      
    • 如果 split 取得了 self 的值,从而把 Queue 转移出了 q,会导致 q 变为未初始化状态。

  • 关联函数(associated function):即方法,是与特定类型关联的;

  • 自由函数(free function),不是作为 impl 块中的构成项定义的函数。

  • 静态方法:还支持定义不将 self 作为参数的方法,形成与结构体类型本身关联的函数,而不是关联该类型的值。通常用于定义构造器函数。

    impl Queue {
      pub fn new() -> Queue {
        Queue {
          older: Vec::new(),
          younger: Vec::new()
        }
      }
    }
    
    • 引用这个方法的方式:类型名、双冒号和方法名:

      let mut q = Queue::new();
      
      q.push('*');
      ...
      
  • 一个结构体类型支持对应多个 impl 块,但这些块必须全部与定义该结构体类型在同一个 Rust 包中。

  • Rust 将类型定义与方法定义分开,有以下好处:

    • 容易识别类型的数据成员;
    • 使得类基元结构体和类元组结构体更加简洁;
    • impl 方法也可用于实现特型。

7.6 - 泛型结构体

  • 泛型(generic)结构体:创建一个模板,可以在其冲插入任何类型。

    /// 对于任意类型T,Queue<T>包含两个Vec<T>类型的字段
    pub struct Queue<T> {
      older: Vec<T>,
      younger: Vec<T>
    }
    
    • Queue<T> 中的 <T> 读作:对于任何元素类型 T
    • Vec 本身也是一个泛型结构体。
  • 类型参数:泛型结构体中,位于尖括号 <> 中的 “类型名”。

  • 泛型结构体的 impl 块如下所示:

    impl <T> Queue<T> {
      pub fn new() -> Queue<T> {
        Queue {
          older: Vec::new(),
          younger: Vec::new()
        }
      }
    
      pub fn push(&mut self, t: T) {
        self.younger.push(t);
      }
    
      pub fn is_empty(&self) -> bool {
        self.older.is_empty() && self.younger.is_empty()
      }
      ...
    }
    
    • Self 类型不同于 self,表示把方法添加到其中的任意类型

      pub fn new() -> Self {
        Queue {
          older: Vec::new(),
          younger: Vec::new()
        }
      }
      
    • 在上述 new 的方法体中,不需要在构造表达式中写出类型参数,只需要写成 Queue {...}

    • 在函数签名和类型定义中,仍然需要给出类型参数。

  • 调用静态方法时,可以使用极速鱼符号::<>,显示地提供类型参数:

    let mut q = Queue::<char>::new();
    
    • 实际开发中,Rust 可以自动推断类型:

      let mut q = Queue::new();
      let mut r = Queue::new();
      
      q.push("CAD"); // 自动推断为Queue<&'static str>
      r.push(0.74);  // 自动推断为Queue<f64>
      

7.7 - 带生命期参数的结构体

  • 如果结构体类型中包含引用,那么必须指定这些引用的生命期:

    struct Extrema<'elt> {
      greatest: &'elt i32,
      least: &'elt i32
    }
    
    • struct Extrema<'elt> 理解为:给定任意生命期'elt,都可创建一个包含具有该生命期引用的 Extrema<'elt> 结构体。

      /// 找出最大值和最小值
      fn find_extrema<'s>(slice: &'s [i32]) -> Extrema<'s> {
        let mut greatest = &slice[0];
        let mut least = &slice[0];
      
        for i in 1..slice.len() {
          if slice[i] < *least {
            least = &slice[i];
          }
          if slice[i] > *greatest {
            greatest = &slice[i];
          }
          Extrema {
            greatest;
            least
          }
        }
      }
      
    • Rust 会推断函数调用的生命期,不需要在调用中指出。

      let a = [0, -3, 0, 15, 48];
      let e = find_extrema(&a);
      assert_eq!(*e.least, -3);
      assert_eq!(*e.greatest, 48);
      
    • 实际开发中,find_extrema 的函数签名可以写成如下所示:返回类型与某个参数的生命期相同。

      fn find_extrema(slice: &[i32]) -> Extrema {
        ...
      }
      

7.8 - 为结构体类型派生共有特型

  • 类型的 CopyCloneDebugPartialEq 等特性,在 Rust 中被称为共有或标准特型(trait)。

  • 简单结构体如下所示,默认不支持标准特型:

    struct Point {
      x: f64,
      y: f64
    }
    
    • 如果需要派生共有特型,则需要使用#[derive] 属性标记:

      #[derive(Copy, Clone, Debug, PartialEq)]
      struct Point {
        x: f64,
        y: f64
      }
      
    • 只要结构体中的字段类型,支持某些特型,那么这个结构体就可以派生这样的特型。

7.9 - 内部修改能力

  • 内部修改能力(interior mutability):在一个不可修改的值内部,有一个可修改的数据。

  • std::cell 中定义了两种类型 Cell<T>RefCell<T>,可以使目标类型支持这种能力。

  • Cell<T> 是一个包含 T 类型的私有值的结构体,不需要对其创建 mut 引用,即可读取或设置其内的私有字段的值。

    • Cell::new(value):创建一个新的 Cell 类型的变量,将 value 值转移到其中。

    • cell.get():返回 cell 中值的副本。

    • cell.set(value):把 value 值保存到 cell 中,并丢弃之前保存的值。

      • set 方法的 self 参数,以非 mut 引用形式传入:

        fn set(&self, value: T)   // 不是&mut self
        
  • RefCell<T> 是一个包含 T 类型的泛型类型。

    • RefCell::new(value):创建一个新的 RefCell 类型的变量,将 value 值转移到其中。
    • ref_cell.borrow():返回一个 Ref<T>,基本上是对 ref_cell 中值的共享引用。
    • ref_cell.borrow_mut():返回一个 Ref<T>,基本上是对 ref_cell 中值的可修改引用。
  • Cell<T>RefCell<T> 的特点:

    • RefCell 支持借用它的 T 类型的引用,而 Cell<T> 不支持。
    • 它们都不是线程安全的,不允许同时使用多个线程访问它们。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

phial03

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值