Rust所有权
所有权:
所有权概念是Rust语言的一个重要特性,因为通过它才使得Rust的“安全”,“高并发”得以发挥出优势。因为它让Rust无需垃圾回收,即可保障内存安全。
1、内存安全
对于C/C++程序员来说,可能一直在跟内存安全打交道,内存泄漏呀、智能指针呀什么的。对于一些别的语言来说,会有垃圾回收(garbage collector)机制。例如Python的GC机制,通过“标记清除”、“分代回收”,还有一种忘记了,
目的就是为了解决内存分配的问题,当我们在系统中创建了一个变量,它其实就存在了一条python维护的一条链表上,所有链表检测出这个变量没有引用就会将其回收,以此保证内存的整洁。然而假如有多个变量引用了一个段内存数据,则
按照我们之前的清除方式,会被清除两次?这就有问题了,python会怎么处理这种情况那,有兴趣的可以查看另外一篇讲python的GC的文章。
上面两种方式各有优缺点。Rust则是通过所有权和借用来保证内存安全。很多人不理解为啥说Rust是内存安全的,其实就是在默认情况下,你是写不出内存不安全的代码的。
2、堆和栈
《Rust程序设计》中这么解释堆和栈
在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中, 值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。
我们会在本章的稍后部分描述所有权与栈和堆相关的内容,所以这里只是一个用来预热 的简要解释。 栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入
值的顺序存 储值并以相反顺序取出值。这也被称作 后进先出(last in, first out)。想象一下一叠盘 子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,
也从顶部拿走。不 能从中间也不能从底部增加或拿走盘子!增加数据叫做 进栈(pushing onto the stack),而移出数据叫做 出栈(popping off the stack)。
栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数 据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空 间。
操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示 该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),
有时简称为 “分配”(allocating)。将数据推入栈中并不被认为是分配。因为指 针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须 访问指针。 想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空 桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。 入栈比在堆上
分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空 间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为操作 系统必须首先找到一块
足够存放数据的内存空间,并接着做一些记录为下一次分配做准 备。访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中 跳转越少就越快(缓存)。
继续类比,假设有一个服务员在餐厅里处理多个桌子的点 菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜, 接着桌子 B 听一个菜,然后再桌子 A,
然后再桌子 B 这样的流程会更加缓慢。出于同样 原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在 堆上)能更好的工作。在堆上分配大量的空间也
可能消耗时间。 当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数 的局部变量被压入栈中。当函数结束时,这些值被移出栈。 跟踪哪部分代码正在
使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以 及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一 旦理解了所有权,你就不需要
经常考虑栈和堆了,不过明白了所有权的存在就是为了管 理堆数据,能够帮助解释为什么所有权要以这种方式工作。
对于系统编程语言来说,这是傻子都知道的东西,简单介绍一下C++中的堆和栈
堆:由程序员手动分配和释放,完全不同于数据结构中的堆,分配方式类似链表。由malloc或者new来分配,free和delete来释放。若程序员不释放,程序结束时由系统释放
栈:由编译器自动分配和释放的,存放函数的参数值、局部变量的值等。操作方式类似数据结构中的栈(Cpython中也有,只要基于C的都有这个概念)
这就是C++相比于垃圾回收机制语言的优势,灵活高效。但是也会带来内存安全问题,虽然智能指针通过引用计数的方式避免了很多问题,但是这是最优的吗?
注:我个人建议所有C++程序员使用智能指针,如果你嫌弃stl的那一套,你也可以自己造。
3、Rust中的所有权
弄一段英格利息:
Each value in Rust has a variable that’s called its owner.
There can only be one owner at a time.
When the owner goes out of scope, the value will be dropped.
翻译一下:
每一个值都有一个变量,这个变量就是它的所有者。 每一个值在同一时间只能有一个所有者。 作用域结束时,值就会被销毁, fn main(){ { let str = String::from("shuai"); } println!("{}", str); }
编译出错:
error[E0423]: expected value, found builtin type `str` --> src\main.rs:6:20 | 6 | println!("{}", str); | ^^^ not a value
每个{}中都有一个drop方法,类似于java中的析构函数,或者说python的__init__,对局部作用域中的变量进行清理。
这个应该还是很好理解的。我们在大括号(作用域)内声明了变量str,然后用String对str进行初始化,str就成了这个字符串的所有者。当作用域结束时,str被析构,它所管理的内存被释放。
我们一般把这个变量从出生到死亡的整个阶段称为它的“生命周期”。
fn main(){ let str = String::from("shuai"); let str2 = str; println!("{}", str); } //报错 error[E0382]: borrow of moved value: `str` --> src\main.rs:4:20 | 2 | let str = String::from("shuai"); | --- move occurs because `str` has type `std::string::String`, which does not implement the `Copy` trait 3 | let str2 = str; | --- value moved here 4 | println!("{}", str); | ^^^ value borrowed here after move
move occurs because str
has type std::string::String
, which does not implement the Copy
trait。这个又涉及到一个新的知识点trai,这里我们先不细究,总之就是默认下,会产生移动语义。str把这个字符串给了str2了,而Rust同一时间只能有一个所有者,所以现在str2是这个字符串的所有者,str啥也不是!str的生命周期在move的时候就结束了。
4、移动语义
上面已经讲过了,默认情况下的赋值语句会导致移动语义,即“所有权转移”在C++中我们知道,函数调用也会产生一系列拷贝构造之类的问题。const引用可以避免不必要的拷贝什么的。那Rust中的函数调用会不会存在类似的问题呢。
fn main(){ let str = createAstring_fromFn(); println!("str: {}", str); consumeAstring(str); } fn createAstring_fromFn() -> String { let str = String::from("I am a string"); return str; } fn consumeAstring(str : String) { println!("consume: {}", str); } //运行结果: str: I am a string consume: I am a string //分析 首先、main函数调用createAstring_fromFn函数,这个函数创建了一个字符串,所有者是局部变量str。然后通过return语句将str移动到函数外面。main函数里的str变量接收了这个字符串。 然后能够正常打印str,然后调用consumeAstring函数,通过函数参数调用,将str转移到函数内部,调用完后,并没有将str转移处理,此时,str的生命周期也就结束了。
就是说,Rust中的变量绑定操作,默认是移动语义,一旦被新的变量绑定后,原理的变量就不能再被使用了!而C++中就允许赋值构造函数、运算符重载,因此具体会发生什么情况,取决于程序员如何实现重载。Rust就是让我们必须明确的指出来,你如果是复制,你得显示地告诉我!
注:语义不等于最终的执行情况。编译器很有可能去做优化,但是并不影响我们通过语义理解
5、复制语义
fn main() { let a = 1; let b = a; println!("a: {}", a); } 运行结果: a: 1 // 分析: 咦,woc,怎么没报错! 这是因为Rust对一些简单类型,如整数、bool,赋值默认复制操作。
Rust对这些类型(基础类型)实现了std: :marker: : Copy trait,其实对于不可变的变量可能被分配在栈上,
对于自定义类型来说,这里我们先超前用一个struct,默认是不会实现Copy trait的。
struct haha { data : i32 } impl Copy for haha {} fn main() { let ha = haha { data : 20}; let hahei = ha; println!("{}", ha.data); } //结果 编译错误: error[E0277]: the trait bound `haha: std::clone::Clone` is not satisfied --> src\main.rs:5:6 | 5 | impl Copy for haha {} | ^^^^ the trait `std::clone::Clone` is not implemented for `haha` error: aborting due to previous error
其实是Copy继承了Clone,因此实现Copy trai的同时需要实现Clone trait
struct haha { data : i32 } impl Clone for haha { fn clone(&self) -> haha { return haha { data : self.data}; } } impl Copy for haha {} fn main() { let ha = haha { data : 20}; let hahei = ha; println!("{}", ha.data); } 运行结果: 20
这样自定义类型haha也拥有了复制语义。我们还可以使用#[derive (Copy, Clone ]让编译器帮我们实现Clone trait
#[derive(Copy, Clone)] struct haha { data : i32 } fn main() { let ha = haha { data : 20}; let hahei = ha; println!("{}", ha.data); } // 20
当然,并不是所有数据类型都可以实现Copy trait。对于自定义类型而言,只有所有成员都实现了Copy trait,这个类型才能实现Copy trait。
6、析构函数
在Rust中,不存在构造函数的问题,但是有析构函数的概念。析构函数中不仅可以释放申请的内存,还可以编写逻辑用于管理其他的资源,如文件、锁、套接字等。懂的自然懂。
在Rust实现析构函数需要通过Drop trait
trait Drop { fn drop(&mut self); } use std::ops::Drop; struct A { data : i32 } impl Drop for A { fn drop(&mut self) { println!("destruct fn: {}", self.data); } } fn main() { let a = A { data : 100 }; println!("enter a scope"); { let aa = A { data : 200 }; println!("exit scope"); } println!("exit main fn"); } //运行结果: enter a scope exit scope destruct fn: 200 exit main fn destruct fn: 100
Rust中的析构函数的调用时机和C++比较类似。
use std::ops::Drop; struct A { data : i32 } impl Drop for A { fn drop(&mut self) { println!("destruct fn: {}", self.data); } } fn main() { println!("enter a scope"); { let aa = A { data : 200 }; let bb = A { data : 300 }; println!("exit scope"); } } //运行结果: enter a scope exit scope destruct fn: 300 destruct fn: 200
同一作用域下多个局部变量,先声明后析构,因为局部变量存在“栈”中嘛。当然,Rust也可以实现RAII手法来进行资源管理。
Rust中允许主动析构:
use std::ops::Drop; struct A { data : i32 } impl Drop for A { fn drop(&mut self) { println!("destruct fn: {}", self.data); } } fn main() { let a = A{ data : 100 }; a.drop(); } //编译报错: error[E0040]: explicit use of destructor method --> src\main.rs:17:7 | 17 | a.drop(); | ^^^^ explicit destructor calls not allowed
报错了,是的,Rust不允许手动调用析构函数。但是我们自己想一想,怎么才能让他主动调用析构函数呢,之前有一个consume函数还记得吗?
use std::ops::Drop; use std::mem::drop; struct A { data : i32 } impl Drop for A { fn drop(&mut self) { println!("destruct fn: {}", self.data); } } fn main() { let a = A{ data : 100 }; drop(a); } 运行结果: destruct fn: 100
析构函数提前调用了。Rust提供了标准库中一个函数 std::mem::drop
# [inline] pub fn drop<T>( _ x: T) { }
实现和我们的consume是一样的,内部为空,参数值传递。
use std::ops::Drop; use std::mem::drop; struct A { data : i32 } impl Drop for A { fn drop(&mut self) { println!("destruct fn: {}", self.data); } } fn main() { let a = A{ data : 100 }; drop(a); }
//运行结果: destruct fn: 100
我们只需要保证移动语义就好了。对Copy语义的变量调用drop是没有意义的。
use std::mem::drop; fn main() { let a = 1; drop(a); println!("a after droped: {}", a); } //运行结果: a after droped: 1
可以看到,drop无效,前面我们知道了变量遮蔽的概念,那么变量遮蔽是否会导致析构呢。shadowing并不代表生命周期结束。
use std::ops::Drop; use std::mem::drop; struct A { data : i32 } impl Drop for A { fn drop(&mut self) { println!("destruct fn: {}", self.data); } } fn main() { let a = A{ data : 100 }; let a = A{ data : 200 }; } //运行结果: destruct fn: 200 destruct fn: 100
7、借用
所有权的借用。借用指针(引用):&和&mut,只读借用和可读写借用。
借用指针只能临时地拥有对这个变量读或者写的权限,并没有对这个变量生命周期管理的义务,也因此借用指针的生命周期不能大于它所引用的变量的生命周期,否则会导致空悬指针。 对于不可变变量,不能有&mut借用 同一作用域内,&型借用可以由多个。如果存在&mut型借用指针,那么就只能有一个借用指针 fn main() { let mut str = String::from("I love "); println!("original string: {}", str); println!("original string len: {}", getlength(&str)); push_to_string(&mut str); println!("new string: {}", str); println!("new string len: {}", getlength(&str)); } fn getlength(str : &String) -> usize { str.len() } fn push_to_string(str : &mut String) { str.push_str("Rust!"); }
//运行结果: original string: I love original string len: 7 new string: I love Rust! new string len: 12
8、切片
fn main() { let mut str = String::from("Hello World"); let fist_word = &str[0..5]; //注意这里是左闭右开 let second_word = &str[6..11]; println!("fist_word: {}", fist_word); println!("second_word: {}", second_word); } 运行结果: fist_word: Hello second_word: World