3. 我在外包学 rust——结构体 struct

318 阅读10分钟

上节内容回顾

# 我在外包学 rust——引用与借用中,对 rust 的引用和借用进行了介绍,同时对切片类型形成了一定的认识。引用和借用是一体两面的,解决了所有权传来传去不方便的问题。回顾一下引用规则:

引用规则

  • 引用和借用是一体两面,你把东西借给别人用,也就是别人持有了对你这个东西的引用。
  • 所有权型变量的作用域是从它定义时开始到所属那层花括号结束。
  • 引用型变量的作用域是从它定义起到它最后一次使用时结束。
  • 引用(不可变引用和可变引用)型变量的作用域不会长于所有权变量的作用域。这是肯定的,不然就会出现悬垂引用,这是典型的内存安全问题。
  • 一个所有权型变量的不可变引用可以同时存在多个,可以复制多份。
  • 一个所有权型变量的可变引用与不可变引用的作用域不能交叠,也可以说不能同时存在。
  • 某个时刻对某个所有权型变量只能存在一个可变引用,不能有超过一个可变借用同时存在,也可以说,对同一个所有权型变量的可变借用之间的作用域不能交叠。
  • 在有借用存在的情况下,不能通过原所有权型变量对值进行更新。当借用完成后(借用的作用域结束后),物归原主,又可以使用所有权型变量对值做更新操作了。
  • 可变引用的再赋值,会执行移动操作。赋值后,原来的那个可变引用变量就不能用了。这有点类似于所有权的转移.因此一个所有权型变量的可变引用也具有所有权特征,它可以被理解为那个所有权变量的独家代理,具有排它性。

多级引用规则

  • 对于多级可变引用,要利用可变引用去修改目标资源的值的时候,需要做正确的多级解引用操作。
  • 只有全是多级可变引用的情况下,才能修改到目标资源的值。
  • 对于多级引用(包含可变和不可变),打印语句中,可以自动为我们解引用正确的层数,直到访问到目标资源的值,这很符合人的直觉和业务的需求。

今天主要介绍 rust 的其中一种复合类型——结构体 struct。

结构体struct

什么是结构体

  • 用 struct 关键字进行定义
  • 由其他基础类型或复合类型组成
  • 当所有字段(feild)同时实例化后,就生成了这个结构体的实例

示例

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

很像 TS 中的 interface 有没有。上面的User结构体的字段都是基础类型。

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

上面的代码就是结构体的实例化了,也就是当它的所有字段都实例化后,就完成了结构体的实例化。 当然,结构体的字段也可以是结构体:

struct Class {
  serial_number: u32,
  grade_number: u32,
  entry_year: String,
  members: Vec<User>,  
}

在实际应用中,结构体往往是一个程序的骨干,用来承载对目标问题进行建模和描述的重任。

注意:一旦 struct 的实例是可变的,那实例中所有的字段都是可变的。

结构体有哪些形式

  1. 命名结构体

    变量与字段同名,可以偷懒,前端er们有没有想起 es6:

    fn main() {
        let active = true;
        let username = String::from("someusername123");
        let email = String::from("someone@example.com");
        let user1 = User {
            active,    // 这里本来应该是 active: active,
            username,  // 这里本来应该是 username: username,
            email,     // 这里本来应该是 email: email,
            sign_in_count: 1,
        };
    }
    

    更新结构体部分字段:

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

    另一种偷懒,..,依旧是 es6 啊同志们,前端er 狂喜:

    fn main() {
        let active = true;
        let username = String::from("someusername123");
        let email = String::from("someone@example.com");
        let user1 = User {
            active,
            username,
            email,
            sign_in_count: 1,
        };
        let user2 = User {
            email: String::from("another@example.com"),
            ..user1    // 注意这里,直接用 ..user1
        };
    }
    
  2. 元组结构体 tuple struct

    其实是一种匿名结构体,是元组和结构体的结合体。有类型名,但没有字段名。

    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);
    
    fn main() {
        let black = Color(0, 0, 0);
        let origin = Point(0, 0, 0);
    }
    
  3. 单元结构体 unit-like struct

    单元结构体只有一个类型名,没有任何字段。创建时后面的花括号都可以省略。

    struct ArticleModule;
    
    fn main() {
       let module = ArticleModule;    // 请注意这一句,也做了实例化操作
    }
    

    那没有字段的结构体有什么用呢?其实它就相当于定义了一种类型,它的名字就是一种信息,有类型名就可以进行实例化,承载很多东西。后面我们在代码中会经常看到单元结构体。 适用于对某个类型实现某个 trait,但里面有没有要存储的数据。

结构体中的所有权

部分移动:结构体中的部分字段是可以被移出去的。

#[derive(Debug)]
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u32,
}

fn main() {
    let active = true;
    let username = String::from("someusername123");
    let email = String::from("someone@example.com");
    let user1 = User {
        active,
        username,
        email,
        sign_in_count: 1,
    };
    
    let email = user1.email;  // 在这里发生了partially moved
    
    println!("{:?}", user1)   // 这一句无法通过编译
}

报错信息:

error[E0382]: borrow of partially moved value: `user1`
  --> src/main.rs:22:22
   |
20 |     let email = user1.email;
   |                 ----------- value partially moved here
21 |     
22 |     println!("{:?}", user1)
   |                      ^^^^^ value borrowed here after partial move

由于user1中的email的所有权被移动给 email,也就是发生了部分移动,打印user1会报错。 字段是引用类型:

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

这里要通过编译,需要标注生命周期。这是后面的内容了,还没学到 😂。

几乎所有的地方,rust 都会把问题一分为:一是所有权形式的表示,另一个是借用形式的表示,借用形式的表示又可以分为不可变借用和可变借用。

给结构体添加标注

上面代码#[derive(Debug)] 为 struct 添加标注,就可以打印整个结构体,这种语法在 rust 中叫做属性标注,具体来说这里用的是派生宏属性。派生宏作用在下面紧接着的结构体类型上,可以为结构体自动添加一些功能。

如果你学过Java,可能会非常眼熟,这跟Java中的标注语法非常像,功能也是类似的,都会对原代码的元素产生作用。不过,Rust这个特性作为一套完整的宏机制,要强大得多。它让Rust的语言表达能力又上了一个台阶。

面向对象特性

虽然rust 不是一门面向对象的语言,但是具有部分面向对象的特性。 rust 承载面向对象特性的主要类型就是结构体。通过 impl 关键字可以用来给结构体或其他类型实现方法,即关联到某个类型上的函数。

方法(实例方法)

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

impl Rectangle {                // 就像这样去实现
    fn area(self) -> u32 {      // area就是方法,被放在impl实现体中
        self.width * self.height
    }
}

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

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()      // 使用点号操作符调用area方法
    );
}
// 输出
// The area of the rectangle is 1500 square pixels.

self

上面函数中的fn area(self) -> u32self 其实是self:Self的语法糖,Self是 rust 中一个特殊的类型名,表示正在被实现(impl)的类型。

impl Rectangle {
    fn area1(self) -> u32 {
        self.width * self.height
    }
    fn area2(&self) -> u32 {
        self.width * self.height
    }
    fn area3(&mut self) -> u32 {
        self.width * self.height
    }
}

上面的三种方式都是可以的。

  • self:Self 表示传入实例的所有权
  • self:&Self 表示传入实例的不可变引用
  • self:&mut Self 表示传入实例的可变引用

因为是标准用法,rust 帮我们简写成了 self&self&mut self。 调用的时候就是通过 实例.方法的方式调用:

rect1.area();

实例的引用也可以调用实例方法,且对于不可变引用,rust 会自动做正确的多级解引用操作:

#[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,
    };

    // 在这里,取了实例的引用
    let r1 = &rect1;
    let r2 = &&rect1;
    let r3 = &&&&&&&&&&&&&&&&&&&&&&rect1;  // 不管有多少层
    let r4 = &&r1;
    
    // 以下4行都能打印出正确的结果
    r1.area();
    r2.area();
    r3.area();
    r4.area();
}

对同一类型,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
    }
}

关联函数(静态方法)

如果实现在类型上的函数,它的第一个参数不是self参数,那么它就叫做此类型的关联函数。

impl Rectangle {
    fn numbers(rows: u32, cols: u32) -> u32 {
        rows * cols
    }
}

调用时,关联函数使用类型配合路径符 :: 来调用。

Rectangle::numbers(10, 10);

构造函数

Rust社区一般约定使用 new() 这个名字的关联函数作为构造函数,像下面这样把类型的实例化包起来。

impl Rectangle {
  pub fn new(width: u32, height: u32) -> Self {
    Rectangle {
        width,
        height,
    }
  }  
}
let rect1 = Rectangle::new(30, 50);

Default

在对结构体做实例化的时候,Rust又给我们提供了一个便利的设施,Default。

#[derive(Debug, Default)]      // 这里加了一个Default派生宏
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1: Rectangle = Default::default();    // 使用方式1
    let rect2 = Rectangle::default();             // 使用方式2
    
    println!("{:?}", rect1);
    println!("{:?}", rect2);
}

// 打印出如下:
Rectangle { width: 0, height: 0 }
Rectangle { width: 0, height: 0 }

Default有两种使用方式,一种是直接用 Default::default(),第二种是用类型名 ::default(),它们的实例化效果是一样的。 可以看到,打出来的实例字段值都0,是因为u32类型默认值就是 0。对于通用类型,比如u32这种类型来说,取 0 是最适合的值了,想一想取其他值是不是没办法被大多数人接受? 但是,对于我们特定场景的Rectangle这种,我们可能希望给它赋一个初始的非 0 值。在Rust中,这可以做到,但是需要用到后面的知识。目前我们就可以先用约定的 new 关联函数+参数来达到我们的目的。

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

impl Rectangle {
  pub fn new(width: u32, height: u32) -> Self {
    Rectangle {
        width,
        height,
    }
  }  
}

const INITWIDTH: u32 = 50;
const INITHEIGHT: u32 = 30;

fn main() {
    // 创建默认初始化值的Rectangle实例
    let rect1 = Rectangle::new(INITWIDTH , INITHEIGHT);
}

结尾

今天就到这里吧,累了,中午没睡着,下午略崩溃,我要去联调借口了。同志们拜拜,加油!! Passion~(有气无力)