Rust 语言从入门到实战 唐刚--读书笔记05

Rust 语言从入门到实战  唐刚 --学习笔记05

基础篇 (11讲)
05|复合类型(上):结构体与面向对象特性

Rust 中的复合类型——结构体。

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

结构体示例

定义 User 结构体。

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

User 结构体由 4 个字段组成。

  •     active 字段:bool 类型,用户激活状态。
  •     username 字段:字符串类型,用户名字。
  •     email 字段:字段串类型,用户邮箱。
  •     sign_in_count 字段:u64 类型,用户登录次数。

    
User 的实例化要这 4 个字段同时实例化,缺一不可:

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

结构体类型可参与更复杂结构体的构建:

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

Class 表示班级,

  •     serial_number 几班,
  •     grade_number 几年级,
  •     entry_year 起始年份,
  •     members 是 User 的动态数组。

结构体类型可以不断往上一层一层地套。
实际中,结构体往往是一个程序的骨干,承载对目标问题进行建模和描述的重任

结构体的形式

三种:命名结构体、元组结构体、单元结构体。

命名结构体


每个字段都有明确的名字和类型。如User 结构体
在实例化结构体之前,若命名了与结构体字段名同名的变量,可以简写。

fn main() {
    let active = true;
    let username = String::from("someusername123");
    let email = String::from("[email protected]");
    let user1 = User {
        active,    // 这里本来应该是 active: active,
        username,  // 这里本来应该是 username: username,
        email,     // 这里本来应该是 email: email,
        sign_in_count: 1,
    };
}

更简洁,无歧义。结构体创建好之后,可以更新结构体的部分字段。
        单独更新 email 字段:

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

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

注意 user1 前面的 mut 修饰符,不加就没办法修改这个结构体里的字段。

已有了 User 的实例 user1,想再创建一个新的 user2,两个实例之间只有部分字段不同。

        偷懒的办法:

fn main() {
    let active = true;
    let username = String::from("someusername123");
    let email = String::from("[email protected]");
    let user1 = User {
        active,
        username,
        email,
        sign_in_count: 1,
    };
    let user2 = User {
        email: String::from("[email protected]"),
        ..user1    // 注意这里,直接用 ..user1
    };
}

少写很多重复代码。特别是当结构体有几十个字段,只想更新其中的一两个字段,特别有用。

场景:用户信息存在数据库里,要更新一个用户的一个字段的信息时,先要从数据库里把这个用户的信息取出来,做一些基本的校验,然后把要更新的字段替换成新的内容,再把这个新的用户实例存回数据库。

这样写:

// 这个示例是伪代码
let user_id = get_id_from_request;
let new_user_name = get_name_from_request();
let old_user: User = get_from_db(user_id);
let new_user: User = User {
    username: new_user_name,
    ..old_user    // 注意这里的写法
}
new_user.save()

(这是语法糖)

元组结构体


匿名结构体,是元组和结构体的结合体。

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

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

元组结构体有类型名,无字段名,字段是匿名的。
        场景: RGB 颜色对、三维坐标这种各分量之间有对称性,又总是一起出现。

Color 类型和 Point 类型的元组部分,都是 (i32, i32, i32),由于类型名不同,是不同的类型,black 实例和 origin 实例就是两个完全不同的东西,前者表示黑色,后者表示原点。

单元结构体


只有一个类型名字,没有任何字段。

单元结构体在定义和创建实例的时候,连后面的花括号都可以省略。

struct ArticleModule;   // struct ArticleModule{}; 

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


用结构体 ArticleModule 类型实际创建了一个实例,ArticleModule 的定义和实例化都没有使用花括号。这种写法非常紧凑,要注意分辨,不然会疑惑:类型为什么能直接赋给一个变量。

用处:定义了一种类型,它的名字就是一种信息,有类型名就可以进行实例化,承载很多东西。

结构体中的所有权问题

部分移动


        Rust 的结构体有一种与所有权相关的特性,叫部分移动(Partial Move)。
结构体中的部分字段是可以被移出去的:

#[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("[email protected]");
    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

其中,将结构体的一个字段值赋值给一个新的变量。

let email = user1.email;


因为 email 字段是 String 类型,是一种所有权类型,于是 email 字段的值被移动了。移动后,email 变量拥有了那个值的所有权,user1 中的 email 字段就被标记无法访问了。

改一下代码,不直接打印 user1 实例整体,分别打印 email 之外的另外三个字段。

let email = user1.email;

println!("{}", user1.username);      // 分别打印另外3个字段 
println!("{}", user1.active);
println!("{}", user1.sign_in_count);

可以得到正确的输出。单独打印 email 字段,也是不行。
这就是结构体中所有权字段被部分移动的情景。

字段是引用类型


上边的 User 类型里的所有字段,都是带所有权的字段。
        在赋值行为上,bool 和 u32 会默认复制一份新的所有权,而 String 会移动之前那份所有权到新的变量。
        全部定义带所有权的字段,是定义结构体类型的主要方式。


Rust 的结构体支持字段是借用类型。

struct User {
    active: &bool,       // 这里换成了 &bool
    username: &str,      // 这里换成了 &str
    email: &str,         // 这里换成了 &str
    sign_in_count: &u32, // 这里换成了 &u32
}

把 4 个字段都换成了对应的引用形式。


写法当然可以,不过暂时还不能通过 Rust 的编译,需要加一些额外的标注(生命周期)。(看第 20 讲。)


Rust :所有权形式 + 借用形式(不可变借用形式 + 可变借用形式)。

一般用的几乎都是所有权形式的结构体。
初学者,切忌贪图所有语言特性,应该以实用为主。

给结构体添加标注

给类型添加标注。

#[derive(Debug)]        // 这里,在结构体上面添加了一种标注
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u32,
}

标注后,就可以把整个结构体打印出来了。

println!("{:?}", user1);    // 注意这里的 :? 符号

 #[derive(Debug)] 语法在 Rust 中叫属性标注,是派生宏属性。

派生宏作用在下面紧接着的结构体类型上,为结构体自动添加一些功能
        派生 Debug 支持在 println! 中用 {:?} 格式把结构体打印出来,非常方便。

跟 Java 中的标注语法非常像,功能也类似,都会对原代码的元素产生作用。
        Rust 有一套完整的宏机制,要强大得多。让 Rust 的语言表达能力又上了一个台阶。

面向对象特性

Rust 不是一门面向对象的语言,但能支持部分面向对象的特性
        Rust 承载面向对象特性的主要类型就是结构体。
        关键字 impl 给结构体或其他类型实现方法,即关联在某个类型上的函数

方法(实例方法)


用 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.

给 Rectangle 类型实现了 area 方法,在 Rectangle 的实例 rect1 上用点号(.)调用这个方法。

Self


注意 area 方法的签名。

fn area(self) -> u32 

参数是一个 self 。

这是 Rust 的语法糖,self 的完整写法是 self: Self, Self 是 Rust 里一个特殊的类型名,表示正在被实现(impl)的那个类型。

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

3 种形式都可以。

方法是实现在类型上的特殊函数,第一个参数是 Self 类型,含 3 种形式。

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

简写成 self、&self、&mut self。不会产生歧义。

上述代码展开后:

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

方法调用的时候,直接在实例上使用 . 操作符调用,第一个参数是实例自身,会默认传进去不需要单独写出来

rect1.area1();  // 传入rect1
rect1.area2();  // 传入&rect1
rect1.area3();  // 传入&mut rect1

        有没有 C++、Java 等方法的 this 指针的既视感?
Rust 中,基本上一切都是显式化的,不存在隐藏提供一个参数给你的情况。少很多坑。

实例的引用也可以直接调用方法。
如,对不可变引用,可以这样调用。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,
实现在类型上的函数,第一个参数不是 self 参数,叫此类型的关联函数

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

调用时,关联函数用类型配合路径符 :: 来调用。
注意与实例用点运算符调用方法的区别。

Rectangle::numbers(10, 10);

        Rust 中的关联函数跟 C++、Java 里的静态方法起着类似的作用?确实差不多。
Rust 这里不需要额外引入一个 static 修饰符去定义,靠是否有 Self 参数就已经能明确地区分实例方法与关联函数了。

构造函数

不像 C++、Java 等,Rust 中没有专门的构造函数。Rust 中结构体可直接实例化,如前面定义的 Rectangle。

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

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

一般约定用 new() 这个关联函数,把类型的实例化包起来。

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

然后,用这行代码创建新实例。

let rect1 = Rectangle::new(30, 50);

但 new 这个名字不是强制的。会看到 from()、from_xxx() 等用其他名字起到构造函数的功能。


Default

对结构体做实例化,可以 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。
        若希望给它赋一个初始的非 0 值,可以做到,但是需要用到后面的知识。

目前用约定的 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);
}

小结

  • 结构体:命名结构体、元组结构体、单元结构体
  • 结构体中的所有权问题:部分移动。
  • 结构体上添加标注:增强结构体的能力。
  • 用 impl 关键字:为结构体实现面向对象特性。

Rust 不是一门完整的面向对象语言,缺乏继承等机制。OOP 不是编程语言的全部。
Rust 利用 trait 等机制,能提供比 OOP 语言更解耦的抽象、更灵活的配置。

结构体是用户自定义类型的主要实现者
除了具体的语法知识点,用所有权和借用的思路去贯穿 Rust 整个知识体系。
Rust 语言从入门到实战 唐刚--读书笔记05_第1张图片

思考题

可以给 i8 类型做 impl 吗? ->违反孤儿规则(Rust's orphan rules )

不能直接对原生类型做impl
但可以用newtype模式对i8封装一下,再impl


//基本数据类型无法实现impl。
//目前知道可以给基本数据类型添加操作的方式,通过 trait. 
//要注意,trait和类型至少有一个要在当前的模块中

trait Operate {
  fn plus(self) -> Self;
}

impl Operate for i8 {
    fn plus(self) -> Self {
        self + self
    }
}

fn main() {
    let  a = 1i8;
    println!("{}",a.plus());
}


 

你可能感兴趣的:(Rust,语言从入门到实战,唐刚,学习笔记,rust,开发语言,学习)