Personal Rust learning use, from Rust Course.
暂时不用的变量可以在变量名前加 _
以规避编译器提醒,或者在函数前添加 #[allow(unused_variables)]
。
结构式复制和模式匹配:
struct Struct {
e: i32
}
fn main(){
let (mut a, b, c, d, e);
(a, b) = (1, 2);
// _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5 };
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}
Rust 没有隐式类型转换,只能显示的进行转换。例如如果我们想要进行 f64
类型数据和 i32
类型数据的加法,可以通过以下两种方式进行:
let x = 5;
let y = 0.1;
println!("{}", y + x as f64)
println!("{}", y + f64::from(x))
在下面这个变量遮蔽(shadowing)的例子中,两个 spaces 的类型不一样,这也是无法使用 mut
变量进行相同操作的原因和遮蔽的方便之处。
// 字符串类型
let spaces = " ";
// usize数值类型
let spaces = spaces.len();
main
函数和 println!()
不是无返回值,而是返回单元类型 ()
,作为一个值用来占位,但是完全不占用任何内存。无返回值在 Rust 中被定义为 发散函数(diverge function)
。单元类型 ()
也可以作为 map
的 value,表示只关心 key。
- 有返回值的就是表达式(expression),如果没有返回值会隐式返回一个
()
- 表达式后加上分号就变成了一条语句(statement),没有返回值。
let x = 1
和x += 2
都是一条语句 - 使用语句块赋值的时候,记得
{}
后的分号,因为这仍然是一条语句
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
有关函数
- 函数是一种表达式
- 函数的返回值就是函数体最后一条表达式的返回值,也可以提前用
return
返回 - 函数没有返回值或通过
;
结尾,那么返回一个()
,其他时候必须要指明返回类型 - 使用
!
作为函数返回类型,表示该函数永不返回(diverge function)
fn clear(text: &mut String) -> () {
*text = String::from("");
}
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
//todo!();
//unimplemented!()
}
- 基本类型(包括指针不可变引用
&T
)存储在栈上,并通过自动拷贝的方式来赋值,实现了copy trait
- 对于大小未知或者可能变化的数据,将在堆上分配内存存储,并返回一个表示位置地址的指针
- 处理器处理和分配在栈上数据会比在堆上的数据更加高效
- 当所有权转移时,可变性也可以随之改变,如
let mut s1 = s
// copy trait
let x = 5; // 变量绑定
let y = x; // 浅拷贝
let x: &str = "hello, world";
let y = x; // 引用拷贝,x y 引用同一个字符串
// drop trait ,将 String 的所有权从 s1 转交给 s2 ,s1 失效
let s1 = String::from("hello");
let s2 = s1; // move
println!("{}, world!", s1); // error, value used after move
上面这个堆上字符串所有权转换的过程类似浅拷贝 + 令原变量失效,被称为移动(move)。Rust 永远也不会自动创建数据的深拷贝,任何自动的复制都不是深拷贝。深拷贝可以使用 s2 = s1.clone()
方法。
如果要在堆中分配原来在栈上分配的变量,需要用 BOX 来构造。
部分 move:当解构一个变量时,可以同时使用 move 和引用模式绑定的方式,变量中一部分的所有权被转移给其它变量,而另一部分我们获取了它的引用。
fn main() {
#[derive(Debug)]
struct Person {
name: String,
age: Box<u8>,
}
let person = Person {
name: String::from("Alice"),
age: Box::new(20),
};
// 通过这种解构式模式匹配,person.name 的所有权被转移给新的变量 `name`
// 但是,这里 `age` 变量却是对 person.age 的引用, 这里 ref 的使用相当于: let age = &person.age
let Person { name, ref age } = person;
println!("The person's age is {}", age);
println!("The person's name is {}", name);
// Error! 原因是 person 的一部分已经被转移了所有权,因此我们无法再使用它
//println!("The person struct is {:?}", person);
// 虽然 `person` 作为一个整体无法再被使用,但是 `person.age` 依然可以使用
println!("The person's age from person struct is {}", person.age);
}
将值传递给函数,一样会发生 move
或者 copy
。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里,所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到takes_and_gives_back 中,它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
不断转交所有权比较麻烦,可以使用引用解决。
- 常规引用是一个指针类型,指向了对象存储的内存地址,允许使用值但是不获取所有权
- 使用解引用运算符
*
解出引用所指向的值 - 默认不可变
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生
- 只能对可变对象进行可变引用
- borrow checker:同一作用域下,特定数据只能粗在一个可变引用,避免数据竞争
- 可变引用与不可变引用不能同时存在
fn main() {
let mut s = String::from("hello"); // 必须 mut 且为 String,&str 没有 push_str 方法
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Non-Lexical Lifetimes(NLL):引用的作用域从创建开始,一直持续到它最后一次使用的地方,和变量的作用域不一样(到某一个 }
)
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中,r1,r2作用域在这里结束
let r3 = &mut s;
println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束
总结
- 同一时刻(引用作用域),你只能拥有要么一个可变引用, 要么任意多个不可变引用
- 引用必须总是有效的(不会出现悬垂引用)
ref
与&
类似,可以用来获取一个值的引用,但是它们的用法有所不同
字符串切片 &str
:对 String
类型中某一部分的引用。如果无法保证索引的字节刚好落在字符的边界上,会引起程序崩溃。
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
其他切片,如数组,也是一样的。如果使用切片时不加 &
会有 size cannot be known 的报错,因为切片的长度无法在编译期得知,因此无法直接使用切片类型。切片类型 [T]
拥有不固定的大小,而切片引用类型 &[T]
则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此 &[T]
更有用。
- 一个切片引用占用了2个字大小的内存空间,字的大小取决于处理器架构(x86-64下8字节)
- 第一个字是指向数据的指针,第二个字是切片的长度。
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
字符串字面量是切片,该切片指向了程序可执行文件中被硬编码的文本(大小未知)的某个点。字符串字面量是不可变的,因为 &str
是一个不可变引用。
let s1 = "hello" // 此处 s 类型为 &str ,是不可变的字符串字面值,存储在?
let mut s2 = String::from("hello"); // 此处 s2 为动态字符串类型,分配在堆上,可以进行修改
s2.push_str(" world!");
- 事实上 String 是一个智能指针,它作为一个结构体存储在栈上,然后指向存储在堆上的字符串底层数据
- 存储在栈上的智能指针结构体由三部分组成:一个指针只指向堆上的字节数组、已分配的容量 capacity、已使用的长度(已使用的长度小于等于已分配的容量,当容量不够时,会重新分配内存空间)
- 不允许
s[0]
这样的字符串索引,和 utf-8 的实现方式以及性能表现有关,可以用切片&s[0..1]
- 变量在离开作用域后,自动调用
drop
释放其占用的内存
和 &str
的转换
fn main(){
let s = "hello".to_string();
let s = String::from("hello,world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str());
}
fn say_hello(s: &str) {
println!("{}",s);
}
用 +
连接字符串的时候,如果想保留第一个字符串的所有权,可以写 s3 = s1.clone() + &s2
&s[..]
和&s
的区别?&str
和&String
的区别?deref trait
raw string
let raw_str = "Escapes don't work here: \x3F \u{211D}";
// modify above line to make it work
assert_eq!(raw_str, "Escapes don't work here: ? ℝ");
// If you need quotes in a raw string, add a pair of #s
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// If you need "# in your string, just use more #s in the delimiter.
// You can use up to 65535 #s.
let delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", delimiter);
// Fill the blank
let long_delimiter = r###"Hello, "##""###;
assert_eq!(long_delimiter, "Hello, \"##\"")
元组:多种类型组合到一起,长度固定,顺序固定。可以用模式匹配或 .
获取值。
fn main() {
let t: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, _) = t;
let z = t.2;
}
struct
- standard
- tuple struct
- unit struct
默认所有字段都是私有的,只对当前文件公开,使用 pub
进行公开。
dbg!
会拿走表达式的所有权,打印出相应的文件名、行号、表达式结果等 debug 信息(在 stderr 中),并把表达式值的所有权返回。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
&self
是 self: &Self
的简写(注意大小写)。在一个 impl
块内,Self
指代被实现方法的结构体类型,self
指代此类型的实例。
self
依然严格遵守所有权规则
self
表示Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对Rectangle
的不可变借用&mut self
表示可变借用
为枚举类、特征也可以实现方法。
pub struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 关联函数:没有 self,但在 impl 中和结构体紧密相连,需要用 :: 调用
pub fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
pub fn width(&self) -> u32 {
return self.width;
}
}
fn main() {
let rect1 = Rectangle::new(30, 50);
println!("{}", rect1.width());
}
fn main() {
// 编译器自动推导出one的类型(根据后面的 arrays)
let one = [1, 2, 3];
// 显式类型标注
let two: [u8; 3] = [1, 2, 3];
let blank1 = [0; 3];
let blank2: [u8; 3] = [0; 3];
// arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3]
let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2];
// 借用arrays的元素用作循环中
for a in &arrays {
print!("{:?}: ", a);
// 将a变成一个迭代器,用于循环
// 你也可以直接用for n in a {}来进行循环
for n in a.iter() {
print!("\t{} + 10 = {}", n, n+10);
}
let mut sum = 0;
// 0..a.len,是一个 Rust 的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1
for i in 0..a.len() {
sum += a[i];
}
println!("\t({:?} = {})", a, sum);
}
}
Rust-Course 的总结:
- 数组类型容易跟数组切片混淆,
[T;n]
描述了一个数组的类型,而[T]
描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]
的形式去描述 [u8; 3]
和[u8; 4]
是不同的类型,数组的长度也是类型的一部分- 在实际开发中,使用最多的是数组切片
[T]
,我们往往通过引用的方式去使用&[T]
,因为后者有固定的类型大小
if
语句块是表达式- 用
if
来赋值时,要保证每个分支返回的类型一样(或结合 for continue)
循环
for _ in 0..=10
到10,执行11次。- 使用
for
时往往使用集合的引用形式,否则所有权将会被转移,除非是实现了copy trait
的数组,如[i32;10]
。 - 如果想在循环中修改元素,使用
mut
关键字: - 直接循环集合中的元素比用下标索引循环的
for
性能更优,因为是连续访问且不用边界检查 loop
= 无限循环的while
let mut counter = 0;
let result = loop { // loop 是一个表达式
counter += 1;
if counter == 10 {
break counter * 2; // 类似 return,赋值给 result
}
};
println!("The result is {}", result);
let mut v = 0;
for i in 1..10 {
v = if i == 9 {
continue
} else {
i
}
}
println!("{}", v); //8
match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
_ => 表达式3
}
match 本身也是一个表达式,因此可以用它来赋值。
option
let v = Some(3u8);
match v {
Some(3) => println!("three"),
_ => (),
}
if let Some(3) = v {
println!("three");
}
类似接口,定义了一个可以被共享的行为。如果要使用一个特征的方法,需要将该特征引入当前的作用域中
孤儿规则:如果要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的。
默认实现允许调用相同特征中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。
pub trait Summary {
fn summarize(&self) -> String; //也可以写一个默认实现
}
pub struct Weibo {
pub username: String,
pub content: String
}
impl Summary for Weibo {
fn summarize(&self) -> String {
format!("{}发表了微博{}", self.username, self.content)
}
}
fn main() {
let weibo = Weibo{username: "sunface".to_string(),content: "好像微博没Tweet好用".to_string()};
println!("{}",weibo.summarize());
}
pub fn notify(item: &(impl Summary + Display)) {
println!("Breaking news! {}", item.summarize());
}
pub fn notify<T: Summary + Display>(item1: &T, item2: &T) {}
// where 约束
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
// 指定类型 + 特征实现方法
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
// 有条件的实现特征,标准库中为任何实现了 Display 特征的类型实现了 ToString 特征
impl<T: Display> ToString for T {
// --snip--
}
被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能,如 #[derive(Debug)]
。
fn returns_summarizable() -> impl Summary {
Weibo {
username: String::from("sunface"),
content: String::from(
"m1 max太厉害了,电脑再也不会卡",
)
}
}
但是此处返回和特征约束后的代码中只能有一个具体的类型,不能返回两个不同的类型。即代码涉及到的元素必须是 Weibo
或其它某一个结构体对象。如果只需要同质(相同类型)集合,这样写性能更好。想要同时存在多个不同的对象,可以使用特征对象(运行时会从 vtable
中查找需要调用的方法)。
Rust 没有继承!
特征对象指向实现了某个特征的类型的实例,映射关系存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。
可以通过 &
引用或者 Box<T>
智能指针的方式来创建特征对象。
trait Draw {
fn draw(&self) -> String;
}
// 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw>
fn draw1(x: Box<dyn Draw>) {
// 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
x.draw();
}
fn draw2(x: &dyn Draw) {
x.draw();
}
draw1
函数的参数是Box<dyn Draw>
形式的特征对象,其可以通过Box::new(x)
的方式创建draw2
函数的参数是&dyn Draw
形式的特征对象,该特征对象是通过&x
的方式创建的dyn
关键字只用在特征对象的类型声明上,在创建时无需使用dyn
因此,可以使用特征对象来代表泛型或具体的类型。
// 实现多个不同 ui 组件的统一渲染
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75, height: 10,
}),
Box::new(Button {
width: 50, height: 10,
}),
],
};
screen.run();
}
dyn
不能单独作为特征对象的定义,因为特征对象可以是任意实现了某个特征的类型,编译器在编译期不知道该类型的大小,不同的类型大小是不同的,而 &dyn
和 Box<dyn>
在编译器都是已知大小的。
不是所有特征都能拥有特征对象,只有对象安全的特征才行:
- 方法的返回类型不能是
Self
- 方法没有任何泛型参数
运行时仍然需要确定实际的具体类型。
- 泛型:静态分发 static dispatch
- 特征对象:动态分发 dynamic dispatch
可以解决悬垂引用 (Dangling References) 的问题
fn main() {
let reference_to_nothing = dangle();
}
// error: expected named lifetime parameter
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
fn no_dangle() -> String {
let s = String::from("hello");
s
} // String 的所有权被转移给外面的调用者。
#![...]
将对整个文件有效, #[...]
只对该行下面的块有效