简介
- 接上回
Rust 的所有权概念
- Rust 的核心特性就是所有权。
- Rust 和大多数语言的垃圾回收机制(内存垃圾回收)不同,它是通过一个所有权系统来管理内存。
- 所有权特性不会减慢程序的运行速度。
- 这部分内容枯燥且重要,切记。
Stack(栈内存) 和 Heap(堆内存)
- Stack和Heap都是你可用的内存,但是他们的结构不同。
- 栈内存运行速度相较于堆内存快的多,因为栈内存是挨着顺序排列的,但是堆内存并不是绝对连续的。
- 存储数据时需要注意,把值压入stack上不叫分配,因为指针是已知的固定大小,可以把指针存放在stack上。
所有权存在的原因
- 跟踪代码的那部分正在使用heap的哪些数据
- 最小化heap上的重复数据
- 清理heap上未使用的数据以避免空间不用。
所有权规则
- 每个值都有一个变量,这个变量就是该值的所有者。
- 每个值同时只能有一个所有者,这和一些传统语言不同,比如C语言,同一个变量可以有多个指针标记。
- 当所有者超出作用域(socpe)时,该值将被删除。
String 类型
- 基础数据类型存放在栈上,但是String类型是存储在堆上的。
- String类型别基础标量数据类型更复杂,这种类型在heap上分类,能够存储在编译时为止的数量
// 可以使用 from 函数从字符串字面值创建出String类型
let s = String::from("hello");
- 这类字符串是可以被修改的,但是字符串字面值是不能被修改的,因为字符串字面值在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件中,速度快、高效也是因为其的不可变性质。
内存和分配
- Rust 采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动交换给操作系统。
- 内存释放是会自动调用 drop函数。
变量和数据交互的方式(Move)移
-
这是Rust 的特点之一,但是对于“栈”变量和“堆”变量还是有区别的,比如如下代码可以看到第一个println是可以执行成功的,但是第二个就会造成编译错误。
- 那么如上的原因是什么呢,因为对于栈数据来说上面代码实际上是完成了一次深copy,他会给x1 对应的值入栈,但是对于string他移动的就是指针了。
就像上面的结构图一样,Rust是不允许这种情况出现的,一旦发生浅拷贝(实际上是Rust的Move)那么变量s2将指向堆位置,s1也就被释放了。
所以Rust 通过 Move 来解决祖先语言的二次释放的问题。
浅拷贝和深拷贝,Rust中的Move 不仅仅是浅拷贝,因为他不仅仅复制了指针,还把之前的数据指针给释放了,这样就保证了他在编译阶段是安全的。
fn main() {
let msg = String::from("Hello");
let said = msg; // 这里赋值后 msg 变量指针就失效了
println!("Stack num x:{} , y:{}", msg, said);
}
- 如上代码编译是无法通过的会报错:
^^^ value borrowed here after move
- 如果要完成字符串的深copy 需要使用clone 方法,举例:
fn main() {
let s1 = String::from("Hello");
let s2 = s1.clone(); // 这样就不会导致 s1 失效了
println!("Num : {},{}", s1, s2); // 会导致失败
}
- 数据的克隆使用 clone 方法,开销相对较大,主要应用于堆数据,对于栈数据则不需要考虑克隆的问题,因为深拷贝和浅拷贝在栈数据上的行为是一致的,可以看一下如下代码:
// 栈数据的copy
fn main() {
let x = 3;
let y = x; // 深浅copy 行为一致
println!("Stack num x:{} , y:{}", x, y);
}
相关概念总结,Copy trait 可以用于像整数这样完全存放在stack上面的类型,如果一个类型实现了 copy 这个 trait,那么旧的变量复制后仍然可以使用比如 i32,如果一个类型或者该类型的一部分实现了 Drop trait 那么Rust 不允许让它再去实现Copy trait了。
一些拥有 Copy trait 的类型(说人话就是:一些具备Copy接口的类型)
1、任何简单标量的组合类型都是可以Copy的
2、任何需要分配内存或某种资源的都不可以Copy的。
3、一些拥有Copy trait类型,整数全部、bool、浮点、char、
4、Tuple,如果 (i32,i32) 是,(i32,String)不是,也就是这个元组一部分没有实现Copy trait。
所有权与函数
- 如果你理解C语言的指针和传引用概念实际上这部分也不难理解,当然这部分也很重要避免以后一脸懵逼。
- 先举一个简单的例子:
// 如下代码是无法通过编译的
fn main() {
let s1 = String::from("Hello");
say(s1);
println!("Variable val: {}", s1);
}
fn say(msg: String) {
println!("Msg : {}", msg);
}
出现这个问题的原因是s1的所有权被Move了,Move到say里面,结果这个函数
终止后超出作用域,就被销毁了为了处理这种方式可以有两种形式,形式1,是通过返回值将所有权在交回去,例如:
fn main() {
let s1 = String::from("Hello");
let s2 = say(s1);
println!("Variable val: {}", s2);
}
fn say(msg: String) -> String{
// println!("Msg : {}", msg);
msg
}
- 第二种方式是把引用传过去,也就是传一个指针过去,而不是真的把变量扔过去,例如:
fn main() {
let s1 = String::from("Hello");
say(&s1); // 这里面也需要使用 &s1 将引用指针明确的传递过去
println!("Variable val: {}", s1);
}
// 注意这里面的参数定义 &String 表示传递引用指针
fn say(msg: &String) {
println!("Msg in fun : {}", msg);
}
引用与借用
-
通过&符合可以把引用传递过去,其实就是类似C语言中指向指针的指针,这时候传递并不是所有权,所以不会销毁原来的变量。
但是这个也有一个比较特殊的地方需要注意,借用的东西默认是不可变的,如果一定要改变借用变量的值同样需要 mnt 关键字进行辅助。
&mnt String
-
这会导致直接报错。
尝试修改一下:
fn main() {
// 变量要声明成 mut 类型
let mut s1 = String::from("Hello");
say(&mut s1); // 带入值也需要传入可变引用地址
println!("Variable val: {}", s1);
}
fn say(msg: &mut String) { // 注意这里声明成 mut 可变
msg.push_str("!!!");
println!("Msg in fun : {}", msg);
}
-
这样是可以的
- 可变引用重要的限制,在特定的作用域内,对某一块数据,只能有一个可变的引用,这样做的好处是在编译时防止数据竞争,以下三种行为下会发生数据竞争:
1、两个或者多个指针同时访问同一个数据。
2、至少有一个指针用于写入数据。
3、没有使用任何机制来同步对数据的访问。
- 多个不变的引用是可以的。
- 另外不可以同时拥有一个可变引用和一个不可变引用。举例来说:
fn main() {
// 变量要声明成 mut 类型
let mut origin_str = String::from("Hello");
let s1 = &origin_str;
let s2 = &origin_str;
let s3=&mut origin_str; // 这就会报错,某一个区块只能有一个可变引用
}
-
单个区块多个可变引用也会报错,违反了某个区块只能有一个可变引用的原则
但是可变引用也并非不能创建多个,可以通过作用域来创建多个可变引用,举例:
// 可以用 {} 创造一个块
fn main() {
// 变量要声明成 mut 类型
let mut origin_str = String::from("Hello");
// let s1 = &origin_str;
// let s2 = &origin_str;
{
let s3=&mut origin_str;
}
let s4=&mut origin_str;
println!("! {} ", s4);
}
悬空引用 Dangling References
Rust 在编译器就可以防止这种“野指针”的存在,如果你引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域。
-
举例来说,下面的截图中函数 dangle 返回字符串引用后,字符串就s就会被回收,此时如果可以编译会导致str变成野指针,但是Rust显然可以防止这种情况的出现
对于引用再做个小节:
1、一个可变的引用。
2、任意数量不可变的引用。
3、引用必须一直有效。
切片
- Rust 另外一种不持有所有权的数据类型,就是切片(slice)
字符串切片
- 字符串切片是指向字符串中一部分内容的引用
fn main() {
let slice_str = "What do you want?";
let s1 = &slice_str[..5];
let s2 = &slice_str[5..8];
println!("{},{}", s1, s2);
}
- 需要注意的问题:
1、字符串切片的范围索引必须发生在有效的UTF8字符边界内。
2、如果尝试从一个多字节的字符串中创建字符串切片,程序会报错并退出。
-
用字符串切片的好处就是,当原始字符串被释放掉后切片也就不可用了,比如下图的情况:
-
行6调用clear() 方法会释放掉 slice_str 从而造成程序错误,不过Rust强大的编译机制会事先防止这种情况的发生,如果硬编译会提示下面的错误:
字符串字面值就是切片
- 有经验的Rust 开发者会采用 &str 作为参数类型,因为这样就可以同时接受 String 和 &str 类型的参数了,因为 String 可以通过切片创造 &str ,这样API函数就会变得更通用。
- 建议把函数参数改成字符串切片,就是 &str
数组的切片
- 说实话这个没什么可说的,和字符串切片的使用方法类似。
fn main() {
let num_arr = [1,2,3,4,5];
let select_num = &num_arr[1..2];
println!("Second number is : {}", select_num[0]);
}
结束
- 感谢阅读