[Rust入门系列2]用结构体、内联函数实现类和面向对象的功能

struct,或者 structure,是一个自定义数据类型,允许你包装和命名多个相关的值,从而形成一个有意义的组合。

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

使用结构体、关联函数或方法的组合可以实现其他语言class的功能。

结构体的定义和实例化

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

定义结构体,需要使用struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为字段(field)。

一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value键-值对的形式提供字段,其中key是字段的名字,value是需要存储在字段中的数据值。

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

更新值,也需要是mut。

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

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

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

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

字段初始化简写语法(field init shorthand):

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。

结构体更新语法(struct update syntax)

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了,因为user1的username字段中的String被移到user2中。如果我们给user2的email和username都赋予新的String值,从而只使用user1的active和sign_in_count值,那么user1在创建user2后仍然有效。这是因为active和sign_in_count的类型是实现Copytrait 的类型。详情请见所有权。

元组结构体(tuple structs)

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

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值的类型不同,因为它们是不同的元组结构体的实例。元组结构体实例类似于元组,你可以将它们解构为单独的部分,也可以使用.后跟索引来访问单独的值

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

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
    let color = Color(32,44,156);
    println!("{}", color.2);
}

会得到输出156。

类单元结构体(unit-like structs)

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

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

以后我们会看到如何定义特征并在任何类型上实现它们,包括类单元结构体。

结构体数据的所有权

在前面定义的User结构体的定义中,我们使用了自身拥有所有权的String类型而不是&str字符串slice类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。

可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上生命周期(lifetimes),这是一个以后章节会讨论的Rust功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样:

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

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

编译器会报错:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` due to 2 previous errors

以后会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像String这类拥有所有权的类型来替代&str这样的引用以修正这个错误。

关联函数(associated functions)与方法(method)

定义方法

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

#[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类型相关联。

然后在main中将使用方法语法(method syntax)在Rectangle实例上调用area方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。

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

-> 运算符去哪里了?

在C/C++语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 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对方法接收者的隐式借用让所有权在实践中更友好。

关联函数

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

比如在String类型上定义的String::from函数

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

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

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

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

    if Rectangle::width(&rect1) {  // 使用关联函数调用
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

和下面代码是一样的:

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

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

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

总结

结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在impl块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,让你指定结构体的实例所具有的行为。

  • 结构体关键字struct
  • 与结构体相关联关键字impl
  • 与结构体关联函数用::,与实例关联的方法用.
  • 方法首参数可用:不可变引用&self,可变引用&mut self或获取所有权self(少见)
  • 结构体实例的=赋值遵循和变量一样的所有权关系(移动、拷贝)
  • 自动引用和解引用

本文整理自官方教程

posted @ 2025-04-10 17:48  NearlyHeadlessJack  阅读(126)  评论(0)    收藏  举报