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("someone@example.com"),
sign_in_count: 1,
};
}
结构体类型可参与更复杂结构体的构建:
struct Class {
serial_number: u32,
grade_number: u32,
entry_year: String,
members: Vec<User>,
}
Class 表示班级,
- serial_number 几班,
- grade_number 几年级,
- entry_year 起始年份,
- members 是 User 的动态数组。
结构体类型可以不断往上一层一层地套。
实际中,结构体往往是一个程序的骨干,承载对目标问题进行建模和描述的重任。
结构体的形式
三种:命名结构体、元组结构体、单元结构体。
命名结构体
每个字段都有明确的名字和类型。如User 结构体
在实例化结构体之前,若命名了与结构体字段名同名的变量,可以简写。
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,
};
}
更简洁,无歧义。结构体创建好之后,可以更新结构体的部分字段。
单独更新 email 字段:
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");
}
注意 user1 前面的 mut 修饰符,不加就没办法修改这个结构体里的字段。
已有了 User 的实例 user1,想再创建一个新的 user2,两个实例之间只有部分字段不同。
偷懒的办法:
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
};
}
少写很多重复代码。特别是当结构体有几十个字段,只想更新其中的一两个字段,特别有用。
场景:用户信息存在数据库里,要更新一个用户的一个字段的信息时,先要从数据库里把这个用户的信息取出来,做一些基本的校验,然后把要更新的字段替换成新的内容,再把这个新的用户实例存回数据库。
这样写:
// 这个示例是伪代码
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("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
其中,将结构体的一个字段值赋值给一个新的变量。
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 整个知识体系。
思考题
可以给 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());
}