结构体和元组类似,它们都包含多个相关的值。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。

Rust 程序设计语言学习——结构体-LMLPHP

1 定义结构体

定义结构体,需要使用 struct 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段(field)。例如,下面展示了一个存储用户账号信息的结构体:

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

一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value 键 - 值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像下面这样来声明一个特定的用户:

fn main() {
    let user1 = User {
        active: true,
        username: String::from("xxx"),
        email: String::from("xxx@163.com"),
        sign_in_count: 1,
    };
}

为了从结构体中获取某个特定的值,可以使用点号。举个例子,想要用户的邮箱地址,可以用 user1.email。如果结构体的实例是可变的,我们可以使用点号并为对应的字段赋值。下面展示了如何改变一个可变的 User 实例中 email 字段的值:

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("xxx"),
        email: String::from("xxx@163.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("xxx111@163.com");
}

注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。

如下展示了一个 build_user 函数,它返回一个带有给定的 email 和用户名的 User 结构体实例。active 字段的值为 true,并且 sign_in_count 的值为 1

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

为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 emailusername 字段名称与变量有些啰嗦。如果结构体有更多字段,重复每个名称就更加烦人了。

2 结构体使用字段初始化简写语法

如果函数参数名与字段名都完全相同,我们可以使用字段初始化简写语法(field init shorthand)来重写 build_user,这样其行为与之前完全相同,不过无需重复 usernameemail 了,如下所示。

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

这里我们创建了一个新的 User 结构体实例,它有一个叫做 email 的字段。我们想要将 email 字段的值设置为 build_user 函数 email 参数的值。因为 email 字段与 email 参数有着相同的名称,则只需编写 email 而不是 email: email

3 结构体更新语法

使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有用的。这可以通过结构体更新语法(struct update syntax)实现。

首先,示例展示了不使用更新语法时,如何在 user2 中创建一个新 User 实例。我们为 email 设置了新的值,其他值则使用了实例中创建的 user1 中的同名值:

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

使用结构体更新语法,我们可以通过更少的代码来达到相同的效果,.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。

fn main() {
    let user2 = User {
        email: String::from("another@163.com"),
        ..user1
    };
}

上面的代码也在 user2 中创建了一个新实例,但该实例中 email 字段的值与 user1 不同,而 usernameactivesign_in_count 字段的值与 user1 相同。..user1 必须放在最后,以指定其余的字段应从 user1 的相应字段中获取其值,但我们可以选择以任何顺序为任意字段指定值,而不用考虑结构体定义中字段的顺序。

请注意,结构更新语法就像带有 = 的赋值,因为它移动了数据。在这个例子中,总体上说我们在创建 user2 后不能就再使用 user1 了,因为 user1username 字段中的 String 被移到 user2 中。如果我们给 user2emailusername 都赋予新的 String 值,从而只使用 user1activesign_in_count 值,那么 user1 在创建 user2 后仍然有效。activesign_in_count 的类型是实现 Copy trait 的类型。

4 元组结构体

也可以定义与元组类似的结构体,称为元组结构体(tuple structs)。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。

要定义元组结构体,以 struct 关键字和结构体名开头并后跟元组中的类型。例如,下面是两个分别叫做 ColorPoint 元组结构体的定义和用法:

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

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

注意 black 和 origin 值的类型不同,因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段可能有着相同的类型。例如,一个获取 Color 类型参数的函数不能接受 Point 作为参数,即便这两个类型都由三个 i32 值组成。在其他方面,元组结构体实例类似于元组,你可以将它们解构为单独的部分,也可以使用 . 后跟索引来访问单独的值等等。

5 类单元结构体

我们也可以定义一个没有任何字段的结构体!它们被称为类单元结构体(unit-like structs)因为它们类似于 (),即“元组类型”中提到的 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。下面是一个声明和实例化一个名为 AlwaysEqual 的 unit 结构的例子。

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

要定义 AlwaysEqual,我们使用 struct 关键字,我们想要的名称,然后是一个分号。不需要花括号或圆括号!然后,我们可以以类似的方式在 subject 变量中获得 AlwaysEqual 的实例:使用我们定义的名称,不需要任何花括号或圆括号。想象一下,我们将实现这个类型的行为,即每个实例始终等于每一个其他类型的实例,也许是为了获得一个已知的结果以便进行测试。我们不需要任何数据来实现这种行为。

6 方法语句

方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),并且它们第一个参数总是 self,它代表调用该方法的结构体实例。

6.1 定义方法

下面我们先来看一段示例和它的运行结果,再来解释这段示例中的一些语法。

#[derive(Debug)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
    a: u8
}

impl Color {
    fn argb(&self) -> u32 {
        ((self.a as u32) << 24) |
        ((self.r as u32) << 16) | 
        ((self.g as u32) << 8) | 
        (self.b as u32)
    }
}

fn main() {
    let pink = Color {
        r: 255,
        g: 192,
        b: 203,
        a: 255
    };

    println!("The pink of argb is 0x{:X}.", pink.argb());
}

运行结果

The pink of argb is 0xFFFFC0CB.

为了使函数定义于 Color 的上下文中,我们开始了一个 impl 块(impl 是 implementation 的缩写),这个 impl 块中的所有内容都将与 Color 类型相关联。接着将 argb 函数移动到 impl 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self。然后在 main 中使用方法语法(method syntax)在 Color 实例上调用 argb 方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。

argb 的签名中,使用 &self 来替代 color: &Color&self 实际上是 self: &Self 的缩写。在一个 impl 块中,Self 类型是 impl 块的类型的别名。方法的第一个参数必须有一个名为 selfSelf 类型的参数,所以 Rust 让你在第一个参数位置上只用 self 这个名字来缩写。注意,我们仍然需要在 self 前面使用 & 来表示这个方法借用了 Self 实例,就像我们在 color: &Color 中做的那样。方法可以选择获得 self 的所有权,或者像我们这里一样不可变地借用 self,或者可变地借用 self,就跟其他参数一样。

这里选择 &self 的理由跟在函数版本中使用 &Color 是相同的:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self。通过仅仅使用 self 作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。

使用方法替代函数,除了可使用方法语法和不需要在每个函数签名中重复 self 的类型之外,其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 impl 块中,而不是让将来的用户在我们的库中到处寻找 Color 的功能。

6.2 带有更多参数的方法

比如给 Color 结构体添加一个 alpha_argb 方法,使用方法参数传入的 alpha 值返回相应的 argb 十六进制表示形式。

#[derive(Debug)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
    a: u8
}

impl Color {
    fn argb(&self) -> u32 {
        ((self.a as u32) << 24) |
        ((self.r as u32) << 16) | 
        ((self.g as u32) << 8) | 
        (self.b as u32)
    }
    
    fn alpha_argb(&self, alpha: u8) -> u32 {
        ((alpha as u32) << 24) |
        ((self.r as u32) << 16) | 
        ((self.g as u32) << 8) | 
        (self.b as u32)
    }
}

fn main() {
    let pink = Color {
        r: 255,
        g: 192,
        b: 203,
        a: 255
    };

    println!("The pink of argb is 0x{:X}.", pink.argb());
    println!("The pink of argb is 0x{:X}.", pink.alpha_argb(128));
}

6.3 关联函数

所有在 impl 块中定义的函数被称为关联函数(associated functions),因为它们与 impl 后面命名的类型相关。我们可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在 String 类型上定义的 String::from 函数。

不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new ,但 new 并不是一个关键字。

例如将 alpha_argb 函数改造一番,&self 改为 color: &Color,函数内部也做相应的修改,整体修改后如下:

#[derive(Debug)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
    a: u8
}

impl Color {
    fn argb(&self) -> u32 {
        ((self.a as u32) << 24) |
        ((self.r as u32) << 16) | 
        ((self.g as u32) << 8) | 
        (self.b as u32)
    }
    
    fn alpha_argb(color: &Color, alpha: u8) -> u32 {
        ((alpha as u32) << 24) |
        ((color.r as u32) << 16) | 
        ((color.g as u32) << 8) | 
        (color.b as u32)
    }
}

fn main() {
    let pink = Color {
        r: 255,
        g: 192,
        b: 203,
        a: 255
    };

    println!("The pink of argb is 0x{:X}.", pink.argb());
    println!("The pink of argb is 0x{:X}.", Color::alpha_argb(&pink, 128));
}

使用结构体名和 :: 语法来调用这个关联函数。这个函数位于结构体的命名空间中,:: 语法用于关联函数和模块创建的命名空间。

6.4 多个 impl 块

每个结构体都允许拥有多个 impl 块。这里没有理由将这些方法分散在多个 impl 块中,不过这是有效的语法。

impl Color {
    fn argb(&self) -> u32 {
        ((self.a as u32) << 24) |
        ((self.r as u32) << 16) | 
        ((self.g as u32) << 8) | 
        (self.b as u32)
    }
}

impl Color {    
    fn alpha_argb(color: &Color, alpha: u8) -> u32 {
        ((alpha as u32) << 24) |
        ((color.r as u32) << 16) | 
        ((color.g as u32) << 8) | 
        (color.b as u32)
    }
}

参考链接

  1. Rust 官方网站:https://www.rust-lang.org/zh-CN
  2. Rust 官方文档:https://doc.rust-lang.org/
  3. Rust Play:https://play.rust-lang.org/
  4. 《Rust 程序设计语言》
03-27 17:32