Rust语言所有权与引用(详解)
往期回顾:
Rust的数据类型和函数控制流
所有权
Rust的核心功能之一是所有权。
所有程序都必须管理其运行时使用计算机内存的方式。
一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存。
在另一些语言中,程序员必须亲自分配和释放内存。
Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。
在编译的时候,编译器就会知道这些规则,所以说在运行时不会产生任何速度上的影响。
栈与堆
数据存放在栈或堆上极大影响了Rust语言的规则。
- 栈:栈中的所有数据都必须占用已知且固定的大小。且满足先进后出的原则,栈存储静态数据。
- 堆:如果数据无法提前预知其大小,则必须把他放入堆中,堆存放动态内存分配的数据。
- 存放规则:访问在栈上的数据的速度明显比访问在堆上的数据的速度快,因为栈是已知大小且数据每次进来都会在栈顶,因为(入栈时)分配器无需为存储新数据去搜索内存空间;相反堆中存储的都是动态内存分配的数据,我们无法知道他们具体的大小,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
- 关于速度:访问栈的连续数据比访问堆的不连续数据的速度快,堆还要搜索位置,而栈不需要搜索。
所有权规则
- Rust 中的每一个值都有一个 所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
变量的作用域:
fn ps0() { { let a=10; } println!("{}",a); }
a在局部作用域下,离开{},生命周期便结束,此时如果你想打印出a,会发现编译器会直接提示你出错:
这种直接检查的功能在其他语言是没有的,Rust还是很nb的。
String类型
使用String来为一个变量分配内存:
这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。
使用From来指定字符串字面值即:你需要的内存空间大小会直接确定:
let mut s=String::from("Hello ");
使用push_str方法来在后面插入一个字符串:
fn ps1() { let mut s=String::from("Hello "); s.push_str("ylh"); println!("{}",s); }
内存与分配
- 第一部分:分配
对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
- 必须在运行时向内存分配器(memory allocator)请求内存。
- 需要一个当我们处理完 String 时将内存返回给分配器的方法。
这由我们自己完成:即当调用 String::from 时,它的实现请求其所需的内存。这在编程语言中是非常通用的。
- 第二部分:释放
- 许多语言都提供了垃圾处理机制:我们无需关注这些内存,因为他们会当作垃圾自动释放。
- 有些语言没有垃圾回收这个概念,我们需要显式的释放这些垃圾,否则就会造成垃圾未释放而内存泄漏。
- Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。 我感觉有点类似于C++智能指针。
如:
{ let a=10; let s=String::from("hhh"); } //释放内存,下面将不能在访问变量 // s a 不再有效
他会在离开作用域时自动释放。
移动与深拷贝
看下面这两个例子:
fn ps20() { let a=10; let b=a; //拷贝a到b }
fn ps21() { let s=String::from("Hello"); let str=s; println!("{},{}",s,str); }
一个是整数变量的拷贝,一个是动态分配内存的拷贝。
你可能会认为他们都能正常运行,但是:只有第一个会正常运行。第二个会出现以下错误:
原变量s被移动了,找不到s原变量。
学过C++的都知道:指针变量的拷贝不能简单的赋值运算符,赋值的只是原变量的一份浅拷贝。
当我们将 s 赋值给 str,String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。
这将会导致 转移所有权。即原变量不复存在。
他们两个指向同一个内存空间。这样不仅在运行速度上会有严重的影响,甚至在离开作用域时,由于释放内存,会导致同一块内存 释放两次:二次污染。
因此编译器察觉到我们使用浅拷贝,但又访问了原变量时,会直接报错:
因此新拷贝的变量将替代了原变量,原变量不复存在。
如果我们不希望它消失,我们应该使用深拷贝:
Rust为我们提供了clone函数可以直接拷贝堆指针的内存空间:
fn ps2() { let s=String::from("Hello"); //let s="wpani"; 无需深拷贝 //所有权转换 //调用clone实现深拷贝 let str=s.clone(); println!("{},{}",s,str); }
这样两个变量都可以访问了。
那么为什么两个整数的浅拷贝不会使得原变量消失呢?
- 原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
往函数转移所有权
fn main() { let s=String::from("Hello"); let x=655; //ps3(s.clone()); 则可以继续使用s ps3(s); //无法使用s ps4(x); println!("s={},x={}",s,x); } fn ps3(some_str:String) { println!("{}",some_str); //some_str被释放 } fn ps4(num:u32) { println!("{}",num); }
在传递给函数后,转移所有权。函数将作为一个块作用域,导致堆上的内存被释放,所以在函数之后,堆变量就不复存在了,但是栈变量仍然存在。
转移返回值的所有权
fn main() { let a=ps5(); //得到了num的所有权 let str=ps6(); //得到了str的所有权 println!("{},{}",a,str); } fn ps5()->u32 { let num=10; num } fn ps6()->String { let str=String::from("Hello"); str }
他们可以从函数返回,直到结束作用域则销毁,又被称为:转移返回值的所有权。
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
drop :当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 处自动调用 drop。
转移函数参数的所有权
示例:返回元素从元组中赋值:
fn main() { let str=String::from("elllo"); //函数返回元组:String和usize类型 let (s,len)=ps7(str); //赋值给s和len变量。 //str消失,无法使用 println!("{},{}",s,len); } fn ps7(s:String)->(String,usize) { let length=s.len(); (s,length) //返回元组 }
但是如果我们在想访问原始数据:str,则会出错:变量的所有权被转移,变量已经消失。
这些都是需要获取变量的所有权才能使用值的示例,那么原来的变量就会消失,那么有没有什么办法不获取所有权就能使用值呢?
引用与借用
Rust 对此提供了一个不用获取所有权就可以使用值的功能,叫做 引用
常引用
fn main() { let str=String::from("elllo"); //函数返回元组:String和usize类型 let (s,len)=ps7(&str); //赋值给s和len变量。 //仍可以使用str println!("{},{},{}",str,s,len); } fn ps7(s:&String)->(&String,usize) { let length=s.len(); (s,length) //返回元组 }
还是这个示例:注意这次我们给他加了一个引用符号:&
意味着我们可以由此访问储存于该地址的属于其他变量的数据。
而所有权不会消失,例如我们在打印原始的变量str:
所有权并未被转移,而且我们还获得了访问的权力。
&s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。
那么我们能否修改常引用的变量呢:
fn main() { let mut num=String::from("100"); println!("num={}",num); ps8(&mut num); println!("num={}",num); } fn ps8(str:&String) { str.push_str("woaini"); }
正如变量默认是不可变的,常引用也一样。(默认)不允许修改引用的值。
但是我们就一定不能修改引用了吗? 我们可以使用可变引用
可变引用
我们只需要在函数参数里加上mut即可:
fn main() { let mut num=String::from("100"); println!("num={}",num); ps8(&mut num); println!("num={}",num); } fn ps8(str:&mut String) { str.push_str("woaini"); }
mut就非常清楚地表明 ,函数将改变它所借用的值。
作用域重合:
可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败:
let mut num=String::from("100"); let num1=&mut num; let num2=&mut num; //不能创建第二个可变引用,因为它在第一个可变引用的作用域中 println!("{},{}",num1,num2); //除非你等到第一次结束num1后再创建num2
防止同一时间对同一数据进行多个可变引用的限制允许可变性,不过是以一种受限制的方式允许。新 Rustacean 们经常难以适应这一点,因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。
数据竞争类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
Rust摊牌了,因为它不会编译存在数据竞争的代码!
let mut num=10; //可以同时拥有常引用 let a=# let b=# //在常引用的作用域内,不能再次拥有可变引用 let c=&mut num;
我们先把c注释:可以打印出a和b这两个常引用的值:
但是如果我们再尝试一块打印c:
我们也不能在拥有不可变引用的同时拥有可变引用。
这会产生作用域重合。
不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
避免作用域重合:
!!!!注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止!!!!!!!!!
如下面的代码:r1和r2是常量引用,从声明时开始一直到最后一次使用println!函数之后就结束了,作用域消失,此时在创建可变引用,不会产生作用域重合:
fn main() { let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 println!("{} and {}", r1, r2); // 此位置之后 r1 和 r2 不再使用 let r3 = &mut s; // 没问题 println!("{}", r3); }
不可变引用 r1 和 r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器在作用域结束之前判断不再使用的引用的能力被称为 非词法作用域生命周期(Non-Lexical Lifetimes,简称 NLL)。
可变引用也是如此:
let mut num=10; let c=&mut num; //只能有这一个 println!("{}",c); //上一个可变引用作用域周期结束,可以创建新的可变引用 let c1=&mut num; println!("{}",c1);
悬垂引用
垂悬引用与垂悬指针是一样的原理:
悬垂指针是其指向的内存可能已经被分配给其它持有者。
相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
我们试图让一个引用指向一个无效的 String并且返回:
fn main() { let s=ps9(); } fn ps9()->&String { let s=String::from("Hello"); &s }
在函数中创建的变量函数结束时生命周期结束,变量被销毁,返回的是一个空悬引用。
编译器会给出错误,但是在其他语言中如果没有出现崩溃性错误,编译器无法提示并且给出错误提示。
我们只要返回S,而不是引用,就可以避免这个错误。
fn main() { let s=ps9(); println!("{}",s); } fn ps9()->String { let s=String::from("Hello"); s }
小结
引用的规则
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
Slice类型我们留到下一期讲解。
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209717.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战