rust学习之所有权,引用和借用

1. 什么是所有权?

1.1 关于堆内存的题外话

所有权是rust独有的概念,是一种资源管理机制,主要是针对堆内存管理的,栈也没啥好管理的,在编译时以及程序运行时自动完成,不需要人为干预,而堆就不一样了,拿C语言来说,其最基本的申请堆内存的函数malloc,申请的内存用完以后都要使用free及时释放,要不然就会造成大名鼎鼎的内存泄漏,但是我觉得应该有很多很多人在初学的时候总是忘记释放,而且由于堆内存的特殊性,像汽车这种对安全性要求比较高的行业,基本上都是禁止使用诸如malloc,calloc系列函数的,原因一般有以下几点:

  1. 汽车行业的特殊性,汽车ECU(汽车的电子控制单元)的实时性要求比较高,而那些动态内存分配函数的内存分配时间和内存的使用状态有很大关系,所以会对汽车软件的执行增加很大的不确定性;
  2. 避免内存碎片,如果malloc使用次数太多的话会产生很多内存碎片,这时候有可能无法分配连续的内存来使用,这时候对汽车的行车安全来说就会有很大的潜在风险;
  3. 就是上面说的内存泄漏了,这个就不说了,内存泄漏到无内存可用的时候,ECU就会故障,如果这是和转向,制动等有关的ECU,那就有可能车毁人亡了。

在汽车工业软件可靠性协会(MISRA)制定的 C 语言编程规范MISRA C 中就有如下规定:

Dir 4.12 Dynamic memory allocation shall not be used
Category Required
Applies to C90, C99
Amplification
This rule applies to all dynamic memory allocation packages including:

  • Those provided by The Standard Library;
  • Third-party packages.

Rationale
The Standard Library’s dynamic memory allocation and deallocation routines can lead to undefined behaviour as described in Rule 21.3. Any other dynamic memory allocation system is likely to exhibit undefined behaviours that are similar to those of The Standard Library. The specification of third-party routines shall be checked to ensure that dynamic memory allocation is not being used inadvertently. If a decision is made to use dynamic memory, care shall be taken to ensure that the software behaves in a predictable manner. For example, there is a risk that:

  • Insufficient memory may be available to satisfy a request — care must be taken to ensure
    that there is a safe and appropriate response to an allocation failure;
  • There is a high variance in the execution time required to perform allocation or deallocation
    depending on the pattern of usage and resulting degree of fragmentation.

翻译过来的意思就是“最好别用,非要用,那你自己掂量好”。

1.2 认识所有权

所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存,比如Java;在另一些语言中,程序员必须亲自分配和释放内存,比如C语言。rust 则是第三种方式:通过所有权系统管理内存。
关于所有权有以下三点规则需要记住:

  1. 每个值都有一个所有者:在rust代码中创建的任何值(无论是简单的整数、复杂的结构体还是动态分配的数据结构)都有一个变量与之关联,这个变量就是这个值的所有者,看下面的例子;
fn main() {
	let s1 = String::from("hello");    // s1是"hello"的所有者
	let s2 = 666;                      // s2是666的所有者
}
  1. 同一时间内,一个值只能有一个所有者:和C语言不同,该语言可以使多个指针指向同一个地址,而Rust严格限制了这种做法,如果一个拥有所有者的堆上的变量赋给另一个变量的时候,之前的变量就会失效,看下面的例子;
fn main() {
	let s1 = String::from("hello");        // s1是"hello"的所有者
	let s2 = s1;                           // "hello"的所有权从s1转移到s2,s1失效,无法使用
	println!("s1 is {s1}");                // 编译报错
}

编译器会报错,提示如下,说明s1已经凉凉了:

borrow of moved value: `s1`
value borrowed here after moverustcClick for full compiler diagnostic
  1. 当所有者离开作用域时,它所拥有的值会自动释放,看下面的例子:
fn main() {	
	{
		let s1 = String::from("Hello");
		let s2 = 666;
	}
	println!("s1 is {s1} and s2 is {s2}");    // 编译报错,因为s1和s2在上一个花括号结束时都失效了
}

无论是堆上的Hello还是栈上的666,在离开作用域的时候都被释放了,栈上的变量随着程序调用出栈,自动失效;堆上的变量离开作用域的时候rust自动调用drop方法来释放堆上的内存,这也是rust的内存管理机制。

1.3 变量与数据的交互方式

1.3.1 移动

先看下面的例子。

fn main() {
	let s1: i32 = 666;                         // s1是666的所有者
	let s2 = s1;                               // s2是666的所有者
	let s3 = "Hello1";
	let s4 = String::from("Hello2");
	let s5 = s4;                               // "Hello2"的所有权转移给了s5
}

s1s2都等于666,因为666是具有固定大小的简单值,所以s1s2都会存储在栈上,在编译时可以确定大小的值都会存储在栈上,比如这里的666或者字符串的字面值(比如这里的s3),而s4则存放在堆上,因为在程序运行之前我们是不知道s4占多大内存的,为了在程序运行时动态操作s4,所以s4则存放在堆上,堆上存放的变量的赋值会转移所有权,因为访问堆上的内存必须通过指向该内存的指针来访问,而这个指针是存放在栈上的,而Rust又不允许两个指针指向同一块内存,所以之前的那个变量就失效了,这个操作就叫移动。所以对于赋值操作来说,对于栈上的变量和堆上的变量会有不同的结果,用对堆上的变量进行赋值就是浅拷贝之后再删除原变量(浅拷贝是只改变指针,不改变指向的地址),对栈上的变量来说就直接是深拷贝了(深拷贝是完全复制一个新的副本)。

1.3.2 克隆

如果确实想复制堆中的数据,想对数据进行深拷贝而不进行所有权的转移,那该怎么办?这时候就需要使用clone方法,如下图:

fn main() {
	let s1 = String::from("hello");
	let s2 = s1.clone();
	println!("s1 = {s1}, s2 = {s2}");
}

这个代码可以正常运行,说明堆上的数据确实被复制了。rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait,也就是说这二者是互斥的。任何不需要分配内存或某种形式资源的类型都可以实现 Copy,也就是说存储在栈上的数据都可以实现Copy 。

1.3 所有权的转移

前面已经接触到了所有权转移的概念,那就是存储位置涉及堆内存的变量之间的赋值会转移所有权,再做一点补充,另外一个与所有权转移有关的概念就是函数调用,函数调用与所有权的关系和变量赋值与所有权的关系差不多,一个变量作为实参传递给函数的时候第一步也是赋值,看下面的例子;

fn main() {
	let s = String::from("hello");    // s获得"hello"的所有权
	trans_owenership(s);              // "hello"的所有权转移到trans_owenership函数内
	println!("{s}");                  // 编译报错,因为trans_owenership函数并没有将"hello"的所有权返回出来
}

fn trans_owenership(some_string:String) {
	println!("{some_string}");
}

按照如下方式修改后代码就可以正常运行了

fn main() {
	let mut s = String::from("hello");    // s获得"hello"的所有权,由于需要给s二次赋值,所以声明为可变类型
	s = trans_owenership(s);              // "hello"的所有权转移到trans_owenership函数内,处理完后再将所有权返回出来
	println!("{s}");                  // 正常运行,因为s重新获得了"hello"的所有权
}

fn trans_owenership(some_string:String) -> String {
	println!("{some_string}");
	some_string
}

2. 引用和借用

一开始学习的时候按照我朴素的理解,我觉得引用就是指针,传递一个变量的引用和就是传递指向这个变量的指针,但是后来去查资料发现这两个概念还是有一些区别的,原来大众所说的引用是C++里的概念,之前并没有接触过C++,借用书上的原话来描述一下引用,引用(reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。 来看一下1.3节的代码,按照下面的方式修改;

fn main() {
	let s = String::from("hello");    // s获得"hello"的所有权
	trans_owenership(&s);              // "hello"的引用传递到trans_owenership函数内
	println!("{s}");                  // 正常运行,因为"hello"的所有权还在s手里
}

fn trans_owenership(some_string: &String) {
	println!("{some_string}");        //不需要对some_string进行解引用,因为使用{}格式化输出引用的时候rust自动进行解引用操作
}

如果你对C语言很清楚的话,相信你可以很容易看懂上面的代码,无非就是传了个地址嘛。所以使用变量的引用可以使用其值而不获得其所有权。
创建一个引用的行为就叫做借用,这很好理解,因为变量的所有权并没有转移,既然所有权没有转移,那通过引用能不能修改变量的值呢?

fn main() {
	let s = String::from("hello");    // s获得"hello"的所有权
	trans_owenership(&s);              // "hello"的引用传递到trans_owenership函数内
	println!("{s}");                  // 编译报错,因为s是不可变变量
}

fn trans_owenership(some_string: &String) {
	some_string.push_str(", world");       //push_str方法是给some_string后面拼接字符串
}

因为s是不可变变量,所以通过引用来使其改变肯定也是行不通的,那假如把s改称可变变量呢?

fn main() {
	let mut s = String::from("hello");    // s获得"hello"的所有权
	trans_owenership(&mut s);              // "hello"的引用传递到trans_owenership函数内
	println!("{s}");                  // 编译报错,因为s是不可变变量
}

fn trans_owenership(some_string: &mut String) {
	some_string.push_str(", world");       //push_str方法是给some_string后面拼接字符串
}

这时候程序正常运行了,这样的引用叫做可变引用。可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这样做可以避免数据竞争,数据竞争的意思是指在多线程或者并发环境下,两个或多个线程同时访问同一块内存区域,并且至少有一个访问是写操作,这可能会导致程序出现未定义行为。不可变引用可以同时存在多个,但是不可变指针和可变指针不能同时存在,看最后一个例子。

fn main() {
	let mut s = String::from("hello");
	let r1 = &s;
	let r2 = &s;
	let r3 = &mut s;
	println!("{}, {}, and {}", r1, r2, r3);    // 编译报错,因为执行println!的时候同时存在可变引用和不可变引用
}

这个例子就说明了不可变指针和可变指针不能同时存在,这样设计的目的还是为了避免数据竞争,比如一个线程通过不可变引用访问s,它认为自己得到的s的值是可靠的,而另一个线程通过可变引用修改s的值,这样的话第一个线程访问到的值完全不可靠了。所以rust 编译器通过借用检查器来强制执行这些引用规则。当代码违反这些规则的时候,即同时创建同一变量的可变引用和不可变引用时,借用检查器会在编译阶段就报错。

你可能感兴趣的:(李某学rust,rust,学习,开发语言)