结构体

Rust 中的结构体是一种自定义数据类型,可以将多个不同类型的数据组成一个结合体。

定义

关键字 struct 用于定义结构体。结构体可以包含字段 (成员变量),每个字段都有自己的名称和类型。结构体名称通常以大写字母开头,以区分于变量和函数名称,也要能够反应出自身数据组合的意义。

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

在创建结构体变量时需要为每个字段赋予具体的值

let user1 = User {
    active: true,
    username: String::from("ymfc"),
    email: String::from("xxxxxx@gmail.com"),
    sign_in_count: 1,
};

创建后可通过点号来访问特定字段

println!("{}", user1.email);

如何要修改某个字段的值,需将结构体变量声明为可变,加上 mut 就行。

注意:若结构体变量声明为可变,那么所有字段都是可变的。但 Rust 不允许单独声明某一部分字段为可变!

在函数中返回结构体

定义一个返回结构体的函数

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

当函数的参数名与结构体中字段名称相同时可简化写法

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

结构体更新语法

在创建结构变量中,若只需要修改一小部分字段,其余字段的值与旧实例相同,可以使用更新语法来快速创建。

// 结构体更新语法
let user2 = User {
    email: String::from("123456789@qq.com"),
    ..user1
};

针对上述有几点需要说明:

  • 上述代码发生了移动;
  • 若后续去访问 user1.username 时编译会报错,在创建完 user2 后,user1 中 username 就不能再访问了;
  • 其它字段可访问,因为它们实现了 Copy trait;
  • 代码中 ..user1 必须得放置在结构体初始化代码的最后;
println!("{}", user2.email);
// println!("{}", user1.username); // 发生了移动,String 没实现 Copy trait,所以 user1 中此字段失效
println!("{}", user1.active); // 其它的可以,因为 bool 实现 Copy trait

元组结构体

元组结构体是使用类似元组的方式来定义,这类结构体无须在声明它时对其字段进行命名。

// 元组结构体
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

// 使用元组结构体
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

单元结构体

单元结构体也为空结构体,意为没有包含任何字段的结构体,单元结构体在什么情况下会用到,当你想要在某些类型上实现一个 trait,却不需要在该类型中存储任何数据时,单元结构体就可以发挥相应的作用。

// 定义单元结构体
struct AlwaysEqual;

// 使用
let subject = AlwaysEqual;

单元结构体不需要花括号或圆括号。我们可以出于测试的目的,为这个类型实现某些特殊的行为,让它的实例等于任何类型的任何实例,而这个行为的实现不会依赖任何数据!

使用结构体示例程序

多种方式编写一个计算长方形面积的程序,相关说明见代码注释。

// 注解,为结构体启用调试输出
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    // 简单方式计算
    let width1 = 30;
    let height1 = 50;
    println!("The area the rectangle is {} square pixels.", area(width1, height1));

    // 使用元组传参方式
    let rect1 = (30, 50);
    println!("The area the rectangle is {} square pixels.", area1(rect1));

    // 使用结构体引用传参方式
    let rect2 = Rectangle {
        width: 30,
        height: 50,
    };
    println!("The area the rectangle is {} square pixels.", area2(&rect2));

    // 格式化结构体数据,使用 Debug 格式化输出
    // Debug 是另一种格式化 trait
    println!("rect2 is {:?}", rect2);
    println!("rect2 is {:#?}", rect2);

    // 使用 Debug 格式打印的另一种方法是用 dbg! 宏
    // 它会获得表达式的所有权
    // 打印出宏调用时的文件名称、代码行号及表达式的结果值,并将结果值的所有权返回
    let scale = 2;
    let rect3 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };
    // dbg! 宏会将内容打印到标准错误流 stderr
    // println! 宏则将其打印到标准输出流 stdout
    dbg!(&rect3);
}

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

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

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

输出

The area the rectangle is 1500 square pixels.
The area the rectangle is 1500 square pixels.
The area the rectangle is 1500 square pixels.
rect2 is Rectangle { width: 30, height: 50 }
rect2 is Rectangle {
    width: 30,
    height: 50,
}
[src/main.rs:27:16] 30 * scale = 60
[src/main.rs:30:5] &rect3 = Rectangle {
    width: 60,
    height: 50,
}

方法

方法也是用 fn 关键字及一个名称来声明,可以拥有参数和返回值,与函数依然是两个不同的概念,因为方法总是被定义在结构体 (或者枚举类型、trait 对象) 的上下文中,并且方法的第一个参数永远都是 self,用于指代调用该方法的结构体实例。

定义

定义方法需要使用 impl 关键字

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

impl 块中的内容都会关联至 Rectangle 类型,调用时用点号

// 方法
let rect4 = Rectangle {
    width: 30,
    height: 50,
};
println!("The area of the rectangle is {} square pixels.", rect4.area());
if rect4.width() {
    println!("The rectangle has a nonzero width; it is {}", rect4.width);
}

如果要修改值,需要将不可变引用改为可变引用 &mut self,当然,如果只是读取数据,最好还是不要这么做。

方法的名称可以与结构体中字段名称相同,这种通常用于返回字段的值,而不做其它复杂的操作,类似的方法也被称为访问器。

Rust 中没有提供类似 -> 运算符,但设计了一种名为自动引用与解引用的功能作为替代,方法调用是 Rust 中少数几个拥有这种行为的地方之一。当使用 object.something() 调用方法时,Rust 会自动为调用者 object 添加 &、&mut 或 *,以使其能够符合方法的签名,以下是等价的

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

带有更多参数的方法

来看一个更多参数的方法

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

调用

// 调用带有更多参数的方法
let rect5 = Rectangle {
    width: 30,
    height: 50,
};
let rect6 = Rectangle {
    width: 10,
    height: 45,
};
let rect7 = Rectangle {
    width: 60,
    height: 45,
};
println!("Can rect5 hold rect6? {}", rect5.can_hold(&rect6));
println!("Can rect5 hold rect7? {}", rect5.can_hold(&rect7));

关联函数

所有定义在 impl 块中的函数都被称为关联函数,可以在 impl 块中定义没有将 self 作为第一个参数的函数,当然,此函数也不能称为方法了,它不会作用于某个具体的结构体实例,String::from 就是这样的一个函数。

关联函数常常被用作构造器来返回一个结构体实例的新实例,虽然这些函数常常被命名为 new,但 new 并不是一个内置于语言本身中的特殊名称。

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

返回类型及函数体中的 Self 关键字是一个别名,它指向了 impl 关键字后面的类型,也就是 Rectangle。

调用关联函数

// 调用关联函数
let sq = Rectangle::square(3);
println!("sq width is: {}", sq.width);

总结

结构体可以让你创建有意义的自定义类型。通过使用结构体,可以将相关联的数据组合起来,并为每个数据赋予名字,从而使用代码变得更加清晰。在 impl 块中,可以定义那些与类型相关联的函数,而方法作为一种关联函数,可以为结构体的实例指定行为,但结构体不是创建自定义类型的唯一方法,例如枚举。