RUST 0x03 Ownership
Ownership是Rust最独特的一个特性,能够保证内存安全。
1 What's the Ownership!?
Ownership Rules
- Rust中的每一个值都有一个叫做它的owner的变量。
- 同时只能有一个owner。
- 当owner离开作用域(scope),这个值将被丢弃。
变量作用域
以string为例:
{ // s还没有被声明,不有效
let s = "hello"; // 从这里开始,s有效
// 用s搞事情
} // 这个作用域结束了,s不再有效
简单来说:
- 当s出现到作用域中,它有效。
- 直到它脱离作用域前,它一直都有效。
String
类型
为了更好地阐述Ownership的规则,我们需要一个比之前学得更复杂的数据类型。之前的数据类型都存储在栈(stack)中,并在它们的作用域结束之后从栈中弹出(pop off)。而String
存储在堆(heap)中,并通过指针(pointer)访问。
在这里我们只集中讨论String
的和ownership有关的部分,这些方面也适用于standard library中的其它复杂数据类型。
上面的例子中,s为字符串常量,它不可以改变。而且必须在代码中将字符串的值写出。用接下来的代码可以创建可变的、不必在代码中将字符串的值写出的String
:
let mut s = String::from("hello");
s.push_str(", world!");
println!("{}", s); // `hello, world!`
-
::
→ 命名空间,使from
函数具体指代String
类下的from
函数。 -
push_str()
→ 在一个String后拼接一个字符串。
为什么String
可以被改变但是字符串常量(literals)不行?区别就在这两种类型在内存中的存储方式。
内存和分配
对于字符串常量,在编译时我们便知道其内容,所以其文本将直接被写入最终的可执行文件。因此字符串常量又快又有效率。
对于String
类型,为了使其内容可变,我们需要在堆上分配一定数量(在编译时不知道)的内存来存储内容。这意味着:
- 其所需内存必须在运行时向操作系统索取。
- 需要有在用完
String
后归还内存的方法。
第一部分可以由String::from
完成。
第二部分在Rust中是自动完成的:如果owner脱离了作用域,那么内存也将自动地被归还(drop
)。
变量和数据interact的方法:Move
整数例:
let x = 5;
let y = x;
//将5绑定到x上,然后复制一份x的值并使其绑定到y上。
String
例:
let s1 = String::from("hello");
let s2 = s1;
String
类由三部分组成:指针(pointer)、长度(length)、容量(capacity)。这三部分数据都存在栈中。
其中指针指向堆中字符串的首个字母的地址,长度代表String
正在使用的内存,容量代表操作系统分配给String
的总内存量(单位都是字节)。
当我们let s2 = s1;
时,String
数据被复制了。这意味着我们复制的是指针、长度、容量,而不是在堆中的字符串本身。
但是之前我们说过,当一个变量脱离作用域时,Rust会自动归还其内存。但是这里我们的s1
和s2
都指向同一个内存,那么这会不会引起double free错误呢?
为了保证内存安全,在进行let s2 = s1;
之后,Rust就认为s1
不再有效,因此当s1
脱离作用域时也不必归还内存。如:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
编译时将会抛出一个CE。
因为在复制指针、长度、容量之后,s1
不再有效,所以我们可以说是s1
被移动(move)到了s2
。
变量和数据interact的方法:Clone
如果我们想要复制的是String
在堆中的数据,而不仅仅是栈中的数据,我们可以用一个常用方法clone
。如:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
此时s2
是s1
的一份复制,包括栈中的数据与堆中的数据,此时s1
仍然有效。但是要付出运行效率的代价。
Stack-Only Data: Copy
以下的一段代码能够运行,而且有效:
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
但是这看起来好像和之前所说的相悖:我们没有用clone
,但是x
仍然有效,没有被移动到y
。
原因是,像整数类型这种在编译时就有固定的size的数据类型,完全被存储在栈上,因此创建副本很方便快捷。所以让x
在被y
复制后仍然有效对运行效率的影响并不大,也没理由不这么做。所以调用clone
与否并没有多大区别,也不必管它。(当然,要是愿意的话,恁也可以写let y = x.clone()
)
在Rust中,Stack-Only Data有一个叫做Copy
的特性(trait)。如果一个类型有Copy
特性,那么被用来赋值(assign)的变量仍然有效。如果一个变量脱离了作用域,我们又想对它使用Copy
,那么我们会得到一个CE。
几种常见的可Copy
类型:
- 所有整数类型,比如
u32
- 逻辑类型
bool
,包括true
和false
- 所有浮点类型,比如
f64
- 字符类型
char
- 只包含可
Copy
类型的Tuple,比如(i32, i32)
可Copy
,而(i32, String)
不可以。 - 数组与Tuple同理
Ownership与函数
将一个值传递给一个函数的原理与将一个值赋值给一个变量是相似的,要么move,要么copy
。如:
fn main() {
let s = String::from("hello"); // s进入作用域
takes_ownership(s); // s的值被move到函数里,因此在这里不再有效
let x = 5; // x进入作用域
makes_copy(x); // x被move到函数里,但是因为i32可Copy,因此x仍然有效
} // x脱离作用域。然后是s,但是因为s的值已经被move了,所以没有什么特别的发生
fn takes_ownership(some_string: String) { // some_string进入作用域
println!("{}", some_string);
} // some_string脱离作用域,内存释放
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // some_integer脱离作用域,没有什么特别的发生
如果我们尝试在调用takes_ownership()
后使用s
,则会抛出一个CE。
返回值和域
返回值也可以转移ownership,比如:
fn main() {
let s1 = gives_ownership(); // gives_ownership()将它的返回值move到s1
let s2 = String::from("hello"); // s2进入作用域
let s3 = takes_and_gives_back(s2); // s2被move到takes_and_gives_back()里,
// takes_and_gives_back()将它的返回值move到s3
} // s3脱离作用域并释放内存。s2脱离作用域,但是因为它被move,因此没有什么特别的发生。
// s1脱离作用域并释放内存。
fn gives_ownership() -> String { // gives_ownership会将它的返回值move到
// 调用它的函数里
let some_string = String::from("hello"); // some_string进入作用域
some_string // some_string被return,并被move到调用的函数
}
fn takes_and_gives_back(a_string: String) -> String { // a_string进入作用域
a_string // a_string被return, 并被move到调用的函数
}
简单来说,当一个值被赋值给另一个变量时,这个值被move。当一个在堆上有数据的变量脱离作用域时,它的值会被drop
清除,除非它被move
到另一个变量上。
如果我们想要让一个函数在不取走ownership的情况下使用一个值怎么办?一个方法是返回传递的值——将它与函数的返回值一起返回,如返回一个tuple:
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(); // len()返回一个String的长度(length)
(s, length)
}
但这太麻烦了……
2 引用与借用(References&Borrowing)
利用引用可以方便地让函数在不取走ownership的情况下使用一个值。如:
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是一个String类的引用
s.len()
} // s脱离作用域。但是由于它没有它指向的值的ownership,什么都不会发生
-
&
→ references
引用可以让恁在不取走ownership的情况下使用一个值。
我们称呼这种将引用作为函数参数的行为为借用(Borrowing)。
但是如果想要修改正在借用的值,则会抛出一个CE。
就像变量默认是不可变的,引用默认也不可变。
可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
为了使引用可变,首先,我们得使s
变成mut
,然后用&mut s
创建一个可变引用,然后在函数的参数列表里写some_string: &mut String
来接受它。
但是可变引用有一个很大的限制:在一个特定的域中只能有对一段特定的数据的可变引用。如:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
这样创建两个引用则会在编译时抛出一个CE。
Rust中有这种限制,是为了防止data race:
- 两个以上的指针同时指向同一个数据。
- 至少一个指针在被用来对数据进行写入。
- 没有同时access数据的机制。
Data races会引起难以检查判断的RE,为了防止这个问题,Rust直接在编译时就将它们报错。
我们可以运用{}
创建多个不同时的可变引用:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1脱离了作用域,所以我们可以创建一个新的可变引用
let r2 = &mut s;
相似地,我们不能在有一个不可变引用的同时创建一个可变引用,但是可以同时创建多个不可变引用:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
此外,一个引用的作用域从它被创建开始,一直到它最后一次被使用结束。
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1和r2此后没有再被使用
let r3 = &mut s; // no problem
println!("{}", r3);
r1
、r2
与r3
的作用域没有交叉,所以可以通过编译。
Dangling References
在其他的一些带有指针的语言中,很容易不小心创建一个dangling pointer,一个指向 某段已经被someone else使用的内存 的引用。在Rust中,编译器会保证在引用脱离作用域前,被引用的数据不会脱离作用域。如以下一段代码会被抛出CE:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
解决方法是直接返回s
,而不是它的引用。
引用的规则(The Rules of References)
- 在任一时间,同时只能有一个可变引用或者多个不可变引用。
- 引用必须是有效的。
3 切片(Slice)类型
另一种没有ownership的数据类型是slice,切片可以让恁引用数据的某一段连续的部分而不是整个数据。
比如返回一个字符串的第一个单词的函数:
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()
}
因为我们要在String
中一位一位地查找空格,我们将String
用as_bytes
方法转换为一列字节。否则将会抛出CE。
-
.as_bytes()
→ as bytes -
.iter().enumerate()
→ 将.iter()
的结果用一个tuple(index, reference)
包起来返回 -
b''
→ byte literal syntax,将字符byte化
但是如果是要查找第二个单词呢?如果在查找完之后字符串又发生了改变呢?
字符串切片
String slice是对String
的一个部分的引用,如:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
我们可以用[starting_index..ending_index]
(左闭右开)创建切片,也就是说切片的数据结构存储了切片开始的位置与切片长度(length == ending_index - starting_index)。
其中&s[0..5]
可以缩写为&s[..5]
。此外,&s[3..len]
可以缩写为&s[3..]
,&s[0..len]
可以缩写为&s[..]
。
运用切片重写上面的函数:
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[..]
}
由于返回的其实是一个s
的不可变引用,因此在后续操作里不会有我们上面所担心的错误。
字符串是切片
回忆这句代码:
let s = "Hello, world!";
这里的s
的类型其实是&str
,其实是一个切片,这也就是字符串不可变的原因——&str
是不可变引用。(注意:&str
!=&String
)
将字符串切片作为参数
在知道了我们可以对字符串和String
取切片后,我们可以将下面的代码进行改进:
fn first_word(s: &String) -> &str {
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
fn first_word(s: &str) -> &str {
如果我们要传递的是一个字符串切片,我们可以直接传递它。如果我们要传递的是一个String
,我们可以传递整个String
作为切片[..]
。这样改进可以使我们的API更通用。
其它切片
还有其它更通用的切片类型,如:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
这个切片的类型是&[i32]
,机制和字符串切片一样——存储起始点和长度。
小结
ownership、借用和切片的概念保证了内存的安全。
参考
The Rust Programming Language by Steve Klabnik and Carol Nichols, with contributions from the Rust Community : https://doc.rust-lang.org/book/