前言
本章大部分内容与 Rust 相关,由于作者也是锵锵入门 Rust,基本都是编译器在教我编程,难免有所疏漏或存在错误,欢迎指正!
相关代码可以直接在线运行测试,访问 Rust Playground 即可。
面向对象的 Rust
在继续进行后面的说明前让我们首先来了解一下 rust 的面向对象。
JS 中的对象
以 JS 为例,我们可以很方便地像下面这样创建对象, 甚至你初始化的时候漏掉参数直接不写也不一定报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| let person = { name: "jack", age: 22, greet: function() { console.log(`${this.name} greetings!`) } } person.greet();
class Person { constructor(data) { this.name = data.name; this.age = data.age; } greet() { console.log(`${this.name} greetings!`); } }
let Jack = new Person({ name: "Jack", age: 22 }); Jack.greet();
let John = new Person({ name: "John" }); John.greet();
|
Rust 构建对象
但是 rust 本身并没有实际地提供类似 Class 这样的类型来实现对象,而是通过结构体和枚举的方法实现了类似对象的功能,让我们将上面的 JS 对象转成 rust 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| pub struct Person{ pub name: String, pub age: u8 }
impl Person { pub fn greet(&self) { println!("{} greetings!", self.name); } pub fn rename(&mut self, new_name: String) { self.name = new_name; } }
fn main() { let mut jack = Person { name: "Jack".to_string(), age: 22 }; jack.greet(); jack.rename("Mike".to_string()); jack.greet(); }
|
相信看了上面的代码,完全不了解 rust 的小伙伴肯定一脸懵。
impl 在这里是怎么用,枚举不应该是 enum?
let 后面怎么还接了个 mut,mut 又是干什么的?
name 已经是字符串了,为什么要再 to_string() ?
那当然,与之对应的展开来就是 rust 的语言特性了,它们分别是:
方法 Method 与特征 Trait
所有权与借用
生命周期
数据类型及泛型
方法 Method 与特征 Trait
方法与函数的区别相信熟悉面向对象编程的小伙伴肯定了如指掌, impl 就相当于定义了一个函数块来单独存放对象的方法,当然这也意味着 impl 必然是和 struct 成对出现的。
方法有了,那特征又是什么呢?
别急,现在我们假设这样一个场景,有 Teacher 和 Student 两个对象,它们有着部分相同的属性和方法。
为了更进一步的抽象,我们很容易想到将公有的属性方法抽离出来,作 Person 类,然后 Teacher 和 Student 分别继承 Person 类,再在此基础上定义自身的属性与方法。
但是 Rust 并没有继承机制,而是使用了 trait 特征来实现类似功能,至于为什么这么做可以看看 继承,作为类型系统与代码共享。
现在让我们回到刚刚举的例子,看看应该如何实现它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| pub struct Student { pub name: String, pub age: u8, pub grade: u8, }
pub struct Teacher { pub name: String, pub age: u8, pub subject: String }
pub trait Actions { fn get_name(&self) -> String; fn greet(&self) { println!("{} greetings!", self.get_name()); } fn bye(&self) { println!("{} say bye~", self.get_name()); } }
impl Actions for Student { fn get_name(&self) -> String { self.name.clone() } }
impl Actions for Teacher { fn get_name(&self) -> String { self.name.clone() } fn bye(&self) { println!("Byebye from {}", self.get_name()); } }
fn main() { let jack = Student{ name: "Jack".to_string(), age: 18, grade: 13}; let ada = Teacher{ name: "Ada".to_string(), age: 32, subject: "math".to_string()}; jack.greet(); ada.greet(); jack.bye(); ada.bye(); }
|
很好,程序能正常跑,说明我们的对象都有正确定义和初始化,现在我们就可以根据上述举例作以下简单的总结:
- 特征 Trait 可以应用于多个结构体,提高了灵活度。
- 特征 Trait 虽然有 self,但是 self 只能指向 trait 内函数,即 self.get_name() 是不能用 self.name 替换的。
- 特征 Trait 中的方法可以被覆盖修改,方便实现更个性化的操作。
当然 方法与特征 还有很多内容,本文并不会一一列举,详见:
Rust 语言圣经之方法 Method
Rust 语言圣经之特征 Trait
Rust 程序设计语言之面向对象特性
所有权与借用
现在让我们假设有两个学生,他们姓名一致,年龄不同,尝试初始化这样子的两个结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #[derive(Debug)] struct Student { pub name: String, pub age: u8 }
impl Student { fn print_info(&self) { println!("name: {}, age: {}", self.name, self.age); } }
fn main() { let normal_name = "Jake".to_string();
let student1: Student = Student { name: normal_name, age: 16}; let student2: Student = Student { name: normal_name, age: 18};
student1.print_info(); student2.print_info(); }
|
在 VSCode 环境下配合 rust-analyser 很容易发现 student2
会被标红且生成如下提示,即 normal_name
这个变量的所有权在 student1
初始化后是被 student1
所拥有的,student2
不可以使用它。
1
| use of moved value: `normal_name` value used here after move
|
我们可以通过修改变量类型为引用类型使其正常运行。
1 2 3 4 5 6 7
| let normal_name = "Jake"; let student1: Student = Student { name: normal_name.to_string(), age: 16}; let student2: Student = Student { name: normal_name.to_string(), age: 18};
name: Jake, age: 16 name: Jake, age: 18
|
这看上去似乎很麻烦,尤其是结构体初始化过程中值很容易出现多处使用的情况,rust 又为什么非要用这么麻烦的机制来限制变量?
简单来说就是保证了内存安全。
熟悉 c/c++ ,尤其是写嵌入式代码的,对指针应该是又爱又恨:爱它的方便,直接访问内存地址甚至可以实现很多神奇操作,比如 bootloader;恨它太灵活,指针的灵活使用很容易造成悬空引用或内存泄漏等问题。
而 rust 则通过所有权机制在编译阶段就对内存安全进行检查,若发现无法保证内存安全就不能通过编译,大大降低了运行中可能发生的内存不安全相关问题。
当然我们这里也仅仅是对所有权与借用进行简单的描述,有关的更多详细介绍可以看看这篇文章 Rust’s Ownership and Borrowing Enforce Memory Safety。
生命周期
上面的例子里结构体能用是是能用了,但是用起来实在不太美观,反正 “Jake” 是 &str 类型,何不直接定义 name 为 &str 类型?好想法!让我们来实践一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #[derive(Debug)] struct Student { pub name: &str, pub age: u8 }
impl Student { fn print_info(&self) { println!("name: {}, age: {}", self.name, self.age); } } fn main() { let normal_name = "Jake";
let student1: Student = Student { name: normal_name, age: 16}; let student2: Student = Student { name: normal_name, age: 18};
student1.print_info(); student2.print_info(); }
|
1 2 3 4 5 6 7 8 9 10 11 12
| // output error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | pub name: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 2 ~ struct Student<'a> { 3 ~ pub name: &'a str, |
|
发现并不可以,提示我们结构体中的引用类型需要的是被生命周期修饰的参数,根据编译器提示进行修改,最终结构体长这样了,此时即可顺利通过编译,正常运行输出。
1 2 3 4 5 6 7 8 9 10 11
| #[derive(Debug)] struct Student <'a>{ pub name: &'a str, pub age: u8 }
impl<'a> Student <'a>{ fn print_info(&self) { println!("name: {}, age: {}", self.name, self.age); } }
|
可是这个 ‘a
是个什么东西?这样的结构体和之前的功能上有什么区别,哪里又体现了生命周期?
简单来说 ‘a
就是个注解,告诉编译器这里是个带生命周期的参数,整个结构体使用相同的 ‘a 也意味着整个结构体的生命周期都是 ‘a
代表的生命周期。
而这样的结构体和之前的功能上的区别是:没什么区别,也没怎么体现生命周期,但当你需要一个不需要进行数据所有权转移和拷贝的结构体,使得结构体中的原始数据可以被方便地在外界被访问,带生命周期的结构体就是个不二选择。
现在让我们来看看一个更通用的例子,两数比较找最大。
1 2 3 4 5 6 7 8 9 10 11
| fn max_num(x: &u8, y: &u8) -> &u8 { let res = if x > y {x} else {y}; res }
fn main() { let x = 1; let y = 2; let max = max_num(&x, &y); println!("max number: {}", max); }
|
看上去似乎没什么问题,但编译器告诉我们,你错了,怎么能没有生命周期注解呢?
1 2 3 4 5 6 7 8 9 10 11 12 13
| error[E0106]: missing lifetime specifier --> src/main.rs:1:31 | 1 | fn max_num(x: &u8, y: &u8) -> &u8 { | --- --- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` help: consider introducing a named lifetime parameter | 1 | fn max_num<'a>(x: &'a u8, y: &'a u8) -> &'a u8 { | ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
|
看到这里想必多多少少有点懵,我就引用下,借点东西,怎么还非要上生命周期了。
其实换个说法粗浅点讲可能比较容易理解:指针是可能在被使用前被提前释放的,c++ 信任你,不做任何限制,但rust 不这样,必须得先解决了可能存在的安全隐患才能允许编译。
当然粗浅理解就是粗浅理解,想深入理解的话可以看看这篇文章 细说 rust 生命周期参数。
根据经验来说,一般作者是很不希望碰生命周期的,因为它带来的麻烦将远大于可能带来的便利。
数据类型及泛型
这部分就属于孰能生巧了,在此不多赘述,仅介绍大致、常用的一些类型,更多相关知识可见 数据类型 及 泛型数据类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| name: String family_name: str gender: char
age: u8 cash: f32 is_single: bool
location: (str, u8, bool) location2: [] location3: Vec<bool>
struct Point<T> { x: T, y: T }
|
结语
当我们了解了上述内容,我们就可以开始接触 Tauri 相关的知识了,在下一章我们将要学习到的是有关前后端通信的内容,其中就会涉及到本章所提及的许多内容,希望到时候大家都能觉得本章打下的基础都十分有用。