什么是所有权
所有程序在运行时都必须管理其使用计算机内存的方式:
而Rust则是通过所有权系统管理内存:
在很多语言中,程序员不需要经常考虑到堆和栈,但在Rust这样的系统编程语言中,一个值存储在heap上还是stack上,会很大程度上影响语言的行为,所以这里先对堆和栈进行简单介绍。
分配内存
栈(stack):
堆(heap):
访问数据
所有权存在的原因
所有权存在的原因,就是为了管理存放在heap上的数据:
所有权规则
所有权的规则如下:
变量作用域
在下面的代码中,变量s从第三行声明开始变得可用,在第五行代码块结束时离开作用域变得不可用。如下:
fn main() {
//s不可用
let s = "hello"; //s可用
//可以对s进行相关操作
} //s作用域到此结束,s不再可用
String类型
为了后续讲解Rust的所有权,我们需要借助一个管理的数据存储在heap上的类型,这里选择String类型。
String类型由三部分组成:
String类型的这三部分数据存储在stack上,而String管理的字符串则存储在heap上。如下:
创建String字符串
创建String字符串可以使用from函数,该函数可以基于字符串字面值来创建String字符串。如下:
fn main() {
let mut s = String::from("Hello");
s.push_str(" String");
println!("{}", s); //Hello String
}
说明一下:
::
,表示from是String类型的命名空间下的函数。内存与分配
注:在Rust中,当某个值离开作用域时,会自动调用drop函数释放内存。
变量与数据交互的方式:移动(Move)
在Rust中,多个变量可以采取不同的方式与同一数据进行交互。如下:
fn main() {
let x = 10;
let y = x;
println!("x = {}", x); //x = 10
println!("y = {}", y); //y = 10
}
说明一下:
如果将代码中的整数换成String,那么运行程序将会产生报错。如下:
fn main() {
let x = String::from("hello");
let y = x;
println!("x = {}", x); //error
println!("y = {}", y);
}
报错的原因就是我们借用了已经被移动的值x。如下:
现在我们来分析一下代码,刚开始声明变量x的时候,整体布局如下:
当把变量x赋值给变量y时,String的数据被拷贝了一份,但拷贝的仅仅是stack上的String元数据,而并没有拷贝指针所指向的heap上的数据。如下:
当变量离开作用域时,Rust会自动调用drop函数释放内存,为了避免这种情况下heap上的数据被二次释放,因此Rust会让赋值后的变量x失效,此时当x离开作用域时就不会释放内存。如下:
这就是为什么在赋值后访问变量x就会产生报错的原因,因为此时变量x已经失效了。
说明一下:
变量与数据交互的方式:克隆(Clone)
如果确实需要对String的heap上的数据进行拷贝,那么可以使用String的clone方法。如下:
fn main() {
let x = String::from("hello");
let y = x.clone(); //深拷贝
println!("x = {}", x); //x = hello
println!("y = {}", y); //y = hello
}
拷贝后变量x和变量y都是有效的,因为String的clone方法会将stack和heap上的数据都进行拷贝。如下:
stack上的数据:拷贝(Copy)
(i32, i32)
是可Copy的,而(i32, String)
是不可Copy的。说明一下:
所有权与函数
将值传递给函数和给变量赋值的原理类似:
例如,下面代码中变量s传入函数时将发生移动,后续不再有效,而变量x传入函数时将发生拷贝,后续仍然有效。如下:
fn main() {
let s = String::from("hello world");
take_ownership(s); //发生移动
//println!("s = {}", s); //error
let x = 10;
makes_copy(x); //发生拷贝
println!("x = {}", x); //x = 10;
}
fn take_ownership(some_string: String) {
println!("{}", some_string); //hello world
}
fn makes_copy(some_number: i32) {
println!("{}", some_number); //10
}
返回值与作用域
函数在返回值的过程中同样会发生所有权的转移。如下:
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
}
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
代码说明:
引用与借用
例如,下面代码中的calculate_length函数的参数类型是&String,该函数返回传入String的长度但不获取其所有权,函数调用后传入的String变量仍然有效。如下:
fn main() {
let s1 = String::from("hello world");
let len = calculate_length(&s1);
println!("'{}'的长度是{}", s1, len); //'hello world'的长度是11
}
fn calculate_length(s: &String) -> usize {
s.len()
}
实际calculate_length函数的参数s就是一个指针,它指向了传入的实参s1。如下:
说明一下:
&
引用相反的操作是解引用,解引用运算符是*
。可变引用
引用和变量一样默认也是不可变的,要让引用变得可变,同样需要使用mut关键字。如下:
fn main() {
let mut s1 = String::from("hello world");
let len = calculate_length(&mut s1);
println!("'{}'的长度是{}", s1, len); //'hello world!!!'的长度是14
}
fn calculate_length(s: &mut String) -> usize {
s.push_str("!!!"); //修改了引用的变量
s.len()
}
但可变引用有一个重要的限制就是,在特定作用域内,一个变量只能有一个可变引用,否则会产生报错。如下:
fn main() {
let mut s = String::from("hello world");
let s1 = &mut s;
let s2 = &mut s; //error
println!("s1 = {}, s2 = {}", s1, s2);
}
Rust这样做可以在编译时就防止数据竞争,但可以通过创建新的作用域来允许非同时的创建多个可变引用,因为只要保证同一个作用域下一个变量只有一个可变引用即可。如下:
fn main() {
let mut s = String::from("hello world");
{
let s1 = &mut s;
}
let s2 = &mut s;
}
可变引用的其他限制
Rust中不允许一个变量同时拥有可变引用和不可变引用,否则会产生报错。如下:
fn main() {
let mut s = String::from("hello world");
let r1 = &s;
let r2 = &s;
let s1 = &mut s; //error
println!("{} {} {}", r1, r2, s1);
}
原因: 不可变引用的要求其引用的值不能发生改变,而可变引用却可以改变其引用的值,因此一个变量同时拥有可变引用和不可变引用,就是的不可变引用的作用失效了,但一个变量同时拥有多个不可变引用是可以的。
悬垂引用(Dangling References)
悬垂引用指的是,一个指针引用了内存中的某个地址,但这块内存可能已经释放了。如下:
fn main() {
let r = dangle();
}
fn dangle() -> &String {
let s = String::from("hello world");
&s //悬垂引用
}
在Rust中编译器确保引用永远不会变成悬垂状态,因为编译器会确保数据不会在其引用之前离开作用域,因此上述代码会编译报错。如下:
说明一下: 报错内容说缺少一个生命周期说明符,生命周期相关的内容会在后续博客中讲解。
引用的规则
引用的规则如下:
字符串切片
&字符串变量名[开始索引..结束索引]
。例如,下面分别创建了字符串hello world
的hello
的切片和world
的切片。如下:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("hello = {}", hello); //hello = hello
println!("world = {}", world); //world = world
}
切片中包含一个指针和一个长度,比如上述的world
切片,其指针指向字符串索引为6的位置,其长度就是5。如下:
切片在Rust中是非常有用的,比如获取字符串中的第一个单词,那么借助字符串切片可以编写出如下代码:
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
//s.clear(); //error: s已经存在一个不可变引用
println!("word = {}", word); //word = hello
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
说明一下:
&
。注意:
字符串字面值就是切片
字符串字面值的类型实际上就是字符串切片&str
,这就是为什么字符串字面值不可变的原因,因为字符串切片&str
就是不可变的。如下:
fn main() {
let s = "hello world"; //s的类型是&str
}
将字符串切片作为参数
如果要将字符串切片作为函数的参数,那么最好将函数的参数类型定义为&str
,而不是&String
,这样就能同时接收&String和&str的参数了,能够使我们的API更加通用且不会损失任何功能。如下:
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string); //接收&String
let my_string_literal = "hello world";
let word = first_word(my_string_literal); //接收&str
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
说明一下:
其他类型的切片
与字符串切片类似,其他类型也可以有切片,比如对于下面代码中的数组来说,其切片类型就是&[i32]
。如下:
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; //&[i32]类型
}