Rust 所有权、引用与借用

所有权

所有权是rust最独特的特性,它让Rust无需GC就可以保证内存安全。
通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

  • 入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。

  • 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。

关于各种数据类型的内存模型的讨论我们以后会再讨论

  • 所有权存在的原因:管理heap(堆)数据
1. 跟踪代码的哪些部分正在使用heap的那些数据
2.  最小化heap上的重复数据量
3. 清理heap上未使用的数据以避免空间不足
  • 所有权三条规则
1. 每个值都有一个变量,这个变量是该值的所有者
2. 每个值同时只能有一个所有者
3. 当所有者超出作用域时,该值将被删除
  • 以String类型为例
    在heap上分配,能够存储在编译时未知数量的文本
fn main(){
    
    
	let mut s= String::from("Hello");
	s.push_str(",World");
	println!("{}",s);
}

Rust释放堆的方式为:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统

如本例中,s作用域结束失效实际会调用drop函数

  • 变量和数据交互的方式:移动(Move)
let s1=String::from("hello");
let s2=s1;

String 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。
1
当把s1赋给s2,String的数据被复制了一份:
在stack上复制了一份指针、长度、容量
并没有复制指针指向的heap上的数据
2
当变量离开作用域时,Rust会自动调用drop函数,并将变量使用的heap内存释放。
为了保证内存安全:
Rust没有尝试复制被分配的内存
Rust让s1失效
当s1离开作用域的时候,Rust不需要释放任何东西

浅拷贝
也许会将复制指针、长度、容量视为浅拷贝,但由于Rust让s1失效了,所以叫移动。这种行为避免了二次释放的可能性。Rust不会自动化创建数据的深拷贝。

  • 克隆(Clone)
    如果想对heap上的String数据进行深度拷贝,可以使用clone方法
    3
  • 复制(Copy trait)
    可以用于像整数这样完全存放在stack上的类型。
    如果一个类型实现Copy,那么旧的变量在赋值后仍然是可用的。

所有权与函数

在语义上,将值传递给函数和把值赋给变量是类似的:发生移动或复制

fn main(){
    
    
	let s = String::from("Hello World");
	take_ownership(s);
	let x=5;
	make_copy(x);
	println!(x:{
    
    },x);
}

s在进入函数后被移动到函数里,不再有效
x由于是i32类型实现了copy trait,往函数里传的是x的副本,x在第8行以后仍然是有效的。
到main末尾,s和x离开了作用域。

  • 返回值与作用域
    所有权也会发生改变
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 并移出给调用的函数
}

4

  • 如何让函数使用某个值,但不获得其所有权?

Rust有一个特性叫引用

fn main() {
    
    
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

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

fn calculate_length(s: &String) -> usize {
    
    
    s.len()
}

引用:允许你使用值但不获取其所有权
s是s1的引用,s即是一个指针指向s1。
5
s离开函数作用域后,不会销毁数据,因为没有所有权

  • 借用
    把引用作为函数参数的行为
    借用的变量不可变

  • 可变引用

fn main() {
    
    
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    
    
    some_string.push_str(", world");
}

可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败。在编译时防止数据竞争。

可以通过创建新的作用域,来允许费同时的创建多个可变引用。

    let mut s = String::from("hello");

    {
    
    
        let r1 = &mut s;
    } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

    let r2 = &mut s;

另外一个限制:
不可以同时拥有一个可变引用和一个不变引用
可以多个不可变

(不可变引用 r1 和 r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器在作用域结束之前判断不再使用的引用的能力被称为 非词法作用域生命周期(Non-Lexical Lifetimes,简称 NLL)。)

  • 悬垂引用
    rust编译器会报错,防止其发生

参考:《Rust程序设计语言》

猜你喜欢

转载自blog.csdn.net/weixin_61631200/article/details/127810099