Rust快速学习开发(五)- 认识所有权

134 阅读11分钟

什么是所有权

栈和堆

栈:后进先出,想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子! 增加数据叫做 进栈(pushing onto the stack),而移出数据叫做 出栈(popping off the stack)。 栈中所有数据都必须占用已知固定的大小,在编译时大小未知或大小可能变化的数据,要改为存储在堆上面。 堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。 这个过程称作 在堆上分配内存(allocating on the heap) 入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作, 这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。

所有权规则

  1. rust中每一个值都有一个所有者。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)变量离开作用域,这个值将被丢弃

变量作用域

作用域:作用域是一个项(item)在程序中有效的范围。
变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 作用域 结束时都是有效的。

fn main(){
     //  let s="hello";
     // s 在这里无效,它尚未声明
     let s = "hello";   // 从此处起,s 是有效的
     // 使用 s
     // 此作用域已结束,s 不再有效
     println!("{s}");
}

String类型

Rust 有第二个字符串类型,String。这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本

fn main(){
    let mut a=String::from("hello");
    //在字符串后面添加值
    a.push_str(",world!!!");
    println!("{a}");
}

思考:String类型有什么区别呢?为什么 String 可变而字面值却不行呢? 区别在于两个类型对内存的处理上。

内存与分配

Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。

  • 必须在运行时向内存分配器(memory allocator)请求内存。
  • 需要一个当我们处理完 String 时将内存返回给分配器的方法。
fn main() {
    {
      let s = String::from("hello"); // 从此处起,s 是有效的
      // 使用 s
    }                         // 此作用域已结束,
                              // s 不再有效
}

变量与数据交互的方式

移动
fn main() {
    let x=5;
    let y=x;
}

我们大致可以猜到这在干什么:“将 5 绑定到 x;接着生成一个值 x 的拷贝并绑定到 y”。现在有了两个变量,x 和 y,都等于 5。因为整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。

将变量x的整数值赋给y
fn main() {
    let s1=String::from("hahahah");
    let s2=s1;//为了确保内存安全,在 let s2 = s1; 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西
    println!("{}, world!", s1);//运行会报错,因为s1被内存清理了
}

如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。 不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是叫做浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中

克隆
fn main() {
   let s1=String::from("hahahah");
   let s2=s1.clone();//克隆 深度复制 String 中堆上的数据,使用clone()通用函数
   println!("{}, world!", s1);//hahahah
}
拷贝

只在栈上的数据:拷贝

fn main() {
   let x = 5;
   let y = x;
   println!("x = {}, y = {}", x, y);
}

但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone,不过 x 依然有效且没有被移动到 y 中。 原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。
换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。

所有权与函数

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("yours"); // 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 (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度

    (s, length)
}

引用与借用

引用
fn main() {
     let s1=String::from("hello");
    let len=quote_varable(&s1);//&s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。
    println!("s1 and len is:{s1},{len}");
}

/*
  
*/
fn quote_varable(s:&String) -> usize{
    s.len()//返回字符长度
}// 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
 // 所以什么也不会发生
 
 //fn quote_varable_update(s:&String){
    //  s.push_str(", world");//报错  变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。
 //}
 

引用(reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同,引用确保指向某个特定类型的有效值。 引用以一个对象的引用作为参数而不是获取值的所有权,正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。 首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 &s1 给 quote_varable, 同时在函数定义中,我们获取 &String 而不是 String。这些 & 符号就是 引用,它们允许你使用值但不获取其所有权

转移返回值的所有权

//转移返回值的所有权
fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}
/*
转移返回值的所有权:我们必须将 String 返回给调用函数,
以便在调用 calculate_length 后仍能使用 String,因为 String 被移动到了 calculate_length 内
*/
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的长度
    (s, length)
}
可变引用
 fn main(){
     let mut s1=String::from("gogogogo");//我们必须将 s1 改为 mut。然后在调用 change 函数的地方创建一个可变引用 &mut s
     change(&mut s1);
     println!("{}",s1);
     //也可以这样可变引用,但只能有一个可变引用,不能够有多个
     let mut a1=String::from("hahah");
     //let b = &mut a1;报错不能有多个可变引用
     let c = &mut a1;
     println!("{}------{}",a1,c);
     //在这个位置之后b和 c不再使用 
     let mut can_update_quote=String::from("abc");
     let e=&mut can_update_quote;
     e.push_str("aabba");
     println!("eeeeeeeeeeee{}",e)
 }
 //并更新函数签名以接受一个可变引用 s: &mut String。这就非常清楚地表明,change 函数将改变它所借用的值。
fn change(s: &mut String) {
    s.push_str(", world");
}
悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针

fn main(){
    //let quote_to_nothing=dangle();//报错,垂悬引用 因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!Rust 不会允许我们这么做。
    let quote_to_nothing=no_dangle();//这样处理不会报错 直接返回String
    println!("quote_to_nothing value is{} ",quote_to_nothing);
}
//fn dangle()->&String{
   // let s=String::from("kkk");
    //&s // 返回字符串 s 的引用
//}// 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!

fn no_dangle()->String{
let s=String::from("good");
 s
}
引用规则
  • 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。
  • 引用必须总是有效的。

slice类型

不用 slice 编写这个函数的签名,来理解 slice 能解决的问题,如下代码,这个程序编译时没有任何错误,而且在调用 s.clear() 之后使用 word 也不会出错。因为 word 与 s 状态完全没有联系,所以 word 仍然包含值 3。 可以尝试用值 3 来提取变量 s 的第一个单词,不过这是有 bug 的,因为在我们将 3 保存到 word 之后 s 的内容已经改变。 不得不时刻担心 word 的索引与 s 中的数据不再同步,这很啰嗦且易出错!如果编写这么一个 second_word 函数的话,管理索引这件事将更加容易出问题。

fn main(){
   let mut s=String::from("hah ahah");
   let word=frist_word(&mut s);
   s.clear();//清空了字符串,使其等于""
   //word此处仍然为3
   //但是没有更多的字符串让我们有效的应用数值3,word的值现在完全无效
   println!("s value is {s},word value is {word}");
}

fn frist_word(s:&String)-> usize{
   let bytes=s.as_bytes();//因为需要逐个元素的检查 String 中的值是否为空格,需要用 as_bytes 方法将 String 转化为字节数组。
   //在 for 循环中,我们通过字节的字面值语法来寻找代表空格的字节。
   for(i,&item) in bytes.iter().enumerate(){//使用 iter 方法在字节数组上创建一个迭代器: iter 方法返回集合中的每一个元素,而 enumerate 包装了 iter 的结果
	   println!("i:{i},item:{item}");
	   if item==b' '{
		   return i;//如果找到了一个空格,返回它的位置;表达式return返回,后面代码不执行
	   }
   }
	s.len()//如果for循环没找到空格 使用 s.len() 返回字符串的长度:
}

字符串slice

fn main(){
	let s=String::from("hello World!");
	let len=s.len(); 
	// 截取超出范围会报错,字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出
	let hello=&s[0..5];// 和这句是相同的 let hello=&s[..5] 如果想要从索引 0 开始,可以不写两个点号之前的值
	let world=&s[6..12];// 和这句是相同的 let world=&s[6..len]
	let all_str=&s[..];
	println!("{hello}------{world}-----{all_str}");

	let two=frist_word2(&s);
	println!("----{two}");
}

字符串字面值就是slice

s 的类型是 &str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。

fn main3(){
	let s = "Hello, world!";
}

使用slice处理第一个问题

//使用slice处理第一个问题
fn frist_word2(s:&String)-> &str{//重写 first_word 来返回一个 slice。“字符串 slice” 的类型声明写作 &str:
	let bytes=s.as_bytes();
	 for (i, &item) in bytes.iter().enumerate() {
		 if item==b' '{
			return &s[..i];//返回0-有空格处
		  }
	 }
	 &s[..]//全部字符
}

总结

所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。 Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。