Rust 入门 (五)

定义并介绍结构体

结构体和咱们前面学习的元组相似,结构体中的每一项均可以是不一样的数据类型。和元组不一样的地方在于,咱们须要给结构体的每一项命名。结构体较元组的优点是:咱们声明和访问数据项的时候没必要使用索引,能够直接使用名字。函数

声明结构体

咱们直接看一个结构体的例子:学习

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

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

结构体实例化

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

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

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

结构体数据的存取

建立告终构体实例,那咱们应该如何存取实例中的数据呢?好比咱们要获取邮箱信息,能够 user1.email 获取邮箱内容,若是实例是可变的,咱们能够直接给它赋值。直接看个赋值的例子吧:debug

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

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

修改部分字段

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

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

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

结构体字段初始化简写

咱们直接看例子吧:索引

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("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

若是咱们使用告终构体更新语法,建立新结构体就变成了这样:

let user2 = User {
    email: String::from("another@example.com"),
    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
    }
}

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

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

相关文章
相关标签/搜索