第六章:所有权系统
通用概念
栈内存和堆内存
值语义与引用语义
值语义: 是指 按位复制 与原值无关,保证值的独立性。
基本数据都是值语义。
按位复制 就是栈复制,也是浅复制
深复制 : 对栈上和堆上数据一起复制
引用语义:
- 指数据存储在堆内存中,通过存储在栈内存的指针来管理堆内的数据,并且禁止按位复制。
- 动态数组和字符串都是属于引用语义
复制语义 与 移动语义
复制语义对应值语义,具有值语义特性的变量可以在栈上进行按位复制,以便于内存管理
移动语义对应引用语义,在堆上存储的数据只能进行深复制,而深复制又可能带来极大的性能开销,因此需要在栈上移动指向堆上数据的指针地址,不仅保证了内存安全,还用于按位复制的性能。
所有权机制
Rust 所有权机制的核心是以下的3点:
- 每个值都有一个被称为其所有者的变量,也就是每块内存空间都有其所有者,所有者具有这块内存空间的释放和读写权限
- 每个值在任意时刻由其仅有一个所有者
- 当所有者(变量)离开作用域,这变量将被丢弃
变量绑定
一般使用let 声明变量,但是本质是一种绑定语义。 let 关键字 将 一个变量 与一个值绑定在一起。这个变量就拥有了这个值的所有权。或者说与该内存空间绑定,从而拥有这块内存空间的所有权。
Rust确保对于每块内存空间都只有一个绑定变量与之对应,不允许有两个变量同时指向同一块内存空间
变量绑定具有空间和时间的双重属性。
空间属性:指变量与内存空间进行了绑定,
时间属性: 指绑定的时效性,也就是他的生命周期。
fn foo(){
let s = String::from("hello"); // s 有效
} // 函数执行完成,s离开作用域,drop函数 清理s 的内存空间
当多个变量,指定同样的堆存储的数据,注意!!!
所有权转移
所有权转移 对应于 移动语义。
—— 其绑定变量的所有权转移给另一个变量的过程
example : 变量赋值,函数传递值,函数返回值
变量赋值
所有权机制只针对堆上分配的数据,
基本类型的存储都在栈上
fn main() {
let x = 5;
let y = x;
println!("x :{},y:{}", x, y);
let s1 = String::from("hello");
let s2 = s1; // 这里所有权转移
println!("s1:{},s2:{}", s1, s2) // 转移之后,并不能再使用s1 了: 因为指针无法指向堆上面的数据,
}
上述代码,有两种解决办法:
- 采用深度复制(栈堆数据都复制),将堆上的数据再复制一份给s2 (深度复制如果数据量大,会造成性能影响)
- 采用浅复制(只复制栈上数据),将 两个变量,都指向同一份堆数据(涉及修改变量,另一个也会改变。涉及s1,s2 离开作用域,drop 会清理两次,导致潜在的安全漏洞出现)
所以Rust 的解决办法, 就前变量 进行 设置为无效状态,在函数或者方法执行完毕后,不会再进行drop 清理。
向函数传递值
将值传递给函数在语义上给变量赋值相似,
向函数传递字符串参数时转移所有权
fn main() {
let s = String::from("hello");// s有效
take_ownership(s); // s 所有权转移给函数参数
let x = 4;// x有效
make_copy(x); // x 绑定值按位复制传递给函数参数
} // 作用域结束,x无效,s无效,s所有权转移,无须特殊处理
fn take_ownership(str: String) {
println!("{}", str)
} // 释放str 的内存
fn make_copy(int: i32) {
println!("{}", int)
}// int 无效,无须特殊操作
向HashMap 的方法传递&str 类型参数时不转移所有权
fn main() {
let key = "Favorite color";
let value = "red";
let mut map = HashMap::new();
map.insert(key, value);
println!("{}", map[key])
}
向HashMap的方法传递String类型参数是转移所有权
fn main() {
let key = String::from("Favorite color");
let value = String::from("red");
let mut map = HashMap::new();
map.insert(key, value);
println!("{}", map[key]); // 会 panic ,,插件也会有红下划线
}
// panic
向HashMap 的方法传递String类型参数的引用时不转移所有权
fn main() {
let key = String::from("Favorite color");
let value = String::from("red");
let mut map = HashMap::new();
map.insert(&key, value);
println!("{}", map[key]);
}
// 根据上述代码,传递引用,不会报错
从函数返回值
函数的形参所获得的所有权会在函数执行完成时失效,失效之后再也不能被访问。
解决问题: 通过函数返回值将所有权转移给调用者。
fn main() {
let s1 = give_ownership();
// s1 所有权转移到take_and_give_back 函数中
// take_and_give_back 函数返回值所有权转移给s2
let s2 = take_and_give_back(s1);
}
fn give_ownership() -> String {
let str = String::from("hello");
str
}
fn take_and_give_back(name: String) -> String {
let hello = String::from("hello");
hello + " " + &name
}
浅复制与深复制
浅复制: 只复制栈上的数据
深复制: 复制栈上和堆上的数据
基本数据类型 默认支持 浅复制,String 类型不支持浅复制。
Copy trait 可以区分值定义和引用语义,实现这个的类型,
凡是值语义类型数据都支持浅复制。
整数类型、浮点数类型、布尔类型 等基本数据类型都默认实现了Copy trait,
对于元组类型上文中提到: 如果每个元素类型都实现了Copy trait,那么元组类型也支持浅复制
结构体和枚举,,即使所有元素类型都实现了Copy trait ,也不支持浅复制
当上述无法使用浅复制,必须使用Copy Clone 来变成深复制
#[derive(Copy,Clone)]
引用和借用
书中的理解: 引用是一种语法(本质上是Rust提供的一种指针语义),而借用是对引用行为的描述。
引用分为:
- 不可变引用 -----> 对应着不可变借用
- 可变引用 -------> 对应着可变借用
使用& 操作符执行不可变引用,
使用&mut 执行可变引用
通过&操作符完成对所有权的借用,不会造成所有权的转移
引用和可变引用
向函数传递实参时转移所有权
fn main() {
let vec1 = vec![1, 2, 3];
let vec2 = vec![4, 5, 6];
let answer = sum_vec(vec1, vec2);
println!("v1 : {:?}", vec1);
}
fn sum_vec(v1: Vec<i32>, v2: Vec<i32>) -> i32 {
let sum1: i32 = v1.iter().sum();
let sum2: i32 = v2.iter().sum();
sum1 + sum2
}
// 这里会编译报错,,所有权通过函数参数,进行所有权转移,并不能再使用这个变量了。
通过函数的返回参数返回所有权
fn main() {
let vec1 = vec![1, 2, 3];
let vec2 = vec![4, 5, 6];
let (v1,v2,answer) = sum_vec1(vec1, vec2);
println!("v1 : {:?}", vec1);
}
fn sum_vec1(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
let sum1: i32 = v1.iter().sum();
let sum2: i32 = v2.iter().sum();
(v1, v2, sum1 + sum2)
}
上述存在重复劳动
以引用作为函数参数不获取值的所有权
fn main() {
let vec1 = vec![1, 2, 3];
let vec2 = vec![4, 5, 6];
let answer= sum_vec2(&vec1, &vec2);
println!("v1 : {:?}", vec1);
}
fn sum_vec2(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
let sum1: i32 = v1.iter().sum();
let sum2: i32 = v2.iter().sum();
sum1 + sum2
}
引用默认是只读的,如果想要修改引用,使用&mut 为可变引用
使用 *y 解引用的值,用来获取追踪引用的值
借用规则
为了内存安全,必须遵循规则:
- 对于同一个资源的借用,在同一个作用域只能有一个可变引用(&mut T) ,或 有 n 个 不可变引用(&T) ,但不能同时存在可变引用和不可变引用。
- 在可变引用释放前不能访问资源所有者
- 任何引用的作用域都必须小于资源所有者的作用域,并在离开作用域后自动释放
上述类似于
读写锁
fn main() {
let mut x = 6;
let y = &mut x;
*y += 1;
let z = &x;
println!("y :{},z:{}",*y,*z)
}
// 上述代码就会报错,
报错信息可知: 在同一个作用域,有一个可变引用y,又有一个不可变引用z,这样显然违反了借用规则第一条
借用示例1:切片
切片本身是没有所有权的,他是通过引用语法实现对集合中一段连续的元素序列的借用
1. 切片定义
本质上就是指向一段内存空间的指针,用于访问一段连续内存块中的数据。
字符串切片与动态数组切片
fn main() {
let s = String::from("hello, world");
println!("{}", &s[0..5]);
println!("{}", &s[..5]);
println!("{}", &s[7..s.len()]);
println!("{}", &s[7..]);
println!("{}", &s[..s.len()]);
println!("{}", &s[..]);
let vec = vec![1, 2, 3, 4, 5, 6];
println!("{:?}",&vec[0..2]);
println!("{:?}",&vec[..2]);
println!("{:?}",&vec[2..vec.len()]);
println!("{:?}",&vec[2..]);
println!("{:?}",&vec[0..vec.len()]);
println!("{:?}",&vec[..]);
}
// 这里注意第一个字符串,如果长度不够,并不会报错
2. 切片作为函数参数
切片可以作为函数的参数,把数组、动态数组、字符串中一段连续的元素序列通过引用的方式传递给函数。
切片作为函数参数
fn main() {
let s = String::from("Hello ,Rust!");
let str = "Hello";
let vec = vec![1, 2, 3, 4, 5];
print_str(&s[0..5]);
print_str(&str);
print_vec(&vec[2..1]);
}
fn print_str(s: &str) {
println!("sliect:{:?}", s)
}
fn print_vec(vec: &[i32]) {
println!("sliect:{:?}", vec)
}
3. 可变切片
默认情况下,切片是不能改变所引用的数组、动态数组、字符串中的元素的,也就是说不能通过个更改切片的元素来影响源数据。
但是,如果声明源数据是可变的,同时声明切片也是可变的,就可以通过更改切片的元素来更改源数据。
fn main(){
let mut vec = vec![1,2,3,4,5];
let vec_slice = &mut vec[3..];
vec_slice[0] = 7;
}
借用示例2:迭代器
迭代器 | 所有权借用 | 创建方法 | 迭代器元素类型 |
---|---|---|---|
IntoIter | 转移所有权 | into_iter | T |
Iter | 不可变借用 | iter | &T |
IterMut | 可变借用 | iter_mut | &mut T |
转移所有权IntoIter
迭代器IntoIter 由into_iter 方法创建,会把容器中元素的所有权转移给迭代器,之后原容器不能再使用
into_iter方法转移所有权
let vec = vec!["java", "rust", "python"];
for str in vec.into_iter() {
match str {
"rust" => println!("niubility"),
_ => print!("{} ", str),
}
}
// 这里会报错,创建迭代器后,原容器不能再使用
// println!("{:?}", vec);
}
不可变借用iter
迭代器iter 由iter 方法创建,能把容器中元素的引用传递给迭代器,而不发生所有权转移。即
源容器还可以使用
iter方法获得所有权的不可变引用
fn main() {
let vec = vec!["java", "rust", "python"];
for str in vec.iter() {
match str {
&"rust" => println!("niubility"),
_ => print!("{} ", str),
}
}
println!("{:?}", vec);
}
可变引用IterMut
迭代器IterMut 由iter_mut 方法创建,会把容器中元素的可变引用传递给迭代器,不发生所有权转移。
iter_mut 方法与iter 方法的不同点在于, iter 方法创建的是只读迭代器,不能在迭代器中改变源容器元素。但是item_mut 创建的是可变迭代器,可以改变源容器的元素。
iter_mut 方法获得所有权的可变借用
fn main() {
let mut vec = vec!["java", "rust", "python"];
for str in vec.iter_mut() {
match str {
&mut "Rust" => {
*str = "niubility";
println!("{}", str)
}
_ => println!("{}", str)
}
}
println!("{:?}", vec);
}
生命周期
Rust 的每一个引用以及把汗引用的数据结构,都有一个其保持有效的作用域。
生命周期可以视为这个作用域的名字。
如果存在多个周期某种方式关联的情况,就需要生命周期注解来进行描述。确保使用的引用是有效的。
生命周期语法
生命周期注解的语法是
以‘开头再加上小写字母
生命周期注解位于引用的& 操作符之后,并用一个空格将生命周期注解与引用类型分隔开。比如 &’a i32
注意:
生命周期注解并不改变任何引用的生命周期的大小,只用于描述多个生命周期的关系
隐式生命周期
fn foo(x: &str) -> &str {
x
}
实际上,foo函数包含隐式的生命周期注解,相当于下面代码。
fn foo<'a>(x: &'a str) -> &'a str {
x
}
由编译器自动推导,这里要求返回值的生命周期必须大于或等于参数x的生命周期。
显示生命周期
无须必要的条件下,不需要显示指定,会降低程序的可读性。
fn long_str(x:&str,y: &str) -> &str {
if x.len() > y.len() {
x
}else {
y
}
}
上述代码会报错。编译器无法判断 返回值的生命周期到底是与 x 或者y 的生命周期挂钩。
也就不能通过观察作用域来确定返回的引用是否总是有效。
fn long_str<'a>(x:&'a str,y: &'a str) -> &'a str {
if x.len() > y.len(){
x
}else{
y
}
}
静态生命周期
Rust 预定义一种特殊的生命周期注解’static ,它具有和整个程序运行时相同的生命周期。
let s: &'static str = "I have a static lifetime";
fn foo<'a>(x: &'a str) -> &'a str {
"Hello Rust!"
}
程序中大部分出现的与生命周期有关的问题,都是因为创建了悬垂引用或是生命周期不匹配,要解决这些问题不能只靠将生命周期设为'static
悬垂引用
含义: 引用了无效的数据,就是内存中的数据释放后,被再次使用。
fn main(){
let r;
{
let i = 7;
r = &i;
}
println!("r:{}",r);
}
上述代码变量r 在内部作用域声明,但是在外部作用域使用,故报错。
这里错误,但凡学过的语言都不会出现这样的错误。。
为什么Rust 知道变量r 引用的值在尝试之前就已经离开作用域了?
答: 编译器中的借用检查器,他通过比较生命周期来确保所有的借用都是有效的。
生命周期与函数
通常,在函数体内不需要显示指定生命周期,这是因为上下文中Rust 可以能够分析函数的代码而不用协助。
但是当函数外的代码调用,rust 无法分析参数与返回值的生命周期。。
那么就需要在函数签名中显示指定参数与返回值的生命周期。
注意的是: 在函数签名中指定生命周期,并不会改变任何传入值或返回值的生命周期,而是指出了任何不满足这个约束条件的值都将被借用检查器拒绝。
生命周期可以结合泛型类型、trait约束一起使用
fn main() {
let str1 = String::from("abcd");
let str2 = "xyz";
let result = long_str(str1.as_str(), str2);
println!("logger string:{}", result)
}
// 对于 ‘a 的生面周期等于x 和 y 的生命周期中较小的那个。
fn long_str<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
生命周期与结构体
在结构体定义中使用生命周期注解,类似于声明泛型类型——将生命周期声明在结构体名称后面的的尖括号中。
如果需要为不同的字段指定不同的生命周期,这些生命周期都必须放在尖括号中并以逗号分隔。
impl 实现方法,也必须在impl后面声明生命周期
struct Foo<'a, 'b> {
x: &'a i32,
y: &'b i32,
}
impl<'a, 'b> Foo<'a, 'b> {
fn get_x(&self) -> &i32 { self.x }
fn get_y(&self) -> &i32 { self.y }
fn max_x(&'a self, f: &'a Foo) -> &'a i32 {
if self.x > f.x {
self.x
} else {
f.x
}
}
}
fn main() {
let f1 = Foo { x: &3, y: &5 };
let f2 = Foo { x: &7, y: &9 };
println!("x:{}", f1.get_x());
println!("max_x:{}",f1.max_x(&f2))
}
生命周期省略规则
默认规则——称为生命周期省略规则的引用分析模式,这些规则适用于函数或方法定义。
-
每一个被省略生命周期注解的参数,都具有各不相同的生面周期
fn foo(x:&i32) 等同于fn foo<'a> (x:&'a i32) fn foo(x:&i32,y: &i32)等同于fn foo<'a,'b>(x:&'a i32,y:&'b i32)
-
如果只有一个生命输入周期(无论是否省略),这个 生命周期会赋给所有被省略的输出生命周期。
-
方法中self的生命周期会赋给所有被省略的输出生命周期