rust_learn's People
rust_learn's Issues
2.5 流程控制
if else
fn main() {
if condition == true {
// A...
} else {
// B...
}
}
注意点:
if
语句块是表达式,这里我们使用if
表达式的返回值来给number
进行赋值:number
的值是5
- 用
if
来赋值时,要保证每个分支返回的类型一样(事实上,这种说法不完全准确,见这里),此处返回的5
和6
就是同一个类型,如果返回类型不一致就会报错
循环控制
for 循环
fn main() {
for i in 1..=5 {
println!("{}", i);
}
}
以上代码循环输出一个从 1 到 5 的序列,for 元素 in 集合
。在使用 for 时通常使用集合的引用形式,如果不使用引用的话,所有权会被转移到 for 语句块中,后续无法再使用此集合。
对于实现了
copy
特征的数组(例如 [i32; 10] )而言,for item in arr
并不会把arr
的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用arr
。
如果在循中想要修改元素,可以使用 mut 关键字:
fn main() {
for item in &mut collection {
// ...
}
}
for item in collection
等价于for item in IntoIterator::into_iter(collection)
for item in &collection
等价于for item in collection.iter()
for item in &mut collection
等价于for item in collection.iter_mut()
如果想在循环中获取元素的索引:
fn main() {
let a = [4,3,2,1];
for (i,v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v);
}
}
不声明变量的循环:
fn main() {
for _ in 0..10 {
// ...
}
}
continue
使用 continue
可以跳过当前当次的循环,开始下次的循环:
break
使用 break
可以直接跳出当前整个循环
while 循环
当条件为 true
时,继续循环,条件为 false
,跳出循环。
loop 循环
loop
就是一个简单的无限循环,你可以在内部实现逻辑通过 break
关键字来控制循环何时结束。
- break 可以单独使用,也可以带一个返回值,有些类似 return
- loop 是一个表达式,因此可以返回一个值
fn main() {
let counter = 0;
let res = loop {
counter += 1;
if counter === 10 {
break counter * 2;
}
}
}
2.7 方法
定义方法
在其他的语言中,方法往往存在于 Class 中,但是在 Rust 中,对象的定义和方法的定义是分离的。方法往往和结构体,枚举,特征一起使用,在Rust中 使用 impl
来定义方法:
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
// new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
// 这种方法往往用于初始化当前结构体的实例
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x,
y,
radius
}
}
// Circle的方法,&self表示借用当前的Circle结构体
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
impl
和结构体会自动关联
#[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 };
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
impl Rectangle
表明为 Rectangle
实现方法,这样的写法表示 impl
语句块中的一切都是跟 Rectangle
相关联的。
self, &self 和 &mut self
在 impl
内部,我们使用 &self
来替代本来的参数名和类型, &self
实际是 self: &Self
的简写,在一个 impl
块内,Self
指代被实现方法的结构体类型,self
指代此类型的实例。
需要注意的是,self
依然有所有权的概念:
self
表示Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对Rectangle
的不可变借用&mut self
表示可变借用
方法名和结构体字段名相同
在 Rust 中,允许方法名跟结构体的字段名相同,我们使用 ()
调用是,调用的是它的方法,使用 .
访问时,访问的是它的字段。
这样可以让我们实现一个 getter
, 可以将字段设置为私有字段,而使用方法来访问,当用户尝试访问字段时,就会报错。
关联函数
在 impl
中且没有 self
的函数被称之为关联函数,关联函数没有 self
不能使用 .
调用,只能使用 ::
来调用,例如经常使用的 String::from()
,这些方法一般用来做构造器。
关联函数类似于静态方法,方法类似于成员方法。
多个 impl 定义
Rust 中可以为一个结构体定义多个 impl
块,可以提供更多的灵活性和代码组织性。
为枚举实现方法
枚举也可以像结构体一样实现方法
enum Message {
Quit,
Move {x: i32, y: i32},
Write(String),
ChangeColor(i32, i32, i32)
}
impl Message {
fn call(&self) {
}
}
fn main() {
let m = Message::Wirte(String::from("hello"));
m.call();
}
2.1 变量绑定和解构
变量绑定
使用 let a = "hello world"
来绑定变量,将内存对象的所有权交给一个变量。
所有权 :所有的内存对象都是有主人的,就像现实世界的物品那样。
变量可变性
变量的值默认不能改变。但是可以通过 mut
关键字让变量变为可变变量。
fn main {
let mut x = 5;
x = 6;
}
忽略未使用的变量
未使用的变量需要通过带有 _
前缀的变量名来创建。
fn main {
let _y = 6;
let x = 5; // 会报错
}
变量解构
fn main() {
let (a,mut b):(boolean,boolean) = (true,false);
// a = true; b = false
println!("a = {:?}, b = {:?}", a, b);
}
解构赋值
struct Struct { e: i32 }
fn main() {
let (a,b,c,d,e);
(a,b) = (1,2);
[c,...,d,_] = [1,2,3,4,5];
Struct { e, .. } = Struct { e: 5 };
}
常量
常量使用 const
关键字来定义,不能使用 mut
,从始至终都不可改变,而且常量的值必须标注类型。
fn main() {
const MAX_POINTS: u32 = 100_000;
}
变量遮蔽
Rust 中可以声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的。
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2; // 12
println!({},x);
}
println!({},x); // 6
}
2.8 泛型和特征
泛型
泛型的出现是为了支持函数的多态,比如对于数的加法来讲,在不支持泛型的语言中,需要为每个数字类型单独编写函数。但是支持泛型后,就可以简化代码,用一个函数支持不同的类型
fn add<T> (a:T,b:T) -> T {
a + b
}
fn main() {
add(20,20);
add(1.2,1.2);
}
泛型详解
T
是泛型参数,他的名称不受限制,使用泛型参数前,必须先对其进行声明。例如我们有一个方法,是在列表中找出最大的值,这个函数接受一个 元素为T
的数组切片,它的返回值也是 T
。
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item
}
}
largest
}
但是并不是所有的类型都是可以使用 >
来进行比较的,所以我们需要使用 特征 来为泛型加上类型限制。
在结构体内使用泛型
大体和 TS 类似
struct Point<T,U> {
x: T,
y: U,
}
在枚举中使用泛型
fn main() {
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
}
在方法中使用泛系
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
在方法中使用泛型,需要提前在 impl<T>
声明,后面的 Point<T>
不再是泛型声明,而是一个完整的结构体类型。
除了结构体泛型外,我们还能为结构体的方法定义额外的泛型。
为具体的泛型实现方法
对于 Point<T>
泛型,我们可以为具体的类型定义方法,例如下面的方法,当泛型为 i32 类型时会有一个名为 distance_from_origin
的方法,而其他类型的 Point
则不具备这个方法。
impl Ponit<i32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
const 泛型
对于数组来说,[i32, 2]
和 [i32, 3]
是完全不同的数组类型。如果我们编写一个针对数组的函数,可以通过数组的不可变切片引用来实现。但是有一种针对值的 const 泛型,可以处理数组的长度问题。
fn get_last<T: Copy, const N: usize>(arr: &[T; N]) -> T {
let len = arr.len();
arr[len-1]
}
fn main() {
let arr: [i32; 2] = [3; 2];
get_last(&arr);
}
const 泛型表达式
fn something<T>(val: T) where Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue,
{
//
}
2.4 复合类型
复合类型
复合类型是由其他类型组合而成的,例如结构体 struct
和枚举 enum
。
字符串
fn main() {
let my_name = "Pascal";
greet(my_name);
}
fn greet(name: String) {
println!("Hello, {}!", name);
}
上面的这段代码,在编译时会报错,greet 函数需要一个 String
类型的参数,却传递了一个 &str
类型。
error[E0308]: mismatched types
--> src/main.rs:3:11
|
3 | greet(my_name);
| ^^^^^^^
| |
| expected struct `std::string::String`, found `&str`
| help: try using a conversion method: `my_name.to_string()`
error: aborting due to previous error
切片(slice)
切片允许我们使用集合中连续的元素序列,而不是引用整个集合。而字符串就是切片对 String
类型集合中的某一部分的引用。类似于:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
可以通过 [开始索引..终止索引] 的方式创建()一个切片,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间
。
一些特殊的截取规则:
fn main() {
let s = String::from("hello");
// 省略索引 0
let s1 = &s[..3]; // 'hel'
// 截取到最后一个字节
let s2 = &s[3..]; // 'lo'
// 或
let len = s.len();
let s3 = &s[3..len];
// 截取完整字节
let s4 = &s[0..len];
// 或
let s5 = &s[..];
}
但切片在处理 utf-8 字符串时,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,否则代码会崩溃。
字符串切片的类型标识符是 &str
,let a = [1,3,4,5]
这样的数组的切片类型是 &[i32]
。
字符串字面量切片
字符串字面量的类型是字符串切片 &str
,&str
是一个不可变引用。
Rust 中的字符串
字符串是由字符组成的连续集合,但是在上一节中我们提到过,Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。
Rust 在语言级别,只有一种字符串类型: str
,它通常是以引用类型出现 &str
,也就是字符串切片。
str
类型是硬编码进可执行文件,也无法被修改,但是 String
则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码。
String 和 &str 的转换
String
转&str
:- `String::form("hello,world")
"hello,world".to_string()
&str
转String
: 使用引用fn main() { let s = String::from("hello,world"); say_hello(&s); say_hello(&s[..]); say_hello(s.as_str()); } fn say_hello(s: &str) { println!("{}",s); }
字符串索引访问
Rust 中无法通过索引访问字符串。
字符串切片
字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此无法保证索引的字节刚好落在字符的边界上。
字符串操作
插入
insert()
:接受两个参数,插入索引的位置,插入的字符。insert_str()
:接受两个参数,插入索引的位置,插入的字符串。
这两个方法都会修改原字符串,因此需要可变变量才能调用。
替换
replace()
: 接受两个参数*,第一个是要被替换的字符串,第二个是新的字符串replacen()
:接收三个参数,前两个参数与replace()
方法一样,第三个参数则表示替换的个数。
这两个方法适用于
String
和&str
类型,返回一个新的字符串而不是操作原来的字符串。
replace_range()
:接收两个参数,第一个参数是要替换字符串的范围(一个range,..
表示),第二个参数是新的字符串。
这个方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用
mut
关键字修饰。
删除
pop()
: 删除并返回字符串的最后一个字符,直接修改原来的字符串。remove()
:删除并返回指定位置的字符,直接修改原来的字符串。truncate()
:删除字符串中从指定位置开始到结尾的全部字符,直接修改原来的字符串。clear()
:清空字符串,直接修改原来的字符串。
连接
+
或+=
:要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用+
的操作符时,相当于调用了std::string
标准库中的add()
方法,这里add()
方法的第二个参数是一个引用的类型。因此我们在使用+
, 必须传递切片引用类型。不能直接传递String
类型。+
和+=
都是返回一个新的字符串。所以变量声明可以不需要mut
关键字修饰。- 使用
format!
连接字符串:适用于String
和&str
。format!
的用法与print!
的用类似。
字符串转义
通过\
转义字符串,输出 ASCII 和 Unicode 字符。
操作 UTF-8 字符串
字符
以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars
方法
fn main() {
for c in "**人".chars() {
println!("{}", c);
}
}
字节
使用 bytes
方法可以返回字符串的底层字节数组表现形式
fn main() {
for b in "**人".bytes() {
println!("{}", b);
}
}
获取子串
标准库无法完成这个操作,crates.io
寻找第三方库支持。
2.2 基本类型
基本类型
Rust 中的类型分为基本类型和复合类型,基本类型往往是一个最小化原子类型,无法结构为其他类型。
- 数值类型
- 有符号整数:
i8
,i16
,i32
,i64
,isize
- 无符号整数:
u8
,u16
,u32
,u64
,usize
- 浮点数:
f32
,f64
- 有理数
- 复数
- 有符号整数:
- 字符串:字符串字面量和字符串切片
&str
- 布尔类型:
true
和false
- 字符类型:表示单个 Unicode 字符,存储为 4 个字节
- 单元类型:即
()
,其唯一的值也是()
类型推导与标注
Rust 是一门静态类型语言,编译器在编译期必须知道所有变量的类型,但 Rust 编译器可以根据变量的值和上下文中的使用方法来推导出变量的类型,但是在某些情况下无法推导出变量类型,需要我们手动标注类型。
数值类型
整数类型
整数类型的定义形式为: 有无符号 + 类型大小(位数)。有符号数字为 i (integer),无符号数字为 (unsigned)。
有符号数字的数字范围为 -(2^(n - 1)) ~ 2^(n - 1) - 1,无符号数字的范围为 0 ~ 2^(n - 1)。
i8
类型的范围为 -2^7 ~ 2^7 - 1 ,u8
类型的范围为 0 ~ 2^7 - 1 。
isize
和 usize
则视 CPU 架构而定,分为 32 位或 64 位。
Rust 的整数默认使用
i32
,isize
和usize
主要的应用场景是用作集合的索引。
整型溢出
假设有一个 u8
,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)。
在当使用 --release
参数进行 release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two’s complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8
的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。
要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
- 使用
wrapping_*
方法在所有模式下都按照补码循环溢出规则处理,例如wrapping_add
- 如果使用
checked_*
方法时发生溢出,则返回None
值 - 使用
overflowing_*
方法返回该值和一个指示是否存在溢出的布尔值 - 使用
saturating_*
方法使值达到最小值或最大值
fn main() {
let a : u8 = 255;
let b = a.wrapping_add(20);
println!("{}", b); // 19
}
浮点类型
浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32
和 f64
,分别为 32 位和 64 位大小。默认浮点类型是 f64
,在现代的 CPU 中它的速度与 f32
几乎相同,但精度更高。
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数根据 IEEE-754
标准实现。f32
类型是单精度浮点型,f64
为双精度。
序列
序列通过一个简洁的方式用来生成连续数值,例如 1..5
会生成 1 到 4 不包含 5 的连续数字,1..=5
生成从 1 到 5 的连续数字。
序列还可以用于字符类型,用来生成连续的字符。
fn main() {
for i in 1..=5 {
println!("{}",i);
}
for i in 'a'..='c' {
println!("{}",i);
}
}
// 1
// 2
// 3
// 4
// 5
// 'a'
// 'b'
// 'c'
字符
所有的 Unicode
值都可以作为 Rust 的字符,由于 Unicode
都是 4 个字节编码,因此字符类型也是占用 4 个字节。
Rust 的字符只能用 ''
来表示。
布尔
Rust 中的布尔类型包含 true
和 false
,占用内存大小为 1 字节。
使用布尔类型的场景主要在于流程控制。
单元类型
单元类型就是 ()
,main
函数的返回值就是单元类型,可以作为一个值用来占位,但是完全不占用任何内存。
语句和表达式
Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值。
fn main() {
fn add_with_extra(x: i32, y: i32) -> i32 {
let x = x + 1; // 语句
let y = y + 5; // 语句
x + y // 表达式
}
}
语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。
对于 Rust 语言而言,这种基于语句(statement)和表达式(expression)的方式是非常重要的,你需要能明确的区分这两个概念, 但是对于很多其它语言而言,这两个往往无需区分。基于表达式是函数式语言的重要特征,表达式总要返回值。
语句
fn main() {
let a = 8;
let b: Vec<f64> = Vec::new();
let (a, c) = ("hi", false);
}
以上都是语句,它们完成了一个具体的操作,但是并没有返回值,因此是语句。因为语句没有返回值,所以不会给其他变量赋值。
表达式
表达式会进行求值,然后返回一个值。例如 5 + 6
,在求值后,返回值 11
,因此它就是一条表达式。调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
}
表达式不能以分号结尾
如果表达式不返回任何值,则会隐式返回一个()
fn main() {
assert_eq!(ret_unit_type(), ())
}
fn ret_unit_type() {
let x = 1;
// if 语句块也是一个表达式,因此可以用于赋值,也可以直接返回
// 类似三元运算符,在Rust里我们可以这样写
let y = if x % 2 == 1 {
"odd"
} else {
"even"
};
// 或者写成一行
let z = if x % 2 == 1 { "odd" } else { "even" };
}
函数
Rust 的函数使用以下格式表示:
fn main() {
fn add(i: i32, j: i32) -> i32 {
i + j
}
}
函数要点
- 函数名和变量名使用 蛇形命名(小写 + 下划线分割)
- 函数的位置不重要
- 每个函数的参数都需要标注类型
返回值
函数的返回值就是最后一行表达式(不带 ;
)的结果,函数的返回值也可以用于赋值。
- 返回
()
单元类型- 用来表示函数没有返回值
- 通过
;
结尾返回一个()
- 永不返回的发散函数,这种语法往往用作会导致程序崩溃的函数
fn dead_end() -> ! {
}
2.9 动态数组
动态数组
动态数组使用 Vec<T>
表示,它允许储存多个相同类型的值,如果需要储存不同类型的值,需要使用枚举类型或特征对象。
创建动态数组
在 Rust 中,有多种方式可以创建动态数组。
Vec::new
使用 Vec
类型中的关联函数可以声明一个动态数组。
let v: Vec<i32> = Vec::new();
上面代码中我们显示声明了 v
的类型,因为Rust 编译器无法从 Vec::new()
中得到任何关于类型的暗示信息,因此也无法推导出 v
的具体类型。
let mut v = Vec::new();
v.push(1);
当我们向 v
中增加一个元素时,编译器可以推测出 v
中的元素类型是 i32
,因此推导出的 v
的类型是 Vec<i32>
。
Vec![]
宏
使用 Vec![]
宏创建数组,可以在创建的同时给予初始化值:
let v = vec![1,2,3];
更新 Vector
向数组尾部添加元素可以使用 push
方法,而且必须将实例声明为 mut
后才能进行修改。
Vector
作用域
Vector
元素在超出作用域范围后,会被自动删除。它内部的元素也会被自动删除,但是当 Vector 被引用后情况会有所不同。
从 Vector
中读取元素
从 Vector
中读取元素可以使用 下标索引 和 get
方法两种方法。
let v = vec![1,2,3,4];
let third = v[2];
let third = match v.get(2) {
Some(third) => third,
None => None
}
下标索引和 get 方法的区别
- 下标索引直接返回值,而
get
方法返回一个Option<&T>
需要通过模式匹配得到值。 get
方法比较安全,当发生数组越界的情况时,我们拿到的值是一个None
。- 下标索引比较简洁,发生数组越界的情况时,程序会发生崩溃。
当我们能确保我们的访问不会发生越界时,就使用 下标索引,否则就使用 get 方法。
同时借用多个数组元素
fn main() {
let mut v = vec![1,2,3,4];
let first = &v[0];
v.push(5);
println!("first element is {}",{})
}
在第 3 行代码时,我们创建了对 v
的一个不可变引用,第 4 行我们使用 v.push
可变引用。根据借用规则,不可变引用在可变引用之后被使用了,同一个作用域不能同时存在对一个变量的可变引用和不可变引用,这里会出现一个编译器错误。
原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。
遍历 Vector
中的元素
使用迭代去访问数组元素比直接使用下标更加安全和高效(每次访问都会检查数组边界)。
let v = vec![1,2,3];
for i in v {
println("{}",i)
}
在迭代过程过修改 动态数组
let mut v = [1,2,3];
for i in &mut v {
*i += 10;
}
储存不同类型的元素
我们知道动态数组中只能储存相同类型的元素,但是通过枚举或者特征类型就能实现在动态数组中储存不同类型的元素。
使用枚举
struct Ipv4Address(String)
struct Ipv6Address(String)
enum NetworkAddress {
Ipv4Address,
Ipv6Address
}
let v= vec![
NetworkAddress::Ipv4Address(String::from("...")),
NetworkAddress::Ipv6Address(String::from("...")),
]
使用特征对象
trait IpAddr {
fn display(&self);
}
struct Ipv4Address(String);
struct Ipv6Address(String);
impl IpAddr for Ipv4Address {
fn display(&self) {
println("{}",self.0);
}
}
impl IpAddr for Ipv6Address {
fn display(&self) {
println("{}",self.0);
}
}
fn main() {
let v:Vec[Box<dyn IpAddr>] = vec![
Box::new(Ipv4Address(String::from("...")),
Box::new(Ipv6Address(String::from("...")),
];
}
特征对象数组在实际场景中使用比枚举数组更加频繁,主要是特征对象比较灵活,而编译器对枚举类型限制比较多。
2.3 所有权和借用
所有权
所有权是编译器在编译时根据一系列规则来进行检查的一种内存管理方式。这种检查只发生在编译期,对于程序运行期不会有性能上的损失。
堆和栈
栈
栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。增加数据叫做进栈,移出数据则叫做出栈。
因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。
堆
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
性能区别
写入: 栈的空间已知,入栈时无需分配新的空间,只需要将数据放入栈顶。所以栈的写入速度更快。
读取: 栈一般位于CPU 高速缓存中,而堆数据只能存储在内存中,访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。
处理器处理和分配在栈上数据会比在堆上的数据更加高效。
所有权原则
- Rust 中的每一个值都被一个变量所拥有,这个变量称为值的所有者。
- 一个值同时只能被一个变量所拥有,即一个值只能拥有一个所有者。
- 当所有者离开作用域范围时,这个值将被丢弃。
变量作用域
变量从声明的点开始直到声明的作用域结束都是有效的。
fn main {
{ // s 尚未被声明,无效
let s = "hello"; // s 有效
println!({},s);
} // 作用域结束,无效
}
数据的拷贝
栈数据
对于储存在栈上的数据,直接使用拷贝的方式来赋值。
堆数据
对于堆数据,例如 String
类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成。其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存。容量是堆内存分配空间的大小,长度是目前已经使用的大小。
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}
我们已经知道当 Rust 中的变量在作用域结束时会被释放,那么当离开作用域时 s1 和 s2 都会被释放,如果同一个值拥有两个所有者,它就会被2次释放,会导致内存污染,造成潜在的安全漏洞。
在 Rust 中:当 s1
赋予 s2
后,Rust 认为 s1
不再有效,因此也无需在 s1
离开作用域后 drop
任何东西,这就是把所有权从 s1
转移给了 s2
,s1
在被赋予 s2
后就马上失效了。即当所有权转移后,对 s1
的使用也将无效。
![[Pasted image 20230112174414.png]]
对于 s1
和 s2
来说,赋值的过程 s2
拷贝了 s1
的长度和容量,移动了 s1
的数据指针到 s2
,这并不是深拷贝或浅拷贝。
深拷贝
Rust 永远也不会自动创建数据的“深拷贝”,因此任何自动的复制都是移动,对运行时的性能影响较少,当我们确实需要深拷贝时,使用 clone
方法。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
浅拷贝
浅拷贝只发生在栈上,也就是说所有的除深拷贝之外的拷贝都是浅拷贝,即使是基本类型。
但是因为像整形这样的基本类型在编译时已知大小,所以拷贝是很快速的。
Rust 有一个叫做 Copy
的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用。
那么什么类型是可 Copy
的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: 任何基本类型的组合可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的。如下是一些 Copy
的类型:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都是
Copy
的时候。比如,(i32, i32)
是Copy
的,但(i32, String)
就不是。 - 不可变引用
&T
,例如转移所有权中的最后一个例子,但是注意: 可变引用&mut T
是不可以 Copy的
函数和所有权
向函数传递参数会移交所有权,函数的返回值也有其所有权。
2.6 模式匹配
match 和 if let
enum Direction {
East,
West,
North,
South,
}
fn main() {
let dire = Direction::South;
match dire {
Direction::East => println!("East"),
Direction::North | Direction::South => {
println!("South or North");
},
_ => println!("West"),
};
}
match
跟其他语言中的 switch
非常像,_
类似于 switch
中的 default
:
match
的匹配必须要穷举出所有可能,因此这里用_
来代表未列出的所有可能性match
的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同- X | Y,类似逻辑运算符
或
,代表该分支可以匹配X
也可以匹配Y
,只要满足一个即可
使用 match
使用 match 表达式赋值
match 本身也是一个表达式,所以可以通过 match 完成赋值操作。
enum IpAddr {
Ipv4,
Ipv6
}
fn main() {
let ip1 = IpAddr::Ipv6;
let ip_str = match ip1 {
IpAddr::Ipv4 => "127.0.0.1",
_ => "::1",
};
println!("{}", ip_str);
}
模式匹配
模式匹配的另外一个重要功能是从模式中取出绑定的值,比如扑克牌的例子:
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(u8),
Hearts(u8),
}
fn get_poker_num(c: PokerCard) -> u8 {
match c {
PokerCard::Clubs(num) =>num,
PokerCard::Spades(num) =>num,
PokerCard::Diamonds(num) =>num,
PokerCard::Hearts(num) =>num,
}
}
穷尽匹配
在使用 match 时匹配必须穷尽所有的情况。
_
通配符
当我们不想在匹配时列出所有值的时候,可以使用 Rust 提供的一个特殊模式,使用特殊的模式 _
替代。
if let 匹配
只有一个模式的值需要被处理,其他的值都直接忽略的情况下,match 写起来就很多了。这时候我们用 if let
的方式就可以实现:
let v = Some(38);
// 使用 match
match c {
Some(3) => println!("three"),
_ => (),
}
// 使用 if let
if let Some(3) = v {
println!("three")
}
matches! 宏
Rust 标准库中提供了一个非常实用的宏:matches!
,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true
or false
。
enum MyEnum {
Foo,
Bar
}
fn main() {
let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
}
// v.iter().filter(|x| x == MyEnum::Foo); 报错,无法将 x 直接于枚举成员比较
v.iter().filter(|x| matches!(x, MyEnum::Foo)); // 使用宏
变量覆盖
无论是 match
还是 if let
,他们都可以在模式匹配时覆盖掉老的值,绑定新的值:
fn main() {
let age = Some(30);
println!("在匹配前,age是{:?}",age);
if let Some(age) = age {
println!("匹配出来的age是{}",age);
}
println!("在匹配后,age是{:?}",age);
}
解构 Option
对于使用 Option
枚举来表示变量中是否有值的情况,我们需要从一个 Option
类型中取出 T 值,但是会存在没有值的情况。
enum Option<T> {
Some(T),
None,
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.