找回密码
 立即注册
首页 资源区 代码 rust学习五、Rust所有权和函数传参

rust学习五、Rust所有权和函数传参

梨恐 前天 14:42
在的中译版中,作者用了30页的篇幅来阐述这个问题。
如作者所言,所有权是学习rust语言的基础,不掌握这个,无需继续往下,所以,这是初学rust就必须会的。
 
正是所有权概念和相关工具的引入,Rust才能够在没有垃圾回收机制的前提下保障内存安全。
一、变量的存储方式和赋值方式

要进入rust所有权范围讨论问题,那么必须先理解RUST的变量的存储方式赋值方式
rust出于各种目的,规定变量可以存放在栈和堆上:

  • 栈-存放哪些编译时期就知道大小的。通常存储那些简单的数据类型,例如整数、浮点、布尔、字符、成员类型都是整数、浮点、布尔、字符之一的元组

    • 注意这是一个FILO(先进后出,或者是后进先出)类型的,好似堆碟子,反而最上面的最先用。

  • 堆-存放那些编译时期无法知道其大小的。例如String(字符串)
栈变量转赋
对于栈变量而言,把a赋值给b,仅仅是一种在栈中复制数据的过程-快速且成本小。
  1. let a=10;
  2. let b=a;
复制代码
 上面这个代码中,就是把10复制一份给b。
 
堆变量转赋
但是堆不同,这个就是所有权的问题的来源,见后文。
堆变量和java的类变量是相似的存储方式,都是用一个地址指向实际的存储区域,如下图(来自书本):
1.png

 
所有权问题就是堆变量问题。
二、所有权规则是如何产生的?所有权规则是什么?

2.1、规则怎么来?

要理解所有权,就需要从rust的设计目的谈起,否则难于理解有些概念。
按照rust官方的说法,rust要保证内存安全(一定会释放掉),同时还需要保证高效(不能用java 那样的gc管理器),同时还不能太繁琐(像C++那样手动释放)
所以,他们想到了一个主意:一个变量应该用完就自动释放(通常是立刻释放)
通过设这种设定,那么就解决了一些问题:

  • 不会内存泄漏
  • 不需要借助额外的垃圾收集器,增大了用于系统编程的可行性.因为我们难于想象一些系统级别的组件(高效小巧)会和一个Gc绑定在一起
但这会引发一个问题,怎么知道需要被移除的变量没人用了?
像java那样使用引用计数器之类的?但是现在不搞那一套,就需要定个规矩:一个变量一定要有所有者;而且任意时刻只能有一个所有者
注意:通过后面章节我们知道有Rc,Arc这样的类型用于引用计数的,属于rust中特例独行者,妥协者!
和前面提到的结合起来,就是rust著名的所有权规则:

  • 一个值(不是变量)一定要有所有者
  • 而且任意时刻,一个值只能有一个所有者
  • 值所有者离开作用域后,持有的值会被立刻释放
因为任意时候一个值只有一个所有者,所以一旦所有者离开返回,就可以简单“高效”地执行释放操作,不用担心还被谁用了。
规则就是为了达到这个目的。
 
:第二条规则“任意时刻,一个值只能有一个所有者”这个似乎有问题,因为在书本的第十五章节的Rc(引用计数)指针又声称"然而,有些情况单个值可能会有多个所有者"
 
这个规则有一个瑕疵:按照rust官方的说法,有个drop程序会立刻做这个事情,那么这某种程度上会降低性能。
但是现代高级语言都没有解决这个问题,哪怕C++之类的,所以这个就不算是一个问题了。大家都装糊涂吧,也许某天cpu和操作系统变化了之后,可以解决这个问题。
所以,这应该是一个当前情况下,权衡后的可接受方案。
 
2.2、规则怎么得到保证?

规则定了,那么如何保证这些规则可以得到遵守?
1.明确作用域的
如果是有明确作用域的,例如{}或者函数,那么很好理解:
  1. fn test(){
  2.   let s:String=String::from("种瓜得豆");
  3.   println!("{}",s);
  4. }
复制代码
绝大部分语言都是这么规定,所以能够立刻理解!离开函数或者明确的作用域,就应该要释放掉。
 
2.在一个作用域之内
参考书中的经典例子:
  1. let s1=String::from("锦绣中华");
  2. let s2=s1;
  3. println!("{}",s1);
复制代码
 通不过编译!
  1. let s1=String::from("锦绣中华");
  2.            |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
  3.         12 |     let s2=s1;
  4.            |            -- value moved here
  5.         13 |     println!("{}",s1);
  6.            |                   ^^ value borrowed here after move
复制代码
第一次接触这个,我有一点震惊,是因为两个没有想到:
1.怎么会不行?
2.编译器就能够解决这个问题,而不是在运行时发生。编译器够厉害的,够累的。
 
现在再回过头看下这个编译错误提示,提示大意是这样的:
由于s1是String类型,所以发生的移动情况。String并没有实现Copy trait(类型特性)(意思就是不能通过复制完成值的转赋)
当指定s2=s1的时候,s1的值已经转给了s2(s1已经没有值了)
当再次使用s1的时候,这就是一种典型的错误行为:值被移动后企图借用
 
通过这个提示我们可以看到:在一个作用域之内,如果一个变量转赋给另外一个变量的时候,会发生一件事情值会从一个变量转到另外一个变量上,所有权转移了!
为什么要这么做? 因为只有这样才能保证前面提到的第二个原则:任意时刻只能有一个变量拥有某个值
 
回头再看编译提示,我们还知道一点:类似s1这样的变量只所以会发生这个值转移情况,是因为s1是String,默认没有实现Copy类型特性。
反过来说,如果String实现了Copy类型特性,那么就不会发生这种编译错误。
补充示例(2025/04/26)
  1. #[derive(Debug,Copy,Clone)]
  2. enum EggCategory{
  3.     土鸡,
  4.     散养鸡,
  5.     家养鸡,
  6.     饲料鸡,
  7.     野鸡
  8. }
  9. #[derive(Copy,Clone,Debug)]
  10. struct Egg{
  11.     weight:u8,
  12.     category:EggCategory
  13. }
  14. #[derive(Debug)]
  15. struct Pig{
  16.     name:String,
  17.     weight:u16
  18. }
  19. fn main() {
  20.     let  egg1 = Egg{weight: 10, category:EggCategory::土鸡};
  21.     let  egg2 = egg1;
  22.     println!("{:?}",egg1);
  23.     println!("{:?}",egg2);
  24.     let pig = Pig{name:String::from("猪"),weight: 100};
  25.     //let _pig2 = pig;  这个会报错提示:consider implementing `Clone` for this type
  26.     println!("{:?}",pig);
  27. }
复制代码
 
从这个例子中可以看到Egg实现了Copy,所以egg2=egg1不会让egg2夺取egg1的所有权。
而Pig没有实现,所以如果企图 let _pig2=pig之后再打印pig,那么编译器会提示:consider implementing `Clone` for this type
 
2.3、规则带来的其它问题

虽然三个规则保证了目的,但是会带来不少问题(麻烦),比较典型的问题就是:
一个变量的值被移走后,那么再次使用原来的变量就变得麻烦了。 因为需要再次移动值。如果这样写代码,太冗余啰嗦,谁也受不了!
以下是一个奇怪的例子。
  1. fn main(){
  2.     let mut  address:String=String::from("福建福州");
  3.     println!("{}",address);
  4.     print_str(address);
  5.     //再用address,那么久会报错,如果要不报错,则必须
  6.     //插入诸如 address=xxx之类语句,把值移回来,或者重新赋值
  7.     println!("新地址:{}",address) ;  //报错
  8. }
  9. fn  print_str(s:String){
  10.     println!("{}",s);
  11. }
复制代码
 
在上例中,如果为了避免println!("新地址:{}",address)报错,必须需要在这句话之前做一些事情。这样无疑太麻烦了。
如果按照上面这种方法,那么rust就没有存在的意义,因为一门语言不但要考虑性能、安全等,也需要考虑工程效果:不能让工程师烦
为了解决这个问题,rust给出了5个解决方案

  • 不可变引用-通过某种方式标记这是一种借用,值所有权没有转移。借用完成后会自动归还。当前借用不能修改值
  • 可变引用   -通过某种方式标记这是一种借用,值所有权没有转移,借用完成后会自动归还。这个期间,其它变量不能同时用这个值。但是当前借用能修改这个值
  • 切片引用-只借用了部分(也可以是全部),可以细分为不可变切片可变切片
  • 使用Copy特质(trait)复制,简化转赋和函数传参,这是后面章节的内容
  • 只用Rc和Arc智能指针实现多个变量共享一个数据,这也是后面章节的内容
通过前面3个方案,编译器已经帮工程师默默地完成了借用和自动归还的操作,工程师不需要再操心了。
 
借用是如何实现的,借用原书的2幅图
图_引用
2.png

图_切片引用
3.png

 
原书还列出两个引用原则:

  • 在任何一段给定的时间内,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用                   
  • 引用总是有效的
 
后文演示这几个例子
2.3.1不可变引用
  1. fn main() {
  2.     let name = String::from("岳飞");
  3.     //引用,被引用多少次都无所谓
  4.     print_me(&name);
  5.     print_me(&name);
  6.     let n1 = &name;
  7.     let n2 = &name;
  8.     println!("n1={},n2={}", n1, n2);
  9. }
复制代码
 
书写格式: &s
name可以同时被不可变借用多次。
这意味着,并发情况下可以共用一个值。
2.3.2可变引用

书写格式: &mut s
  1. fn mut_borrow() {
  2.     let mut s = String::from("飞翔");
  3.     let s1 = &mut s;
  4.     println!("{}", s1);
  5.     s1.push_str("在太空!");
  6.     println!("{}", s1);
  7.     let s2 = &mut s;
  8.     println!("{}", s2);
  9.     //println!("{}",s1); //在已经借给s2的情况下,再使用s1会报错 -- first borrow later used here
  10.     let s3 = &mut s;
  11.     s3.push_str("星光灿烂");
  12.     println!("{}", s3);
  13.     let s4 = &mut s;
  14.     let s5 = &mut s;
  15.     let s6 = &s;
  16. }
复制代码
 
由于只能同时有一个可变引用,所以并发是一个问题。当然其它章节会讨论如何某种程度上实现并发下的可变引用。
2.3.3切片引用

书写格式:
&s[..]   -- 等同于s&s[n..m] -- 取从n到m-1的部分,其中n>=0&s[..n]  -- 取0到n-1的部分&s[n..]  -- 取从n及其之后的所有特别地,字符串切片类型写成 &str。
同时定义多个不可变切片,并可同时用
  1. fn show_string_slice(){
  2.     let name=String::from("ABCDEF GHIJKLMN");
  3.     let s1=&name[0..4];
  4.     let s2=&name[5..10];
  5.     println!("s1={},s2={}",s1,s2);
  6. }
复制代码
 
其它切片演示:
可变的数组切片
  1. fn test_mut_slice(){
  2.     let mut numbers = [1, 2, 3, 4, 5];  
  3.     let slice: &mut [i32] = &mut numbers[0..1]; // 获取整个数组的可变切片  
  4.     slice[0] = 10; // 修改切片中的第一个元素,这也会修改原始数组  
  5.     println!("{:?}", numbers); // 输出: [10, 2, 3, 4, 5]
  6. }
复制代码
 
三、函数/方法传参(20250320补)

按照一般的所有权规则,如果一个函数的参数没有使用引用符号,那么很可能传递的时候,所有权就会发生转移。
但是后面的事实告诉我们,并不总是这样的,例如如果对象实现过了Copy特质(trait)。
如果是初学者,可以跳过这个,毕竟这些内容在很后面。
这里记录下例子,并稍微解释下Copy特质。
  1. use std::ops::{Add,Sub};
  2. #[derive(Debug, Copy, Clone, PartialEq)]
  3. struct Point {
  4.     x: i32,
  5.     y: i32,
  6. }
  7. /**
  8. * 这个使用默认类型,来自rust编程语言官方文档的例子
  9. */
  10. impl Add for Point {
  11.     type Output = Point;
  12.     fn add(self, other: Point) -> Point {
  13.         Point {
  14.             x: self.x + other.x,
  15.             y: self.y + other.y,
  16.         }
  17.     }
  18. }
  19. /**
  20. * 实现相减运算符(从而实现Point的-重载),需要实现Sub trait
  21. */
  22. impl Sub for Point {
  23.     type Output = Point;
  24.     /**
  25.      * 需要特别注意的是两个参数的定义
  26.      * self -  没有使用引用
  27.      * other - 没有要求引用
  28.      * 这种不引用的方式,不同于一般的方法定义
  29.      */
  30.     fn sub(self, other: Point) -> Point {
  31.         Point {
  32.             x: self.x - other.x,
  33.             y: self.y - other.y,
  34.         }
  35.     }
  36. }
  37. #[derive(Debug)]
  38. struct trangle{
  39.     a:Point,
  40.     b:Point,
  41.     c:Point
  42. }
  43. fn main() {
  44.     let p1 = Point { x: 1, y: 2 };
  45.     let p2 = Point { x: 3, y: 4 };
  46.     //使用重载的方式调用
  47.     println!("{:?}+{:?}={:?}",p1,p2, p1 + p2);
  48.     println!("{:?}-{:?}={:?}",p1,p2, p1 - p2);
  49.     //不使用重载的方式调用
  50.     let p3 = p1.add(p2).sub(p2);
  51.     let p4 = (p1.sub(p2)).add(p2);
  52.     println!("{:?}+{:?}-{:?}={:?}",p1,p2, p2,p3);
  53.     println!("{:?}-{:?}+{:?}={:?}",p1,p2,p2, p4);
  54.     //演示三角形
  55.     let t = trangle{a:p1,b:p2,c:Point { x: 5, y: 6 }};
  56.     println!("{:?}",t);
  57.     println!("{:?}",p1); //p1所有权没有转移,是因为p1实现了Copy特质
  58. }   
复制代码
 
这个代码正常运行,不会提示所有权已经转移的问题。
这是因为:Copy trait允许类型实现按位复制,也就是说,当赋值或作为函数参数传递时,不需要移动所有权,而是直接复制。
不过,只有满足某些条件的类型才能实现Copy,比如所有字段都实现了Copy,并且类型本身没有实现Drop trait
 
四、小节


  • rust的值的所有权规则是一个非常独特的内容,其它语言暂时没有这样的情况
  • rust的所有权有一点难于理解,但务必理解,因为这是继续的基础,此关不通,不要考虑后面的学习内容
  • rust的所有权,可以算是一种相对高效的主动垃圾回收机制,是一种可以接受的妥协
  • rust提供了多种引用方式,使得同时共享一个值变得可能,例如引用,切片引用
  • rust的编译器默默地干了很多的事情
  • 为了让对象和方法更接近一般人的思维方式和编码习惯,rust一定会采取许多措施,这些措施会导致rust的代码进一步复杂:更奇怪的语法、更少见的符号
  • 所有权的规则2(任意时刻值有且只有一个所有者)可能是有问题,rust存在例外情况--Rc(引用计数指针)
  • 在方法/函数中定义的参数,如果参数类型本身没有实现Copy特质,那么参数可能会发生所有权转移
         特别注意:这些总结都是基于"安全rust"范围内的讨论,rust还有不安全的代码,在不安全的时候,很多规则是不遵守的。
         只不过,"不安全rust"代码比较少,也不推荐。这些都是rust无可奈何的妥协!似乎凡事有例外!
 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册