Rust 探索(五)—— 所有权(二)

1. 所有权与函数

将值传递给函数其实在语义上类似于对变量进行赋值;因而,将变量传递给函数将会触发移动或者复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {

let str1 = String::from("hello"); // 变量str1进入作用域

take_ownership(str1); // 类似于赋值,因而移动str1并使其失效

let x = 5; // 变量x进入作用域

make_copy(x); // x进入函数,但由于是拷贝,可以继续使用
} // 离开作用域,先x,后str1

fn take_ownership(some_string: String) { // some_string进入作用域
println!("{}", some_string);
} // some_string离开作用域

fn make_copy(some_integer: i32) { // some_integer进入作用域
println!("{}", some_integer);
} // some_integer离开作用域

其实可以将向函数传递参数简单地考虑为向函数的形参进行赋值

2. 返回值与作用域

函数可以返回值,而在此过程中会发生所有权的转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {

let s1 = gives_ownership(); // gives_ownership()将返回值移动到s1

let s2 = String::from("hello"); // s2进入作用域

let s3 = takes_and_gives_back(s2); // s2移动进入函数,函数的返回值移动到s3
} // s3、s2、s1依次离开作用域


fn gives_ownership() -> String {

let some_string = String::from("hello"); // some_string进入作用域

some_string // 返回值移动给调用者
}


fn takes_and_gives_back(a_string: String) -> String { // a_string进入作用域

a_string // 返回并移动
}

返回值的所有权转移的规则与先前是类似的:将一个值赋值给另一个值时会转移所有权;当一个持有堆数据的变量离开作用域,它的数据会通过drop()的方式被清理回收,除非发生转移

3. 引用与借用

有的时候,我们想要的不仅仅是返回值,还有原来的参数,那么可以用元组一次返回多个值,但这可能显得不是那么规范,但是没有问题,不是吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {

let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

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

fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}

为了解决这样尴尬的情景,Rust提供了引用

实际上,在调用calculate_length()时,s1通过移动进入了函数的参数,由s取代,而后s返回,又移动给了外面接收的s2,但在此过程中,并没有修改其内容,却一直移动它,显然很没有必要

1
2
3
4
5
6
7
8
9
10
11
12
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()
}

现在,函数没有多余的废话了,使用&声明引用允许在不获取所有权的前提下使用值

image-20230419234315650

&就像s拿着绳子,把传进来的s1和它绑一块

引用不持有值的所有权,也就是说他自己不能行动,因此当引用离开作用域,它指向的值不会被丢弃

1
2
3
fn calculate_length(s: &String) -> usize {   // 声明为指向s1的引用
s.len()
} // s离开作用域,但它没有所有权,没有什么处理

通过引用传递参数给函数的方法称为借用,就像借东西一样,有借有还,但是默认还给别人的还是原来的样子(不可修改)

image-20230419235559178

引用默认是不可变的,Rust不允许我们去修改引用指向的值

4. 可变引用

如果实在需要修改引用的内容,也是可以的,当然需要提前和借你东西的人打声招呼,即使用mut声明可变引用

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {

let mut s1 = String::from("hello"); // 1


change(&mut s1); // 2

}

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

需要注意的修改一共有3处,首先是定义函数参数的时候,需要声明为&mut,而后在传递参数的时候也需要显式表示为&mut,同时这个参数本身还要是mut

对于特定作用域中的特定数据来说,一次只能声明一个可变引用

Rust通过规范约束避免了数据竞争问题

  • 两个或两个以上的指针同时访问同一空间
  • 其中至少有一个指针会向空间中写入数据
  • 没有同步数据访问的机制

这些情形都可能发生不可预知的行为,从而导致复杂的bug,Rust通过代码规范的约束在编译的时候就阻止这种可能

我们无法同时创建不可变引用和可变引用,其实这很好理解,可变引用随意一改,不可变引用那里就发生了灵异事件,不是不可变吗,这不睁眼说瞎话呢嘛?

但是不可变引用可以有多个,因为它们只是读取内容,而不进行修改

5. 悬垂引用

我们有很多拥有指针概念的语言,比如C、C++,在使用它们的过程中很容易出现一种错误——悬垂指针

这类指针曾经指向某处内存,但是如今已经被释放或者重新分配,已经物是人非,其上的操作会非常危险

而在Rust语言中编译器可以确保引用永远不会进入这种悬垂状态

1
2
3
4
5
6
7
8
9
10
fn main() {

let reference_to_nothing = dangle();

}

fn dangle() -> &String { // dangle()会返回指向指向String的引用
let s = String::from("hello");
&s // s离开作用域释放,那么指向什么呢?危险
}

image-20230420235557324

s在函数结束时离开作用域,那么指向的内存被释放,而返回的是该区域的引用,这便是悬垂,那么编译器识别并报错

1
2
3
4
5
6
7
8
9
10
fn main() {

let reference_to_nothing = no_dangle();

}

fn no_dangle() -> String {
let s = String::from("hello");
s // 正常返回String,所有权移动,作用域并没有什么操作
}

6. 引用的规则

主要是进行一些总结和归纳:

  • 在任何一段给定的时间里,只能拥有一个可变引用或者任意多个不可变引用
  • 引用总是有效的