结构体

发布时间 2023-03-22 21:13:33作者: 点解我最型

struct 或者 structure,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合,如果你熟悉一门面向对象语言,struct 就像对象中的数据属性

定义并实例化结构体

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

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

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

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

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

为了从结构体中获取某个特定的值,可以使用点号,如果我们只想要用户的邮箱地址,可以用 user1.email,要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并为对应的字段赋值

fn main() {
    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 并不允许只将某个字段标记为可变,另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例

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

变量与字段同名时的字段初始化简写语法

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

使用结构体更新语法从其他实例创建实例

fn main() {
    // --snip--

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

.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值

fn main() {
    // --snip--

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

请注意,在这个例子中,我们在创建 user2 后不能再使用 user1,因为 user1username 字段中的 String 被移到 user2 中,如果我们给 user2emailusername 都赋予新的 String 值,从而只使用 user1activesign_in_count 值,那么 user1 在创建 user2 后仍然有效,activesign_in_count 的类型是实现 Copy trait 的类型

没有命名字段的元组结构体

也可以定义与元组类似的结构体,称为元组结构体,当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的

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

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

注意 blackorigin 值的类型不同,因为它们是不同的元组结构体的实例,定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型,在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 . 后跟索引来访问单独的值

没有任何字段的类单元结构体

我们也可以定义一个没有任何字段的结构体!它们被称为类单元结构体,因为它们类似于 (),即元组类型一节中提到的 unit 类型,类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

使用结构体的代码例子

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
}

函数 area 现在被定义为接收一个名叫 rectangle 的参数,其类型是一个结构体 Rectangle 实例的不可变借用,第 4 章讲到过,我们希望借用结构体而不是获取它的所有权,这样 main 函数就可以保持 rect1 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &

通过派生 trait 增加实用功能

如果能够在调试程序时打印出 Rectangle 实例来查看其所有字段的值就更好了

使用 #[derive(Debug)] {:?} {:#?}

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

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

    println!("rect1 is {:?}", rect1);
}
$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

另一种使用 Debug 格式打印数值的方法是使用 dbg! 宏,dbg! 宏接收一个表达式的所有权,打印出码调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权,调用 dbg! 宏会打印到标准错误控制台流(stderr),而不是 println!,后者会打印到标准输出控制台流(stdout),我们将在第 12 章 “将错误信息写入标准错误而不是标准输出” 一节中更多地讨论 stderrstdout

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

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

我们可以把 dbg! 放在表达式 30 * scale 周围,因为 dbg! 返回表达式的值的所有权,所以 width 字段将获得相同的值,就像我们在那里没有 dbg! 调用一样。我们不希望 dbg! 拥有 rect1 的所有权,所以我们在下一次调用 dbg! 时传入一个引用,下面是这个例子的输出结果:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

当你试图弄清楚你的代码在做什么时,dbg! 宏可能真的很有帮助!

方法语法

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

定义方法

让我们把前面实现的获取一个 Rectangle 实例作为参数的 area 函数,改写成一个定义于 Rectangle 结构体上的 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()
    );
}

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

area 的签名中,使用 &self 来替代 rectangle: &Rectangle&self 实际上是 self: &Self 的缩写,

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

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

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

请注意,我们可以选择将方法的名称与结构中的一个字段相同。例如,我们可以在 Rectangle 上定义一个方法,并命名为 width

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

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

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

在这里,我们选择让 width 方法的行为是如果实例的 width 字段的值大于 0,返回 true,如果该值为 0,则返回 false:我们可以在同名的方法中使用一个字段,我们可以在同名的方法中使用一个字段来达到任何目的,在 main 中,当我们在 rect1.width 后面加上括号时,Rust 知道我们指的是方法 width。当我们不使用圆括号时,Rust 知道我们指的是字段 width

通常,但并不总是如此,与字段同名的方法将被定义为只返回字段中的值,而不做其他事情,这样的方法被称为 Getters,Rust 并不像其他一些语言那样为结构字段自动实现它们,Getters 很有用,因为你可以把字段变成私有的,但方法是公共的,这样就可以把对字段的只读访问作为该类型公共 API 的一部分。我们将在第七章中讨论什么是公有和私有,以及如何将一个字段或方法指定为公有或私有

带有更多参数的方法

这回,我们让一个 Rectangle 的实例获取另一个 Rectangle 实例,如果 self 能完全包含第二个长方形则返回 true,否则返回 false

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
    }
}

关联函数

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

关联函数经常被用作返回一个结构体新实例的构造函数,例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle 而不必指定两次同样的值

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

使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);,这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间

多个 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 块中,不过这是有效的语法,第 10 章讨论泛型和 trait 时会看到实用的多 impl 块用例