[The RUST Programming Language]Chapter 5. Using Structs to Structure Related Data

结构是一种自定义的数据类型,它能够打包多个相关的数据并命名,使得它们组成一个有意义的组。如果你非常熟悉OO语言,Rust中的结构其实就像一个对象的数据属性。在本章中,我们会比较元组和结构,并示范如何使用结构,讨论如何为这些结构定义相关的方法和关联函数。结构和枚举(第六章)是在程序域中构建新数据类型的基石,它们在Rust编译时会进行类型检查,这也是Rust的优势之一。

Defining and Instantiating Structs 定义并实例化结构

结构和我们第三章中说过的元组很相似,和元组一样,结构的每一部分可以是不同类型的数据。但又有一点不同,你可以在结构中为每一个数据命名,数据的意义会相当明了。同时,由于数据都有自己的名字,结构使用起来会比元组更加灵活:你不须要依赖数据的排序去指定或访问一个实例的值。

在定义结构时,我们使用关键字struct给整个结构命名,一个结构的名字理应能够体现出组成结构的数据的意义。然后在尖括号的代码块中,我们定义并命名了结构中的每一个数据和其对应的类型,这些我们称之为fields 字段。下面就是一个用于存储用户账号信息的结构例子:

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

想要使用我们定义的结构,我们必须为这个结构创建一个实例,指定结构中每一个字段的值。我们通过直接调用结构名来创建结构的实例,并在尖括号中使用一系列形似key: value这样的键值对为结构中的每一个字段分配我们想让它们存储的值,且我们不需要严格按照结构定义中的顺序来为字段赋值。一句话,结构定义其实就像一个类型的普通模板,而实例就是往模板的每个字段中填入符合定义中类型要求的明确的数据。下面的例子就是创建一个具体用户实例:

let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

获得结构中一个具体的值,我们可以使用点表示法。如果我们只想获得用户的邮箱地址,我们可以使用user1.email。更进一步,如果实例是可修改的,我们也可以通过点表示法,通过为字段指定一个新的值来修改它。下面就是一个修改结构实例中字段值的例子:

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

记住,上面的示例中,整个实例必须是可修改的,Rust不允许我们仅仅对结构中局部几个字段标记为可更改。就像其它表达式一样,我们可以将一个结构的实例放在函数体的最后,这样函数就能返回这个结构实例。

下面的例子中,build_function函数返回了一个User实例,它包含有我们传入的邮箱和用户名。结构中的active字段,我们缺省给了值truesign_in_count字段,缺省给了值1

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

函数的形参名与结构的字段名可以是一样的,但重复输入emailusername有一丢丢烦,如果结构包含更多的字段,重复写每一个名字是很麻烦的。幸运的是,这里提供了一个舒服的速记法。

Using the Field Init Shorthand when Variables and Fields Have the Same Name 当变量和字段名字相同时,使用字段速记初始化

在上面的例子中,因为形参和结构中的字段名是一样的,我们可以使用字段速记初始化语法来重写build_user函数,它的功能没有发生变化,但我们不必再在实例化结构时重复emailusername了:

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

这样,我们就创建了一个新的User结构实例。它的字段email的值来自build_user函数的email形参,这里我们只要使用email就行了,而不再需要email: email

Creating Instances From Other Instances With Struct Update Syntax 使用结构更新语法,通过其它实例来创建结构实例

更改一个旧实例中某些字段的值来创建一个新实例,通常是非常有用的。我们可以通过结构更新语法来实现这个功能。

在下面的例子中,我们没有通过结构更新语法创建了一个User实例user2。我们为emailusername设置了新的值,但另一些字段,我们沿用了user1实例中字段的值:

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

下面的例子,通过结构更新语法,使用更少的代码完成了同样的效果。语句 .. 指定了结构中其余的字段应与另一个实例中的字段值保持一致:

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

上面的例子中,我们同样创建了一个实例user2,它的字段emailusername有不同的值,但activesign_in_count字段的值来自于user1

Using Tuple Structs without Named Fields to Create Different Types 使用不包含字段名的元组结构来创建不同的类型

其实我们也能用类似元组的方式定义结构,这类结构被称作tuple structs元组结构。元组结构添加了有意义的结构名,但并不为它里面的字段命名,仅仅罗列了字段的数据类型。当你想让某个元组区别于其它元组时,使用元祖结构会非常有用,如果使用普通的结构,为每个字段是非常冗余的。

定义元祖结构,也是使用关键字struct并紧跟结构名,结构名后就是包含具体数据类型的元组了。下面的例子,我们定义并实例化了两个元组结构ColorPoint

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

留意下,blackorigin的值是不同的数据类型,因为这两个实例是来自不同的元组结构,尽管这两个结构中的字段都有一样的数据类型。举例来说,一个函数的形参是Color类型,那么它不能使用Point作为实参,尽管这两个类型都是由三个i32值组成。此外,元组结构的行为也很像元组,你能将它解构为多个独立的部分,你也能使用.紧跟索引值来访问单独的某个值。

Unit-Like Structs Without Any Fields 不包含任何字段的类单元结构

我们也可以定义不包含任何字段的结构,这类结构被称作unit-like structs 类单元结构,因为它们的行为模式和 () 元数据类型很像。类单元结构在很多场景下都非常有用,譬如你须要为某个数据类型实现一个特性,但你又不想在这个类型中存储任何数据。关于特性的知识,我们会在第十章中讨论。

Ownership of Struct Data
在我们一开始定义的User结构中,我们使用了String类型而非&str字符串切片。这是一个经过深思熟虑的选择,因为我们希望这个实例包含的数据的有效期能够和实例本身一致。

结构也能够存储一些其它数据对象的引用,但这么做须要用到生命周期,这是Rust的一个特点,我们也会在第十章中再去讨论。生命周期可以确保被结构引用的数据在结构实例生效时也始终生效。你也可以照下面的代码尝试在结构中包含引用,但不指定声明周期,这段代码是无法工作的:

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

编译器会在编译时抱怨,它须要指定一个生命周期:

error[E0106]: missing lifetime specifier
 -->
  |
2 |     username: &str,
  |               ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
 -->
  |
3 |     email: &str,
  |            ^ expected lifetime parameter

在第十章中,我们会讨论如何修复这些错误来让你在结构中使用引用,但现在,请先通过使用String而非&str来修复这个错误先。

An Example Program Using Structs 一个使用结构的示例程序

为了更好地理解我们什么时候可能要用到结构,让我们来写个程序计算长方形的面积。我们会先通过单个变量做起,最后使用结构来重写程序。

我们先通过Cargo创建一个新的二进制项目叫做rectangles,它会获得一个长方形的长高,并计算长方形的面积:

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

现在,让我们用cargo run命令来运行它:

The area of the rectangle is 1500 square pixels.

从结果上来看,我们的确通过area函数正确的获得了每个长方形的面积,但是这个程序还是有很大的改进空间。首先一个长方形的长和高是一对现实中有关联的数据,但来看下area函数的函数签名:

fn area(width: u32, height: u32) -> u32 {

尽管area函数能够计算长方形的面积,但是却将长高拆分成了两个形参,这样我们就无法直观的在程序中表达出二者的关联。如果能将长高合理的组合起来,这样就能提高程序的可读性。在第三章中,我们讨论过了一种方法,即使用元组。

Refactoring with Tuples 通过元组来重构

现在让我们使用元组来重构下我们的程序:

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

我们的程序看起来更好了,是不?元组使我们的程序结构更加合理,这次我们也只需要传递一个参数就行了。但这个新版本的程序也有一点问题,元组中各元素的意义并不清楚,因为元组无法对它的元素命名,在使用area函数时,你会感到很困惑,因为你须要通过索引去访问参数里面的元素。

Refactoring with Structs: Adding More Meaning 通过结构去重构,让程序有那味

在结构中,我们可以通过为字段设置标签的方式来赋予它具体的含义,所以我们不妨将程序中的元组替换为结构:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

上面的程序中,我们定义了一个结构叫做Rectangle,在尖括号中的结构体里,我们定义了两个字段widthheight,两个字段都是u32类型。在main函数中,我们创建了一个具体的Rectangle实例,它的长是30,高是50。

我们的area函数现在就只需要定义一个形参rectangle就够了,它的类型是Rectangle实例的不可修改借用。回忆下第4章的知识,我们只想借用这个结构实例,而不须要获取它的所有权,这样main将保留所有权,继续使用rect1,这也是为什么我们在函数签名中使用了&

Adding Useful Functionality with Derived Traits 通过衍生特性为结构增加更多功能

当我们在debug程序时,如果能将Rectangle中各字段的值显示出来,那自然是极好的。所以在下面的例子中,我们使用了之前介绍过的println!这个宏来尝试输出长方形的内容:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {}", rect1);
}

然而这段代码在编译的时候,却返回了如下的错误信息:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

println!能够做许多格式化输出,参数中的尖括号对是用于告诉print宏该在哪里来格式化输出,这种行为也被称为Display:将输出结果以最终用户期望的格式展示。目前我们接触的很多基础数据类型都缺省实现了Display特性,如果你想把1或是其它基础数据类型展现给用户时,都只有很简单的一种样式,因为基础数据类型都是扁平的。但是对于结构,println!就不清楚该如何去Display了:你要不要逗号?需不需要打印尖括号?所有的字段都应该要显示么?对于这种模棱两可的问题,Rust不会去尝试猜测我们怎么样想的,所以结构并没有缺省提供Display的实现。

继续看这个错误消息,我们会找到以下有用的注释信息:

= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

好吧,让我试一试!我们通过println!("rect1 is {:?}", rect1);去调用宏println!,注意到了么,我们在尖括号中加入了:?,这会告诉println!我们想以Debug特性要求的格式去输出。Debug特性使得我们能够以开发者喜闻乐见的方式来显示结构,这样在debug我们的程序时,我们就可以看到结构中的字段值。

可是,当我们再度尝试编译。f**k!我们还是有报错:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Debug`

编译器再一次提供了有用的注释信息:

= help: the trait `std::fmt::Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`

Rust 并不 包含输出调试信息的功能,我们必须在我们的程序中明确的为我们的结构选择这个功能。我们须要在结构的定义前,标注上#[derive(Debug)]

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {:?}", rect1);
}

现在我们再运行程序,就不会有任何错误信息了,并且看到像下面这样的输出:

rect1 is Rectangle { width: 30, height: 50 }

奈斯!尽管这个输出并不漂亮,但它至少显示了结构实例中字段的值,这对于调试程序已有足够的帮助了。如果我们有一个更复杂的结构,这时可以通过使用{:#?}来替代println!参数中的{:?},这样显示的输出将更加方便阅读:

rect1 is Rectangle {
    width: 30,
    height: 50
}

Rust提供了一系列可以使用derive标记来使用的特性,它们能为我们的自定义数据类型提供很多有用的行为。这些特性和它们的行为,都在官方教程的附录C中有罗列。我们将会在第十章中介绍如何用自定义的行为来实现这些特性,也会告诉你如何创建一个你自己的特性。

现在我们的area函数的意义已经非常精确了:它是用来计算一个长方形的面积。如果能进一步将这个求面积的行为关联到我们的Rectangle结构上,那无疑是更好的选择,因为它不能用于其它的结构类型。下面就来介绍下,如何继续重构我们的代码,使得area函数变为我们Rectangle定义中的一个 method方法 吧。

Method Syntax 方法函数语法

方法函数和普通函数很像:它们都通过fn关键字加名字去定义;它们都可以有形参和返回值;它们都包含了一些代码,并且会在它们被调用时执行。方法函数与函数也有不同的地方,方法函数是在结构(枚举、特性对象,这两个概念会分别在第六章和第十七章中介绍)的上下文中被定义,并且它的第一个形参总是selfself代表了调用这个方法函数的结构实例。

Defining Methods 定义方法

让我们修改我们的代码,用area方法函数来替换area函数:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

我们通过impl代码块,在Rectangle的上下文中定义了方法函数实现,并且将area方法函数塞了进去,同时area方法函数签名中的第一个形参是selfmain函数中,我们通过方法函数语法,让我们的Rectangle实例调用了area方法函数。在实例的方法函数语法中,我们在实例对象后添加了一个句点,之后是方法函数名和括号,当然你也可以跟上更多的参数。

area方法函数中,我们使用&self替代了rectangle: &Rectangle,Rust知道self的数据类型就是Rectangle,因为这个方法函数是在impl Rectangle上下文中。记住一点,我们仍然需要在self前加上&,就像我们之前使用&Rectangle一样。方法函数能够获得self的所有权,这里我们对self做了不可修改引用,当然依据参数不同,你也能够对self做可修改引用。

这里我们使用不可修改引用的原因和之前是一样的:我们不想获得所有权,我们只想读取结构的数据,而不涉及写入操作。如果我们想在方法函数中的某一部分中修改实例,可以使用&mut self作为我们第一个形参。在方法函数中直接使用self作为第一个形参来获得实例的所有权,这种情况是非常罕见的,一般这种情况是用于将self转换为其它东西,同时你又不希望原始的调用者在转换后能继续使用这个实例对象。

用方法函数而非普通函数的一大好处是,在方法函数中,你不须要重复在签名里标注self的数据类型。我们可以把实例相关的功能都写进一个impl代码块中,我们功能的用户将来就不须要再像使用普通函数时那样,在代码库中拼命去找哪里用到了Rectangle这个类型。

哪里有 -> 箭头操作符?
在C和C++中,你可以使用两种操作符来调用方法:一种即是.句点符,通过它你可以直接调用对象的方法;另一种是->箭头符,这种情况是用于你想调用指针指向的对象上的方法,这时你须要先取消引用。一句话,如果object是一个指针,object->something()(*object).something()是一样的。

Rust中没有与->相同的操作符,不过,Rust有一个功能叫做automatic referencing and dereferencing 自动引用与取消引用。调用方法函数是Rust种用到这个功能的地方之一。

这里是它工作的原理:
当你使用object.something()调用一个方法函数时,Rust会自动在object前添加&&mut*,使得object 匹配方法函数的签名。换句话说,像下面这两句代码是一样的:

p1.distance(&p2);
(&p1).distance(&p2);

第一行显然看起来更加清楚。方法函数能够使用自动引用,这是因为方法函数的消费者是一个明确的self实例。只要告诉实例调用的方法函数名字,Rust就能推测出这个方法函数是去读&self、修改&mut self还是直接消费self。Rust在调用方法函数时隐藏对实例的借用,这是出于为了更符合代码编写习惯的考量。

Methods with More Parameters 包含多个形参的方法函数

让我们来尝试为Rectangle结构添加第二个方法函数。这次我们想要比较两个Rectangle实例,如果长方形A能够包含长方形B,就返回true,反之则返回false。这个方法函数我们叫它can_hold,实现后我们就可以通过下面的例子去使用它:

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

因为rect2rect1小,而rect3rect1更宽,所以预期的结果应该如下:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

我们知道,如果我们想要定义一个方法函数,就需要将它包含进impl代码块中。这个方法函数名叫can_hold,它有一个Rectangle类型的不可更改引用形参。通过rect1.can_hold(&rect2)调用方法函数时传入的&rect2,我们能够找到对应的形参类型,它是一个Rectangle实例rect2的不可更改引用。这样做就足够了,因为我们只须要读取rect2的值而非写入,我们不须要一个可修改引用。并且我们希望main函数能保留rect2的所有权,这样在can_hold后我们还能继续使用它。can_hold的返回值是一个波尔数值,它会分别去检查self的长高是否都大于另一个Rectangle的长高。让我们将can_hold加入impl代码块中:

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

完成后我们尝试运行main函数中的代码,我们会获得期望的数据。方法函数可以包含多个形参,我们只需要将这些形参加到方法函数签名的self参数后,在方法函数中使用这些形参和在普通函数中使用形参是一样的。

Associated Functions 关联函数

impl代码块还有一个非常有用的功能,我们可以在impl代码块中定义一个不带self形参的方法函数。这种方法函数被称为关联函数,因为它们与结构紧密关联。它们仍然是函数,而不是方法函数,因为它们可以不需要一个结构的实例就能使用。你已经用过String::from这个关联函数了。

关联函数常常被用做为构造函数,它们可以返回一个新的结构实例。举例来说,我们可以提供一个关联函数,它有一个尺寸参数,即被用于长,又被用于高,所以可以通过这个关联函数方便的创建一个正方形,而不须要指定一个相同的值两次:

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

调用关联函数,我们可以使用::语法和结构名,譬如let sq = Rectangle::square(3);::语法除了被用于关联函数外,在模块的命名空间中也会被用到。我们将在第七章中介绍模块相关的内容。

Multiple impl Blocks 多个impl代码块

事实上,一个结构可以有多个impl代码块:

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

在上面的例子中,我们似乎并没有充足的理由来将方法函数拆分到impl代码块中,但这是一个有价值的语法,在第十章中我们会介绍一般类型和特性,那时你会看到在某些场景下,多impl代码块非常有用。

Summary 总结

结构允许你创建一个在域中有明确意义的自定义类型;使用结构,我们可以使得有关联关系的数据紧密连接,并且使得代码结构更为清晰;方法函数能够让你为你的结构实例指定明确的行为;关联函数可以让你在没有实例的情况下,使用结构一些功能。

但是结构并不是你创建自定义类型的唯一方法:下一章我们将介绍Rust中的枚举功能,将另一件强大的工具加入你的豪华午餐。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《The Rust Programming Language》(Rust编程语言)是一本由Rust开发社区编写的权威指南和教程,用于学习和开发Rust编程语言Rust编程语言是一种开源、现代化的系统级编程语言,具有强大的内存安全性、并发性和性能。它最初由Mozilla开发,并于2010年首次发布。Rust的设计目标是实现安全、并发和快速的系统级编程,适用于像操作系统、浏览器引擎和嵌入式设备这样的低级应用程序。 《The Rust Programming Language》提供了对Rust编程语言的全面介绍。它从基本的语法和数据类型开始,然后涵盖了Rust的所有关键概念和特性,如所有权系统、借用检查器、模块化和并发编程等。这本书不仅适合初学者,还可以作为更有经验的开发者的参考手册。 书中详细介绍了Rust的主要特性,例如所有权系统,它可以避免常见的内存错误,如空指针和数据竞争。同时,该书还着重介绍了Rust的错误处理机制和泛型编程。读者将学习如何使用Rust编写高效、安全和易于维护的代码。 《The Rust Programming Language》还包含许多实用的示例代码和练习,帮助读者通过实践加深对Rust的理解。此外,书中还介绍了一系列构建工具和库,以及有用的开发工作流程。 总之,《The Rust Programming Language》为学习和开发Rust编程语言的人们提供了清晰、全面的指南。无论您是初学者还是有经验的开发者,都可以从中受益,提高Rust编程的技能和效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值