结构

很久以前,当牧羊人想看看两群羊是否同构时,他们会寻找一种明显的同构. —John C. Baez and James Dolan,“Categorification”

原文

Long ago, when shepherds wanted to see if two herds of sheep were isomorphic, they would look for an explicit isomorphism.

Rust结构,(有时称为 结构体(structures) )类似于C和C++中的struct类型,Python中的类和JavaScript中的对象.结构将多个类型的多个值组合成单个值,因此你可以将它们作为一个单元来处理.给定结构,你可以读取和修改其各个组件.并且结构可以具有与其相关联的方法,其对其组件进行操作.

Rust有三种结构类型, 命名字段(named-field) ,元组风格(tuple-like)单元风格(unit-like) ,它们在如何引用它们的组件方面有所不同:命名字段结构为每个组件提供一个名称,而元组风格的结构标识它们按照它们出现的顺序排列.单元式结构根本没有任何组件;这并不常见,但比你想象的更有用.

在本章中,我们将详细解释每种类型,并显示它们在内存中的样子.我们将介绍如何向它们添加方法,如何定义适用于许多不同组件类型的通用(泛型)结构类型,以及如何让Rust生成结构的常用方便trait的实现.

命名字段结构(Named-Field Structs)

命名字段结构类型的定义如下所示:

  1. /// A rectangle of eight-bit grayscale pixels.
  2. struct GrayscaleMap {
  3. pixels: Vec<u8>,
  4. size: (usize, usize)
  5. }

这声明了一个类型GrayscaleMap,具有给定类型的两个字段,名为pixelssize.Rust中的约定适用于所有类型(包括结构),其名称大写每个单词的第一个字母,例如GrayscaleMap,一种名为 CamelCase 的约定.字段和方法是小写的,单词用下划线分隔.这叫做 snake_case .

你可以使用 结构表达式(struct expression) 构造此类型的值,如下所示:

  1. let width = 1024;
  2. let height = 576;
  3. let image = GrayscaleMap {
  4. pixels: vec![0; width * height],
  5. size: (width, height)
  6. };

结构表达式以类型名称(GrayscaleMap)开头,并列出每个字段的名称和值,全部用大括号括起来.还有用同名的局部变量或参数填充字段的简写:

  1. fn new_map(size: (usize, usize), pixels: Vec<u8>) -> GrayscaleMap {
  2. assert_eq!(pixels.len(), size.0 * size.1);
  3. GrayscaleMap { pixels, size }
  4. }

结构表达式GrayscaleMap { pixels, size }GrayscaleMap { pixels: pixels, size: size }的简写.你可以在同一结构表达式中对某些字段使用key:value语法,对其他的字段使用简写.

要访问结构的字段,请使用熟悉的.操作符:

  1. assert_eq!(image.size, (1024, 576));
  2. assert_eq!(image.pixels.len(), 1024 * 576);

与所有其他项一样,默认情况下,结构是私有的,仅在声明它们的模块中可见.你可以通过在其定义前添加pub来使结构在其模块外部可见.每个字段同样如此,默认情况下它们也是私有的:

  1. /// A rectangle of eight-bit grayscale pixels.
  2. pub struct GrayscaleMap {
  3. pub pixels: Vec<u8>,
  4. pub size: (usize, usize)
  5. }

即使结构声明为pub,其字段也可以是私有的:

  1. /// A rectangle of eight-bit grayscale pixels.
  2. pub struct GrayscaleMap {
  3. pixels: Vec<u8>,
  4. size: (usize, usize)
  5. }

其他模块可以使用此结构及其可能具有的任何公有方法,但无法通过名称访问私有字段或使用结构表达式来创建新的GrayscaleMap值.也就是说,创建结构值需要所有结构的字段都可见.这就是为什么你不能编写结构表达式来创建一个新的StringVec.这些标准类型是结构,但它们的所有字段都是私有的.要创建一个(这种类型的值),你必须使用Vec::new()等公有方法.

创建命名字段结构值时,可以使用相同类型的另一个结构为你省略的字段提供值.在结构表达式中,如果命名字段后面跟着.. EXPR,那么任何未提及的字段都会从EXPR中获取它们的值,EXPR必须是相同结构类型的另一个值.假设我们在游戏中有一个代表怪物的结构:

  1. struct Broom {
  2. name: String,
  3. height: u32,
  4. health: u32,
  5. position: (f32, f32, f32),
  6. intent: BroomIntent
  7. }
  8. /// Two possible alternatives for what a `Broom` could be working on.
  9. #[derive(Copy, Clone)]
  10. enum BroomIntent { FetchWater, DumpWater }

对程序员来说,最好的童话故事是 巫师的学徒(The Sorcerer’s Apprentice) :一个新手魔术师附魔扫帚为他工作,但不知道如何在工作完成后停止它.用斧头将扫帚劈成两半只会产生两个扫帚,每个扫帚的大小都是原来的一半,但是继续这项任务,与原来一样盲目奉献:

  1. // Receive the input Broom by value, taking ownership.
  2. fn chop(b: Broom) -> (Broom, Broom) {
  3. // Initialize `broom1` mostly from `b`, changing only `height`. Since
  4. // `String` is not `Copy`, `broom1` takes ownership of `b`'s name.
  5. let mut broom1 = Broom { height: b.height / 2, .. b };
  6. // Initialize `broom2` mostly from `broom1`. Since `String` is not
  7. // `Copy`, we must clone `name` explicitly.
  8. let mut broom2 = Broom { name: broom1.name.clone(), .. broom1 };
  9. // Give each fragment a distinct name.
  10. broom1.name.push_str(" I");
  11. broom2.name.push_str(" II");
  12. (broom1, broom2)
  13. }

有了这个定义,我们可以创建一把扫帚,将它切成两半,看看我们得到了什么:

  1. let hokey = Broom {
  2. name: "Hokey".to_string(),
  3. height: 60,
  4. health: 100,
  5. position: (100.0, 200.0, 0.0),
  6. intent: BroomIntent::FetchWater
  7. };
  8. let (hokey1, hokey2) = chop(hokey);
  9. assert_eq!(hokey1.name, "Hokey I");
  10. assert_eq!(hokey1.health, 100);
  11. assert_eq!(hokey2.name, "Hokey II");
  12. assert_eq!(hokey2.health, 100);

元组风格结构(Tuple-Like Structs)

第二种结构类型称为 元组风格结构 ,因为它类似于元组:

  1. struct Bounds(usize, usize);

你构造一个此类型的值,就像构造元组一样,除了必须包含结构名称:

  1. let image_bounds = Bounds(1024, 768);

元组风格结构所保存的值称为 元素(elements) ,就像元组的值一样.你可以像访问元组一样访问它们:

  1. assert_eq!(image_bounds.0 * image_bounds.1, 786432);

元组风格结构的各个元素可以是公有的或非公有的:

  1. pub struct Bounds(pub usize, pub usize);

表达式`Bounds(1024, 768)看起来像一个函数调用,实际上它是:定义类型也隐式定义一个函数:

  1. fn Bounds(elem0: usize, elem1: usize) -> Bounds { ... }

在最基本的层面上,命名字段结构和元组风格结构非常相似.选择使用哪个归结为易读性,模糊性和简洁性的问题.如果你将使用.运算符可以很大程度地获取值的组件,按名称标识字段可为读者提供更多信息,并且可能更能抵御错别字.如果你通常会使用模式匹配来查找元素,那么元组风格结构可以很好地工作.

元组风格结构适用于 新类型(newtypes) ,具有单个组件的结构,你可以定义这些结构以进行更严格的类型检查.例如,如果你使用的是仅ASCII文本,则可以定义如下的新类型:

  1. struct Ascii(Vec<u8>);

将此类型用于ASCII字符串要比简单地传递Vec<u8>缓冲区并在注释中解释它们的含义要好得多.新类型帮助Rust捕获一些其他字节缓冲区被传递给期望ASCII文本的函数产生的错误.我们将在第21章中给出一个使用新类型进行高效类型转换的示例.

单元式结构(Unit-Like Structs)

第三种结构有点晦涩:它声明了一个完全没有元素的结构类型:

  1. struct Onesuch;

这种类型的值不占用内存,很像单元类型().Rust实际上并不累赘地在内存中存储单元式结构值或生成代码来操作它们,因为它可以仅从其类型得知它可能需要了解的关于值的所有信息.但从逻辑上讲,空结构是一种类型,其值与其他类型的值一样—或者更准确地说,这种类型只有一个值:

  1. let o = Onesuch;

在阅读第135页的”字段和元素(Fields and Elements)”时,你已经遇到了单元式结构.而类似于3..5的表达式是结构值Range { start: 3, end: 5 }的简写,而表达式..,省略两个端点的范围,是单元式结构值RangeFull的简写.

在处理traits时,单元式结构也很有用,我们将在第11章中对其进行描述.

结构布局(Struct Layout)

在内存中,命名字段结构和元组风格结构都是相同的东西:一组值,可能是混合类型,在内存中以特定方式布局.例如,在本章的前面我们定义了这个结构:

  1. struct GrayscaleMap {
  2. pixels: Vec<u8>,
  3. size: (usize, usize)
  4. }

GrayscaleMap值在内存中布局,如图9-1所示.

图9-1. 内存中的GrayscaleMap结构.

与C和C++不同,Rust没有就如何在内存中对结构的字段或元素进行排序做出具体的承诺;该图仅显示了一种可能的安排.但是,Rust确实将字段的值直接存储在struct的内存块中.虽然JavaScript,Python和Java会将pixelssize值分别放在它们自己的堆分配块中,并且GrayscaleMap的字段指向它们,但Rust会将pixelssize直接嵌入到GrayscaleMap值中. 只有pixels向量所拥有的堆分配缓冲区仍保留在自己的块中.

你可以使用#[repr(C)]属性要求Rust以与C和C++兼容的方式布局结构.我们将在第21章详细介绍这一点.

使用impl定义方法(Defining Methods with impl)

在整本书中,我们一直在调用各种值上的方法.我们已经使用v.push(e)将元素推送到向量上,使用v.len()获取其长度,使用r.expect("msg")检查Result值以查找错误,等等.

你可以在你定义的任何结构类型上定义方法.Rust方法出现在单独的impl块中,而不是像在C++或Java中那样出现在结构定义中.例如:

  1. /// A last-in, first-out queue of characters.
  2. pub struct Queue {
  3. older: Vec<char>, // older elements, eldest last.
  4. younger: Vec<char> // younger elements, youngest last.
  5. }
  6. impl Queue {
  7. /// Push a character onto the back of a queue.
  8. pub fn push(&mut self, c: char) {
  9. self.younger.push(c);
  10. }
  11. /// Pop a character off the front of a queue. Return `Some(c)` if there
  12. /// was a character to pop, or `None` if the queue was empty.
  13. pub fn pop(&mut self) -> Option<char> {
  14. if self.older.is_empty() {
  15. if self.younger.is_empty() {
  16. return None;
  17. }
  18. // Bring the elements in younger over to older, and put them in
  19. // the promised order.
  20. use std::mem::swap;
  21. swap(&mut self.older, &mut self.younger);
  22. self.older.reverse();
  23. }
  24. // Now older is guaranteed to have something. Vec's pop method
  25. // already returns an Option, so we're set.
  26. self.older.pop()
  27. }
  28. }

impl块只是fn定义的集合,每个定义都成为块顶部命名的结构类型的方法.在这里,我们定义了一个公有的结构Queue,然后给它两个公有方法,pushpop.

方法也称为 关联函数(associated functions) ,因为它们与特定类型相关联.与关联函数相反的是 自由函数(free function) ,它未定义为impl块的项.

Rust将在其上调用方法的值传递给方法,作为方法的第一个参数,该参数必须具有特殊名称self.因为self的类型显然是在impl块的顶部命名的,或者是对它的引用,Rust允许你省略类型,编写self,&self或者&mut self作为self: Queue,self: &Queue或者self: &mut Queue的简写.如果你愿意,可以使用普通书写形式,但几乎所有Rust代码都使用简写,如前所示.

在我们的示例中,pushpop方法将Queue的字段称为self.olderself.younger.与C++和Java不同(“this”对象的成员在方法体中作为非限定标识符直接可见),Rust方法必须显式使用self来引用它所调用的值,类似于Python方法使用self的方式,以及JavaScript方法使用this方式.

由于pushpop需要修改Queue,因此它们都接收&mut self.但是,当你调用方法时,你不需要自己借用可变引用;普通方法调用语法隐式处理.因此,有了这些定义,你可以像这样使用Queue:

  1. let mut q = Queue { older: Vec::new(), younger: Vec::new() };
  2. q.push('0');
  3. q.push('1');
  4. assert_eq!(q.pop(), Some('0'));
  5. q.push('∞');
  6. assert_eq!(q.pop(), Some('1'));
  7. assert_eq!(q.pop(), Some('∞'));
  8. assert_eq!(q.pop(), None);

简单地写q.push(...)借用q的可变引用,就好像你写了(&mut q).push(...),因为这就是push方法的self需要的.

如果方法不需要修改其self,则可以将其定义为接受共享引用.例如:

  1. impl Queue {
  2. pub fn is_empty(&self) -> bool {
  3. self.older.is_empty() && self.younger.is_empty()
  4. }
  5. }

同样,方法调用表达式知道要借用哪种引用:

  1. assert!(q.is_empty());
  2. q.push('☉');
  3. assert!(!q.is_empty());

或者,如果一个方法想要取得self的所有权,它可以通过值来接受self:

  1. impl Queue {
  2. pub fn split(self) -> (Vec<char>, Vec<char>) {
  3. (self.older, self.younger)
  4. }
  5. }

调用此split方法看起来像其他方法调用(一样):

  1. let mut q = Queue { older: Vec::new(), younger: Vec::new() };
  2. q.push('P');
  3. q.push('D');
  4. assert_eq!(q.pop(), Some('P'));
  5. q.push('X');
  6. let (older, younger) = q.split();
  7. // q is now uninitialized.
  8. assert_eq!(older, vec!['D']);
  9. assert_eq!(younger, vec!['X']);

但请注意,由于split通过值接受其self,因此将Queue 移出(moves) q,使q保持未初始化.由于splitself现在拥有队列,它可以将各个向量移出它,并将它们返回给调用者.

你还可以定义不接受self作为参数的方法.这些成为与结构类型本身相关联的函数,而不是与该类型的任何特定值相关联.遵循C++和Java建立的传统,Rust称这些为 静态方法(static methods) .它们经常用于提供构造函数,如下所示:

  1. impl Queue {
  2. pub fn new() -> Queue {
  3. Queue { older: Vec::new(), younger: Vec::new() }
  4. }
  5. }

要使用此方法,我们这样引用它:Queue::new:类型名称,双冒号,然后是方法名称.现在我们的示例代码变得更加苗条:

  1. let mut q = Queue::new();
  2. q.push('*');
  3. ...

Rust中的构造函数通常被命名为new;我们已经看过Vec::new,Box::new,HashMap::new等等.但new名称并没有什么特别之处.它不是关键字,类型经常有其他静态方法作为构造器,如Vec::with_capacity.

虽然你可以为单个类型提供许多单独的impl块,但它们必须位于定义该类型的相同crate中.然而,Rust确实允许你将自己的方法附加到其他类型;我们将在第11章解释.

如果你习惯使用C++或Java,那么将类型的方法与其定义分开可能看起来很不寻常,但这样做有几个好处:

  • 总是很容易找到类型的数据成员.在大型C++类定义中,你可能需要浏览数百行成员函数定义,以确保你没有错过类的任何数据成员;在Rust中,他们都在一个地方.

  • 尽管人们可以想象将方法拟合到命名字段结构的语法中,但它对于元组风格结构和单元式结构并不那么简洁.将方法拉出到impl块允许以上三种结构有一个单个语法.实际上,Rust使用相同的语法来定义非结构类型的方法,例如enum类型和基本类型(如i32).(任何类型都可以拥有方法的事实是Rust不使用术语 对象(object) 的一个原因,更喜欢将所有内容称为 值(value) .).

  • 相同的impl语法也可以很好地实现traits,我们将在第11章中介绍.

泛型结构(Generic Structs)

我们之前对Queue的定义并不令人满意:它是为了存储字符而编写的,但它的结构体或方法根本不特定于字符. 如果我们要定义另一个包含String值的结构,那么代码可以是相同的,除了char将被String替换.那将浪费时间.

幸运的是,Rust结构可以是 泛型的(generic) ,这意味着它们的定义是一个模板,你可以在其中插入你喜欢的任何类型.例如,这里是Queue的定义,可以保存任何类型的值:

  1. pub struct Queue<T> {
  2. older: Vec<T>,
  3. younger: Vec<T>
  4. }

你可以将Queue<T>中的<T>读作”对于任何元素类型T…(for any element type T…)”.所以这个定义读作:”对于任何类型T,Queue<T>Vec<T>类型的两个字段(For any type T,a Queue<T> is two fields of type Vec<T>).”例如,在Queue<String>中,TString,所以olderyounger的类型为Vec<String>.在Queue<char>中,Tchar,我们得到一个与我们开始时的char特定定义相同的结构.事实上,Vec本身就是一个以这种方式定义的泛型结构.

在泛型结构定义中,<尖括号>(<angle brackets>)中使用的类型名称称为 类型参数(type parameters) .泛型结构的impl块如下所示:

  1. impl<T> Queue<T> {
  2. pub fn new() -> Queue<T> {
  3. Queue { older: Vec::new(), younger: Vec::new() }
  4. }
  5. pub fn push(&mut self, t: T) {
  6. self.younger.push(t);
  7. }
  8. pub fn is_empty(&self) -> bool {
  9. self.older.is_empty() && self.younger.is_empty()
  10. }
  11. ...
  12. }

你可以读行impl<T> Queue<T>,类似于”对于任何类型T,这里有一些Queue<T>上可用的方法(for any type T, here are some methods available on Queue<T>).”然后,你可以使用类型参数T作为方法中的类型定义.

我们在前面的代码中使用了Rust的self参数简写;到处都写出Queue<T>会变得冗长和分心.作为另一种简写,每个impl块,无论是否泛型,都将特殊类型参数Self(注意CamelCase名称)定义为我们要添加方法的任何类型.在前面的代码中,Self将是Queue<T>,因此我们可以进一步缩写Queue::new的定义:

  1. pub fn new() -> Self {
  2. Queue { older: Vec::new(), younger: Vec::new() }
  3. }

你可能已经注意到,在new的主体中,我们不需要在构造表达式中编写类型参数;简单地写Queue {...}就足够了.这是Rust的类型推断在工作:因为只有一种类型适用于该函数的返回值—即Queue<T>—Rust为我们提供参数.但是,你始终需要在函数签名和类型定义中提供类型参数.Rust不推断那些;相反,它使用那些显式类型作为它在函数体内推断类型的基础.

对于静态方法调用,可以使用turbo-fish::<>表示法显式提供类型参数:

  1. let mut q = Queue::<char>::new();

但在实践中,你通常可以让Rust为你解决:

  1. let mut q = Queue::new();
  2. let mut r = Queue::new();
  3. q.push("CAD"); // apparently a Queue<&'static str>
  4. r.push(0.74); // apparently a Queue<f64>
  5. q.push("BTC"); // Bitcoins per USD, 2017-5
  6. r.push(2737.7); // Rust fails to detect irrational exuberance

实际上,这正是我们在整本书中使用Vec(另一种泛型结构类型)所做的事情.不仅仅结构可以是泛型的.枚举也可以采用类型参数,语法非常相似.我们将在第212页的”枚举(Enums)”中详细说明.

具有生命周期参数的结构(Structs with Lifetime Parameters)

正如我们在第109页的”包含引用的结构(Structs Containing References)”中所讨论的,如果结构类型包含引用,则必须命名这些引用的生命周期.例如,这里的结构体可能包含对某个切片的最大和最小元素的引用:

  1. struct Extrema<'elt> {
  2. greatest: &'elt i32,
  3. least: &'elt i32
  4. }

之前,我们邀请您考虑像struct Queue<T>这样的声明,意思是,给定任何特定类型T,你可以创建一个包含该类型的Queue<T>.类似地,你可以将struct Extrema <'elt>视为意味着,给定任何特定的生命周期'elt,你可以创建一个包含具有该生命周期的引用的Extrema <'elt>.

这是一个扫描切片并返回Extrema值(其字段引用切片元素)的函数:

  1. fn find_extrema<'s>(slice: &'s [i32]) -> Extrema<'s> {
  2. let mut greatest = &slice[0];
  3. let mut least = &slice[0];
  4. for i in 1..slice.len() {
  5. if slice[i] < *least { least = &slice[i]; }
  6. if slice[i] > *greatest { greatest = &slice[i]; }
  7. }
  8. Extrema { greatest, least }
  9. }

这里,由于find_extrema借用了具有生命周期'sslice的元素,我们返回的Extrema结构也使用's作为其引用的生命周期.Rust总是为调用推断生命周期参数,因此对find_extrema的调用不需要提及:

  1. let a = [0, -3, 0, 15, 48];
  2. let e = find_extrema(&a);
  3. assert_eq!(*e.least, -3);
  4. assert_eq!(*e.greatest, 48);

因为返回类型使用与参数相同的生命周期是如此常见,所以Rust允许我们在有一个明显的候选者时省略生命周期.我们也可以这样写find_extrema的签名,意思没有改变:

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

当然,我们 可能(might) 意味着Extrema<'static>,但这很不寻常.Rust提供常见情况的简写.

派生结构类型的常见Traits(Deriving Common Traits for Struct Types)

结构很容易写:

  1. struct Point {
  2. x: f64,
  3. y: f64
  4. }

但是,如果你开始使用此Point类型,你会很快发现它有点痛苦.如上所述,Point不可复制或可克隆.你不能用println!("{:?}", point);打印它,它不支持==!=运算符.

这些功能中的每一个在Rust中都有一个名称—Copy,Clone,DebugPartialEq.它们被称为 traits .在第11章中,我们将展示如何为你自己的结构手动实现traits.但是在这些标准traits(和其他几个traits)的情况下,除非你需要某种自定义行为,否则不需要手动实现它们.Rust可以自动地为你实现它们,具有机械精度.只需在结构中添加#[derive]属性:

  1. #[derive(Copy, Clone, Debug, PartialEq)]
  2. struct Point {
  3. x: f64,
  4. y: f64
  5. }

如果每个字段都实现了trait,那么这些trait中的每一个都可以自动为结构实现.我们可以要求Rust为Point派生PartialEq,因为它的两个字段都是f64类型,它已经实现了PartialEq.

Rust还可以派生PartialCmp,这将增加对比较运算符<,>,<=>=的支持.我们在这里没有这样做,因为比较两点以确定一个是否”小于”另一个实际上是一个非常奇怪的事情.点上没有一个传统的顺序.因此,我们选择不为Point值支持这些运算符.像这样的情况是Rust让我们编写#[derive]属性而不是自动派生每个trait的一个原因.另一个原因是实现trait自动是一个公有功能,因此可复制性,可克隆性等都是结构的公有API的一部分,应该刻意选择.

我们将在第13章中详细描述Rust的标准trait,并说明哪些是可#[derive]的.

内部可变性(Interior Mutability)

可变性就像其他任何东西:过量,它会导致问题,但你经常只需要一点点.例如,假设你的蜘蛛机器人控制系统有一个中心结构SpiderRobot,它包含设置和I/O句柄.它在机器人启动时设置,值永远不会改变:

  1. pub structSpiderRobot {
  2. species: String,
  3. web_enabled: bool,
  4. leg_devices: [fd::FileDesc; 8],
  5. ...
  6. }

机器人的每个主要系统都由不同的结构处理,每个结构都有一个指针返回到SpiderRobot:

  1. use std::rc::Rc;
  2. pub struct SpiderSenses {
  3. robot: Rc<SpiderRobot>, // <-- pointer to settings and I/O
  4. eyes: [Camera; 32],
  5. motion: Accelerometer,
  6. ...
  7. }

用于网构造,捕食,毒液流控制等的结构也各自具有Rc<SpiderRobot>智能指针.回想一下,Rc代表*引用计数,并且Rcbox中的值总是共享的,因此总是不可变的.

现在假设你要使用标准File类型向SpiderRobot结构添加一些日志记录.有一个问题:File必须是mut.写入它的所有方法都需要mut引用.

这种情况经常出现.我们需要的是一个不可变的值(SpiderRobot结构)中的一些可变数据(一个File).这被称为 内部可变性(interior mutability) .Rust提供了几种风格;在本节中,我们将讨论两种最直接的类型:Cell<T>RefCell<T>,两者都在std::cell模块中.

Cell<T>是一个包含单个T类型的私有值的结构.关于Cell的唯一特殊之处在于,即使你没有对Cell本身进行mut访问,也可以获取并设置该字段:

  • Cell::new(value))创建一个新的Cell,将给定的value移入其中.

  • cell.get()返回cell中值的一个副本.

  • cell.set(value)将给定value存储在cell中,删除先前存储的值.

此方法接受self作为非mut引用:

  1. fn set(&self, value: T) // note: not `&mut self`

当然,对于名为set的方法,这是不常见的.到目前为止,如果我们想要对数据进行更改,Rust已经培训我们期望我们需要mut访问.但出于同样的原因,这一个不寻常的细节是Cell的重点.它们只是一种安全的方式来弯曲不可变性的规则—不多也不少.

Cell还有一些其他方法,你可以在文档中阅读.

如果你在SpiderRobot上添加一个简单的计数器,那么Cell会很方便.你可以写:

  1. use std::cell::Cell;
  2. pub struct SpiderRobot {
  3. ...
  4. hardware_error_count: Cell<u32>,
  5. ...
  6. }

而后即使SpiderRobot的非mut方法都可以使用.get().set()方法访问u32:

  1. impl SpiderRobot {
  2. /// Increase the error count by 1.
  3. pub fn add_hardware_error(&self) {
  4. let n = self.hardware_error_count.get();
  5. self.hardware_error_count.set(n + 1);
  6. }
  7. /// True if any hardware errors have been reported.
  8. pub fn has_hardware_errors(&self) -> bool {
  9. self.hardware_error_count.get() > 0
  10. }
  11. }

这很容易,但它不能解决我们的日志记录问题.Cell 不(not) 允许你在共享值上调用mut方法..get()方法返回cell中值的副本,因此仅当T实现Copy trait时它才有效.对于日志记录,我们需要一个可变File,而File不可复制.

在这种情况下,正确的工具是RefCell.与Cell<T>类似,RefCell<T>是包含单个T类型值的泛型类型.与Cell不同,RefCell支持借用其T值的引用:

  • RefCell::new(value))创建一个新的RefCell,将value移入其中.

  • ref_cell.borrow()返回一个Ref<T>,它实际上只是对ref_cell中存储的值的共享引用.

如果值已被可变借用,这种方法会引起恐慌;看详细信息如下.

  • ref_cell.borrow_mut()返回RefMut<T>,本质上是ref_cell中值的可变引用.

如果已借用该值,则此方法会发生恐慌;看详细信息如下.

同样,RefCell还有一些其他方法,您可以在在文档中找到它们.

只有当你试图打破mut规则,即mut引用是独占引用时,这两种borrow方法才会出现恐慌.例如,这会引起恐慌:

  1. let ref_cell: RefCell<String> = RefCell::new("hello".to_string());
  2. let r = ref_cell.borrow(); // ok, returns a Ref<String>
  3. let count = r.len(); // ok, returns "hello".len()
  4. assert_eq!(count, 5);
  5. let mut w = ref_cell.borrow_mut();
  6. // panic: already borrowed
  7. w.push_str(" world");

为避免恐慌,你可以将这两个借用放入单独的块中.这样,在你尝试借用w之前,r将被丢弃.

这很像普通引用的工作方式.唯一的区别是通常,当你借用对变量的引用时,Rust会 在编译时(at compile time) 检查以确保你安全地使用该引用.如果检查失败,则会出现编译器错误.RefCell使用运行时检查强制执行相同的规则.因此,如果你违反规则,你会得到恐慌.

现在我们准备将RefCell用于我们的SpiderRobot类型:

  1. pub struct SpiderRobot {
  2. ...
  3. log_file: RefCell<File>,
  4. ...
  5. }
  6. impl SpiderRobot {
  7. /// Write a line to the log file.
  8. pub fn log(&self, message: &str) {
  9. let mut file = self.log_file.borrow_mut();
  10. writeln!(file, "{}", message).unwrap();
  11. }
  12. }

变量file的类型为RefMut<File>.它可以像File的可变引用一样使用.有关写入文件的详细信息,请参阅第18章.

Cell易于使用.必须调用.get().set().borrow().borrow_mut()有点尴尬,但这只是我们为弯曲规则而付出的代价.另一个缺点不太明显,更严重:cell—任何包含它们的类型—都不是线程安全的.因此,Rust不允许多个线程一次访问它们.我们将在第19章中描述内部可变性的线程安全风格,在第486页讨论”Mutex<T>“,第494页讨论”Atomics”和第496页的”全局变量(Global Variables)”.

无论结构是命名字段还是元组风格,它都是其他值的聚合:如果我有一个SpiderSenses结构,那么我有一个指向共享的SpiderRobot结构的Rc指针,我有眼睛,我有一个加速度计,等等.所以结构的本质是”和(and)”这个词:我有一个X 和(and) 一个Y.但是如果在”或(or)”这个词周围建立了另一种类型呢?也就是说,当你有这种类型的值时,你有一个X 或(either…or) 一个Y?这些类型变得如此有用,以至于它们在Rust中无处不在,它们是下一章的主题.