-
所有权
-
简介
-
任何编程语言都涉及到内存的分配和回收,当不能正确的对内存进行正确分配,就会导致程序漏洞;不能及时对无用的内存进行及时的回收就会导致大量的内存碎片和垃圾的产生。java和c#都使用了内存的自动回收,编程者不用关注无用内存的销毁,但这会导致内存不能及时回收而导致程序因无可用内存而不能运行。而C和C++语言大量使用指针,而无法及时关注到相关内存是否已经被销毁或超出使用范围了,导致了内存溢出和内存无效等问题,而导致程序bug,而且很难跟踪和解决此类bug。
Rust语言为了解决此类问题,妥善管理内存而设定了一套规则称之为所有权。简而言之就是内存的拥有者用于跟踪内存创建,读、写和销毁。内存只有一个可写的拥有者,称之为所有权,基于此准则,建立一套规则,在程序编译时检查这套规则,如有任何违反,都不能进行编译。使用此规则可以做到内存安全和高效率。
如果精通了其他语言,然后学习了Rust,需要时间熟悉和适应这套规则。如果适应了,则容易开发安全和高效的代码。
-
-
栈内存和堆内存
-
栈:顺序存储,空间有限,先进后出。分配速度块。
堆:临时分配的空间,返回地址指针和长度。分配速度慢。
栈内存用于基本数据类型变量,即长度固定的变量
堆内存用于非基本数据类型变量,即长度可变的变量
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);
/**
* 使用clone,来规避所有权的问题
*/
let s2=s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
/**
* 使用引用,不会发生所有权问题
*/
let s2= &s1;
println!("s1 = {}, s2 = {}", s1, s2);
}
基本类型默认都实现了copy功能,当某个变量赋值给其他变量时,自动在栈内存中复制一个副本,而不是使用指针或地址的方法,进行复制,因此不存在所有权的问题。
-
-
所有权规则
-
Rust中每个值都有一个所有者。
同一时刻只能有一个所有者。
当所有者超出使用范围时,该值将被删除。
使用范围(作用域):Rust语言的作用域使用“{...}”包含起来的部分,或者将变量传递给函数,则函数体内也是一个作用域。
/**
* 每个变量只能有一个拥有者
*/
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // error s1的所有权被移动到s2,因此s1已经是无效的了
fn main() {
/**
* 一个变量可以被多个变量借用,但是不能同时存在可变和不可变借用
*/
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("r1:{},r2:{},r3{}",r1,r2,r3);
}
fn main() {
/**
* 1.变量不能同时使用可变借用和不可变借用 *
*/
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &s;
println!("r1:{},r2:{}",r1,r2);
}
fn main() {
/**
* 一个可变变量不能多次借用到可变借用
*/
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("r1:{},r2:{}",r1,r2);
}
fn main() {
/**
* 使用不用的作用域同时使用可变借用和不可变借用
*/
let mut s = String::from("hello");
{
let r2 = &s;
println!("{} ", r2);
}
{
let r1 = &mut s;
r1.push_str(" world");
println!("{}", r1);
println!("{}", s);
}
}
fn main() {
/**
* 所有权在函数中传递
*/
let s1 = String::from("hello");
takes_ownership(s1);
//println!("{}",s1); //所有权已经被转移到函数内了,因此这条语句报错。
let x = 5;
makes_copy(x);
println!("{}",x);
}
fn takes_ownership(some_string:String){
println!("{some_string}");
}
fn makes_copy(some_number:i32){
println!("{some_number}");
}
返回值和范围
返回值可以转移所有权。
fn main() {
/**
* 返回值可以转移所有权,一般直接转移拥有者,而不是引用或借用
*/
println!("所有权-返回值和范围");
let s1 = gives_ownership();
println!("s1:{}",s1);
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
println!("s3:{}",s3);
}
fn gives_ownership()-> String{
let some_string = String::from("hello");
some_string //所有权已经转出
}
fn takes_and_gives_back(a_string: String)-> String{
a_string //所有权被转出
}
所有权转移遵循相同的模式:
- 变量被赋值给其他变量。
- 超出作用域,所有权被drop掉,除非所有权已经被移出。
使用元祖返回多个值,可以包含传入的值。
fn main() {
/**
* 使用元祖返回多个值,包含传入的值
*/
let s1 = String::from("hello world");
let (s2, len) = calculate_length(s1);
println!("The length of '{s2}' is {len}.");
}
fn calculate_length(s: String)-> (String, usize) {
let length = s.len();
(s, length)
}
-
-
引用与借用
-
不转让所有权,而将其作为参数传入,称之为引用。在函数内使用该参数称之为借用。
有两种引用的类型:
- 共享指针:&
- 可变指针:&mut
他们遵循以下的规则:
- 引用的生命周期不能超过被引用内容
- 可变引用不能存在别名
- 可以同时有多个不可变引用
- 同一作用域只能有一个可变引用
- 不可同一作用域同时交叉使用可变借用和不可以变借用。
- 同一作用域可变引用和不可变引用如果没有交叉使用,可以同时存在。
Rust的引用和指针的概念相似,但是不同之处在于:引用保证了其生命周期内某个特殊类型的值始终是真实有效的。其他语言的指针可能会指向无效内容。
fn main() {
/**
* 使用引用传递参数,而不用传递所有权
*/
let s1 = String::from("hello world");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
/// 计算字符串的长度
///
/// # 参数
/// * `s` - 一个指向String类型的引用,代表待计算长度的字符串
///
/// # 返回值
/// 返回字符串的长度,以usize类型表示
fn calculate_length(s: &String)-> usize {
s.len()
}
上面代码中符号“&”的意思表示将其地址传递到函数中,但是所有权没有转移。下面的图展示了这种概念。
注意:&引用的相反操作是取消引用,即使用符号“*”来表示。
&s1表示创建一个S1值的引用,而不是拥有它,当引用停止使用时,这个值不会被删除掉。同时,在函数声明时需要,需要将参数类型定义为&String,而不是String。来表明这是一个引用。当函数结束时引用会被销毁,但是原变量S1还保持存在。
创建引用的操作,称之为借用。就像现实社会中,如果某人拥有某样东西,你可以向其借用,用完后需要归还,你从来就没有拥有过它。这个过程就称之为借用。
如果你在借用过程中修改它,这种修改是不起作用的。
/**
* 借用过程中改变拥有者的数据
*/
fn main() {
let s= String::from("hello");
change(&s);
}
fn change(some_string: &String){
some_string.push_str(",world");
}
系统报错
如果要改变拥有者的内容,需要使用可变引用,即:使用符号“&mut”
/**
* 使用可变引用来改变拥有者变量的内容
*/
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{s}");
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
操作步骤:
- 变量声明为可变变量
- 函数参数声明为可变引用
- 调用函数时,传递的参数为可变引用
- 在函数内修改其内容
可变引用的限制:一个可变变量只能有一个可变引用,不能有多个。
fn main() {
/**
* 一个可变变量不能多次借用到可变借用
*/
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("r1:{},r2:{}",r1,r2);
}
不同的作用域可以使用多个可变借用
fn main() {
/**
* 在不同的作用域可以使用多个可变引用,但是同一个作用域只能有一个可变引用。
*/
let mut s = String::from("hello");
{
let r1 = &mut s;
r1.push_str("world");
println!("{}", r1);
}
let r2 = &mut s;
r2.push_str("!");
println!("{}", r2);
}
在同一个作用域中可以有多个引用,但是不能同时有引用和可变引用。
fn main() {
/**
* 一个变量可以被多个变量借用,但是不能同时存在可变和不可变借用
*/
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("r1:{},r2:{},r3{}",r1,r2,r3);
}
下面的代码会报错
fn main() {
/**
* 1.变量不能同时使用可变借用和不可变借用
*
*/
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &s;
println!("r1:{},r2:{}",r1,r2);
}
如果可变引用和不可以变引用不是交叉使用,而是分别有各自的使用范围,编译器也不会报错。
fn main() {
/**
* 如果多个不可变引用和可变引用同时存在,但是它们的使用范围不同,那么编译器会允许它们同时存在。
*/
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
let r3 = &mut s;
r3.push_str("world");
println!("{}", r3);
}
悬空引用
一个指针所指向的内容已经销毁,但是该指针还存在,这就会导致悬空引用。在Rust中,编译器保证了引用不会悬空引用:如果某些数据的引用超出了其作用范围,则编译器会报错,从而保证不会有悬空引用。
fn main() {
/**
* 悬空引用,编译器报错
* error[E0106]: missing lifetime specifier
*/
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
错误信息
函数结束时,变量s已经被销毁了,返回对s的引用,会导致悬空引用,因此编译器会报错。
-
-
切片
-
切片运行你对一个集合的某一连续部分进行引用或全部引用,因为切边是引用,所以它没有所有权。
切片适用的集合有字符串和数组。
-
-
-
数组切片
-
-
fn main() {
/**
* 数组切片
*/
let a = [1,2,3,4,5];
let slice = &a[1..3];
println!("{:?}",slice);
let slice = &a[1..3];
println!("{:?}",slice);
let slice = &a[1..=2];
println!("{:?}",slice);
let slice = &a[1..];
println!("{:?}",slice);
let slice = &a[..3];
println!("{:?}",slice);
let slice = &a[..];
println!("{:?}",slice);
}
程序运行的结果是
&a[1..3]表示:从a从0开始计数,取>=1且<3的值。
&a[1..=2]表示:a的索引位置>=1&&a的索引位置<=2。
&a[1..]表示:a的索引位置>=1。
&a[..3]表示:a的索引位置<3。
&a[..]表示:取a的所有元素。
-
-
-
字符串切片
-
-
字符串切片适用Rust的range语法,索引从0开始,小于最后一位数字。
fn main() {
/**
* 字符串切片
*/
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{hello},{world}");
}
从零开始可以将0省略不写,直到最后一位,后面的数字可以不写。如果是整个字符串则起始索引和终止索引都可以不写。
fn main() {
/**
* 字符串切片
*/
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{hello},{world}");
let hello2 = &s[..5];
let world2 = &s[6..];
let all = &s[..];
println!("{hello2},{world2},{all}");
}
索引值可以使用变量替换
let len = s.len();
let hello3 = &s[..len-5];
let world3 = &s[len-5..];
println!("{hello3},{world3}");
注意:字符串切片的范围索引必须出现在有效的UTF-8字符的边界处,否则编译器会报错。
返回字符串切片
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
这个字符串的切片的拥有者的作用域在函数外面,因此可以返回该字符串的切片。
标量字符串(字符串字面值)
fn main() {
/**
* 使用字符串字面量作为参数
*/
let s = "Hello World!";
println!("{}", s);
let first_word = first_word(s);
println!("{}", first_word);
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
注意:传递的参数从“&String”变为“&str”;表明参数类型为字符串切片。
如何从一个字符串转换为一个字符串切片,如下面代码所示:
fn main() {
/**
* 从字符串转为为字符串切片
*/
let s = String::from("Hello World!");
let slice = &s[..];
println!("{}", slice);
}
总结
所有权、借用和切片是为了确保Rust内存安全而定义的规范和概念。使得Rust可以安全的控制内存的使用,在其超出作用域时自动清理这些内存数据,同时也意味着不需要编写额外的代码来保证内存的安全。