0%

Rust-所有权、引用与Slice

Rust 通过所有权系统管理内存,编译器在编译时会根据一系列规则进行检查,如果违反了这些规则,程序不能通过编译。在运行时,所有权系统的任何功能都不会减缓程序的运行

所有权规则

  1. Rust 中的每一个值都有一个所有者
  2. 值在任意时刻有且仅有一个所有者
  3. 当所有者(变量)离开作用域,这个值将被丢弃

变量作用域

同C语言,以 Rust 中的String类型举例

1
2
3
4
{
let str = String::from("hello");
str.push_str(", world !");
}

当变量str离开作用域时,会自动调用一个名为drop的函数,类似C++中的RAII

变量与数据交互的方式:移动

对于以下代码

1
2
let x = 5;
let y = x;

其将5绑定到x,接着生成一个x的拷贝并绑定到y,两个变量都在栈上

而对于以下代码

1
2
let s1 = String::from("hello");
let s2 = s1;

变量s1实际内容是存放在堆中,String对象由指向字符串内容的指针,一个长度与一个容量组成,这一部分数据是在栈上的,此处的操作类似于C++11中的std::move()操作,将s1中长度与容量赋值给s2,将s2的指针指向s1所指向的字符串内容,在C++11中,std::move()这一操作一般是对将亡值使用,在Rust中并无这一限制,但是在Rust中一旦使用这样的移动,则移动前的变量将不再有效,不能使用,这样就避免了变量离开作用域时的多次释放导致的未定义行为

简单来说,以上的操作是直接将s1认定为将亡值并使用移动语义将其内容转义到s2

变量与数据的交互方式:克隆

如下

1
2
let s1 = String::from("hello");
let s2 = s1.clone();

类似C++中的拷贝构造

只在栈上的数据:拷贝

如果数据本身大小已知,且整个存储在栈上,则在赋值时会进行拷贝操作,Rust 中存在一个 Copytraitde的特殊注解,可以用在类似整型这样存储在栈上的类型上,一个类型如果实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。同时,Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解,将会出现一个编译时错误。

一下是一些实现了 Copy trait 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所有权与函数

将值传递给函数与给变量赋值类似,可能会移动或赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let s = String::from("hello"); // s 进入作用域

takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效

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

makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,
// 所以在后面可继续使用 x

} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 没有特殊之处(没有调用drop析构)

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
// 占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处

返回值与作用域

返回值也可以转移所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 并移出给调用的函数
}

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

引用

引用可以不获取所有权就可以使用值

1
2
3
4
5
6
7
8
fn main() {
let str = String::from("tets");
let len = get_str_len(&str);
}

fn get_str_len(str: &String) -> usize {
str.len();
}

&s1语法可以创建一个指向值s1的引用,但不拥有它,这种行为被称为借用,借用的变量不能进行修改

可变引用

1
2
3
4
5
6
7
8
fn main() {
let mut s = String::from("hello");
change(&mut s);
}

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

可以看到,在增加了mut关键字后,可以对引用值进行修改,但需要注意的是,如果拥有一个对该变量的可变引用,那就不能再对该变量创建引用,不能同一时间多次将一个值作为可变变量借用,这样的限制可以避免数据竞争,数据竞争主要由以下三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

当然,可以使用花括号来划分不同的作用域来实现同意值得多次借用,但在同一个作用域内,这样的行为不被允许。而可变引用与不可变引用也不能在同一个作用域中出现,但不可变引用可以在同一作用域出现多个,例如

1
2
3
4
let mut str = String::from("hello");
let p1 = &str;//没毛病
let p2 = &str;//没毛病
let p3 = &mut str;//有毛病

不过,一个引用的作用域是从声明的地方到最后一次使用位置,所以可以进行如下改造

1
2
3
4
5
6
7
let mut str = String::from("hello");
let p1 = &str;//没毛病
let p2 = &str;//没毛病
println!("{} and {}", p1, p2);

let p3 = &mut str;//没毛病
println!("{}", p3);

悬垂引用

我习惯称为野引用,指被借用的值的生命周期小于引用,例如

1
2
3
4
5
6
7
fn main() {
let str = get_str();
}
fn get_str() {
let s = String::from("hello");
&s
}

引用的规则

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

Slice类型

slice允许引用集合中一段连续的元素序列,而不是引用整个集合,它是一类引用,所以没有所有权。

现在假设一个场景,在一个以空格为分隔符的字符串中,找到第一个子串并返回,如果没有空格则返回其本身,在不使用slice的情况下,我们没有办法真正获取部分字符串,但可以返回索引,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word 的值为 5
s.clear(); // 这清空了字符串,使其等于 ""
// word 在此处的值仍然是 5,
// 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效!
}

这会引发一个问题,索引和字符串本身没什么关系,但凡字符串有改动,那么拿到的索引就会失效

字符串slice

字符串sliceString中一部分值的引用,写法如下:

1
2
3
4
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

不同于String的引用,通过切片,将其分为了两个引用,分别是从第0个元素到第5个元素前,第6个元素到第11个元素前,形式为[start_idx..end_idx],如果start_idx的值为0,那么也可以简写为[..end_idx],如果end_idx的值为最后一个元素的位置,那么也可写作[start_idx..],若要获取整个元素的切片,可以写作[..]

现在,试着使用切片重写获取第一个子串的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 错误!
println!("the first word is: {}", word);
}

在进行编译是,会发现sclear()会引发编译错误,这是因为clear想要清空字符串,需要获取一个可变的引用,而其下一句println!则使用了字符串中的不可变引用,rust中,可变引用和不可变引用不能同时存在,可变引用的最后一次使用应该位于不可变引用最后一次使用前

字符串面值也是slice

例如let s = "hello world";此处s的类型是&str,其是指向二进制程序特定位置的slice(静态区),所以&str是一个不可变引用

字符串slice做参数

1
2
3
4
//对于以下函数定义
fn first_word(s: &String) -> &str {
//也可写作
fn first_word(s: &str) -> &str {

对第二种写法,如果有一个字符串slice,可以传递,如果有一个String,则可以传递整个String的引用或对String的引用,即这里会进行一次隐式类型转换,在使用时可如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let my_string = String::from("hello world");

// first_word 适用于String(的 slice),部分或全部
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// first_word 也适用于 String 的引用,
// 这等价于整个 String 的 slice
let word = first_word(&my_string);

let my_string_literal = "hello world";

// first_word 适用于字符串字面值,部分或全部
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);

// 因为字符串字面值已经是字符串 slice 了,
// 这也是适用的,无需 slice 语法!
let word = first_word(my_string_literal);

其他类型slice

string差裘不多

-------------本文结束感谢您的阅读-------------