27

Rust 入门 (五)

 4 years ago
source link: https://segmentfault.com/a/1190000021209140
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

定义并介绍结构体

结构体和我们前面学习的元组类似,结构体中的每一项都可以是不同的数据类型。和元组不同的地方在于,我们需要给结构体的每一项命名。结构体较元组的优势是:我们声明和访问数据项的时候不必使用索引,可以直接使用名字。

声明结构体

我们直接看一个结构体的例子:

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

结构体使用关键字 struct 开头,紧跟结构体的名字,之后就是大括号包裹的多条结构体数据项,每个数据项由名字和类型组成,我们把每个数据项称为字段。

结构体实例化

我们声明了一个结构体后,如何使用它呢?接下来创建一个结构体的实例:

let user1 = User {
    email: String::from("[email protected]"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

可以看到,创建结构体实例 (结构体实例化) 是直接以结构体名字开头,之后就是大括号包裹的键值对。这些键值对顺序和声明结构体的顺序无关,换句话说,声明结构体就是定义一个通用的模版,结构体实例化就是给模版填充值。

结构体数据的存取

创建了结构体实例,那我们应该如何存取实例中的数据呢?比如我们要获取邮箱信息,可以 user1.email 获取邮箱内容,如果实例是可变的,我们可以直接给它赋值。直接看个赋值的例子吧:

let mut user1 = User {
    email: String::from("[email protected]"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("[email protected]");

这个实例整个都是可变的,如果我只想修改 email 和 username 两个字段,而不想修改其它的字体,应该怎么办呢?

修改部分字段

要知道,rust 不允许我们只把部分字段标记为可变。那我们可不可以把这个结构体放在函数中,让函数返回一个新的实例呢?看例子:

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

在这个例子中,函数参数名字和结构体字段名字是相 的,如果有很多字段,一个一个地写名字和参数是很无聊的,不过,rust 为我们提供了简写的方式

结构体字段初始化简写

我们直接看例子吧:

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

email 和 username 的结构体字段名字和函数传入的参数变量的名字是相同的,我们可以只写一遍。

结构体更新

如果旧的结构体实例中的一部分值修改,使之变成一个新的实例,使用结构体更新语法会更加方便。如果在 user1 的基础上修改 email 和 username 而不改变其他的值,我们通常会这样写:

let user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

如果我们使用了结构体更新语法,创建新结构体就变成了这样:

let user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    ..user1
};

利用 .. 语法达到了和前面案例相同的结果。

元组结构体

我们也可以定义看起来像元组的结构体,我们称它为元组结构体。元组结构体只定义字段的类型而不定义字段的名字,它主要用于给整个元组一个名字来区分不同的元组。直接看例子:

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

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

定义了黑色和原点,显然二者的类型不相同,虽然声明的字段类型相同,但是二者是使用不同的元组结构体实例化的。

空结构体

我们也可以定义空结构体,它不包含任何字段。详细内容后文再聊。

写个关于结构体的例子

为了学习什么时候用到结构体,我们来写一个计算长方形面积的程序。我们从使用简单变量写起,一直写到使用结构体为止。

编写项目

我们先来创建一个名叫 rectangles 的项目,然后写一个通过宽高计算面积的函数。

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

    println!(
        "长方形的面积是 {}。",
        area(width1, height1)
    );
}

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

我们运行的结果是:

cargo run
   Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.51s
     Running `target/debug/rectangles`
长方形的面积是 1500。

我们调用 area 函数完成了对长方形面积的计算,但是描述长方形的宽高是分开的,我们能不能想个办法,把两个值变成一个值?很容易想到的办法就是元组,对,没错,是元组。

利用元组重构项目

我们直接看重构完成后的代码:

fn main() {
    let rect1 = (30, 50); // 定义元组

    println!(
        "长方形的面积是 {}。",
        area(rect1)
    );
}

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

现在只有一个值了,但是又有了一个新的问题:元组没有名字,计算面积还好,元组中的两个值混了也没事,如果是把这个长方形画出来,那就得记着 0 位置是宽,1 位置是高,别人调用我们的代码时,别人也得记着这个顺序。这是很不友好的,那应该怎么解决呢?

利用结构体重构项目

我看来看重构后的代码:

struct Rectangle { // 定义结构体
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 }; // 结构体实例化

    println!(
        "长方形的面积是 {}。",
        area(&rect1)
    );
}

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

现在只有一个参数了,而且参数也有了实际的含义了,似乎完成了我们的目标。但是 area 函数只能计算长方形的面积,我们希望这个函数尽可能地在 Rectangle 结构体内部,因为它不能处理其它的结构体。那我们应该如果做呢?我们可以把该函数转变成 Rectangle 结构体的方法。在此之前,我们先看一个调试程序的小技巧。

打印结构体

在我们调试程序的时候,经常想看一下结构体每个字段的值是什么,如果直接打印结构体会报错,比如:

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

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

    println!("rect1 是: {}", rect1); // 报错: `Rectangle` cannot be formatted with the default formatter
}

rust 为我们提供了打印的方法,在结构体定义的上方加入 #[derive(Debug)] 声明,在打印的大括号中加入 :? 就可以了,看例子:

#[derive(Debug)]    // 这里加入声明
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 是: {:?}", rect1); // 这里加入打印的格式
}

运行的结果是:

cargo run
   Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/rectangles`
rect1 是: Rectangle { width: 30, height: 50 }

这个结构体的输出很不美观,我们调整一下,让结构体可以结构化输出,只需要把 {:?} 改成 {:#?} 即可,然后输出就变成了:

cargo run
   Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/rectangles`
rect1 是: Rectangle {
    width: 30,
    height: 50,
}

后文会详细介绍 derive 声明。

结构体方法

方法和函数类似,都是以关键字 fn 打头,后接方法名、参数和返回值,最后是方法体。方法和函数不同之处在于:方法定义在结构体的上下文中,方法的第一个参数是 self (self 代表结构体方法调用者的实例)。

定义方法

我们来修改 area 方法,把它变成 Rectangle 结构体的方法,如下:

#[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!(
        "长方形的面积是 {}。",
        &rect1.area()
    );
}

使用 impl 关键字和结构体名字 Rectangle 来定义 Rectangle 的上下文,然后把 area 函数放进去,就把函数的第一个参数修改成 self,在主函数中,可以让 rect1 实例直接调用 area 方法。

对于方法和第一个参数,我们使用 &self 来代替 rectangle: &Rectangle,因为在 impl Rectangle 声明的上下文中,rust 知道 self 代表的是 Rectangle,但是还需要在 self 前面加上 & 符号,意思是借用 Rectangle 的不可变实例。如果要借用 Rectangle 的可变实例,参数需要写成 &mut self。

使用方法较函数的优势在于:添加方法的时候可以直接使用结构体方法语法,而不必要在每个函数中重复写实例的类型。我们可以把结构体对应的所有方法都写在结构体的上下文中,当我们为别人提供库函数的时候不必要在很多地方寻找需要的函数。

多参方法

我们再写一个例子,第一个长方形能不能装下第二个长方形,如果能装下,就返回 true,否则返回 false,实例代码如下:

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

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


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

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

我们先来看运行结果:

rect1 装下 rect2? true
rect1 装下 rect3? false

我们在 Rectangle 上下文中定义第二个方法 can_hold,该方法借用另一个 Rectangle 实例作参数,我们需要告诉调用者参数的类型。

关联函数

在结构体上下文中也可以定义不含有 self 参数的函数,这种函数被称为关联函数。这里叫函数,不叫方法,因为这些函数不是使用结构体实例调用的,而是使用双冒号调用,比如之前使用的 String::from 就是一个关联函数。

关联函数常常被用于返回结构体实例的构造函数。例如,我们可以提供一个关联函数来生产 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
    }
}

这种语法是有效的,后文学习中可能会使用到,知道有这种语法就好了。

欢迎阅读单鹏飞的学习笔记


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK