往期回顾:
Rust的数据类型和函数控制流
Rust的核心功能之一是所有权。
所有程序都必须管理其运行时使用计算机内存的方式。
一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存。
在另一些语言中,程序员必须亲自分配和释放内存。
Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。
在编译的时候,编译器就会知道这些规则,所以说在运行时不会产生任何速度上的影响。
数据存放在栈或堆上极大影响了Rust语言的规则。
变量的作用域:
fn ps0()
{
{
let a=10;
}
println!("{}",a);
}
a在局部作用域下,离开{},生命周期便结束,此时如果你想打印出a,会发现编译器会直接提示你出错:
这种直接检查的功能在其他语言是没有的,Rust还是很nb的。
使用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::from 时,它的实现请求其所需的内存。这在编程语言中是非常通用的。
如:
{
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);
}
这样两个变量都可以访问了。
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");
}
可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这些尝试创建两个 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类型我们留到下一期讲解。