若是说前面的坑咱们一直在用小铲子挖的话,那么今天的坑就是用挖掘机挖的。git
今天要介绍的是Rust的一个核心概念:Ownership。全文将分为何是Ownership以及Ownership的传递类型两部分。github
每种编程语言都有本身的一套内存管理的方法。有些须要显式的分配和回收内存(如C),有些语言则依赖于垃圾回收器来回收不使用的内存(如Java)。而Rust不属于以上任何一种,它有一套本身的内存管理规则,叫作Ownership。编程
在具体介绍Ownership以前,我想要先声明一点。Rust入坑指南:常规套路一文中介绍的数据类型,其数据都是存储在栈中。而像String或一些自定义的复杂数据结构(咱们之后会对它们进行详细介绍),其数据则存储在堆内存中。明确了这一点后,咱们来看下Ownership的规则有哪些。数组
这三条规则很是重要,记住他们会帮助你更好的理解本文。安全
Ownership的规则中,有一条是owner超过范围后,值会被销毁。那么owner的范围又是如何定义的呢?在Rust中,花括号一般是变量范围做用域的标志。最多见的在一个函数中,变量s的范围从定义开始生效,直到函数结束,变量失效。数据结构
fn main() { // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
复制代码
这个这和其余大多数编程语言很像,对于大多数编程语言,都是从变量定义开始,为变量分配内存。而回收内存则是八仙过海各显神通。对于有依赖GC的语言来讲,并不须要关心内存的回收。而有些语言则须要显式回收内存。显式回收就会存在必定的问题,好比忘记回收或者重复回收。为了对开发者更加友好,Rust使用自动回收内存的方法,即在变量超出做用域时,回收为该变量分配的内存。并发
前面咱们提到,花括号一般是变量做用域隔离的标志(即Ownership失效)。除了花括号之外,还有其余的一些状况会使Ownership发生变化,先来看两段代码。编程语言
let x = 5;
let y = x;
println!("x: {}", x);
复制代码
let s1 = String::from("hello");
let s2 = s1;
println!("s1: {}", s1);
复制代码
做者注:双冒号是Rust中函数引用的标志,上面的意思是引用String中的from函数,这个函数一般用来构建一个字符串对象。函数
这两段代码看起来惟一的区别就是变量的类型,第一段使用的是整数型,第二段使用的是字符串型。而执行结果倒是第一段能够正常打印x的值,第二段却报错了。这是什么缘由呢?ui
咱们来分析一下代码。对于第一段代码,首先有个整数值5,赋给了变量x,而后把x的值copy了一份,又赋值给了y。最后咱们成功打印x。看起来比较符合逻辑。实际上Rust也是这么操做的。
对于第二段代码咱们想象中,也能够是这样的过程,但实际上Rust并非这样作的。先来讲缘由:对于较大的对象来讲,这样的复制是很是浪费空间和时间的。那么Rust中实际状况是怎么样呢?
首先,咱们须要了解Rust中String类型的结构:
上图中左侧是String对象的结构,包括指向内容的指针、长度和容量。这里长度和容量相同,咱们暂时先不关注。后面详细介绍String类型时会提到二者的区别。这部份内容都存储在栈内存中。右侧部分是字符串的内容,这部分存储在堆内存中。
有的朋友可能想到了,既然复制内容会形成资源浪费,那我只复制结构这部分好了,内容再多,我复制的内容长度也是可控的,并且也是在栈中复制,和整数类型相似。这个方法听起啦不错,咱们来分析一下。按照上面这种说法,内存结构大概是这个样子。
这种会有什么问题呢?还记得Ownership的规则吗?owner超出做用域时,回收其数据所占用的内存。在这个例子中,当函数执行结束时,s1和s2同时超出做用域,那么上图中右侧这块内存就会被释放两次。这也会产生不可预知的bug。
Rust为了解决这一问题,在执行let s2 = s1;
这句代码时,认为s1已经超出了做用域,即右侧的内容的owner已经变成了s2,也能够说s1的ownership转移给了s2。也就是下图所示的状况。
若是你确实须要深度拷贝,即复制堆内存中的数据。Rust也能够作到,它提供了一个公共方法叫作clone。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
复制代码
clone的方法执行后,内存结构以下图:
前面咱们聊到的是Ownership在String之间转移,在函数间也是同样的。
fn main() {
let s = String::from("hello"); // s 做用域开始
takes_ownership(s); // s's 的值进入函数
// ... s在这里已经无效
} // s在这以前已经失效
fn takes_ownership(some_string: String) { // some_string 做用域开始
println!("{}", some_string);
} // some_string 超出做用域并调用了drop函数
// 内存被释放
复制代码
那有没有办法在执行takes_ownership函数后使s继续生效呢?通常咱们会想到在函数中将ownership还回来。而后很天然的就想到咱们以前介绍的函数的返回值。既然传参能够转移ownership,那么返回值应该也能够。因而咱们能够这样操做:
fn main() {
let s1 = String::from("hello"); // s2 comes into scope
let s2 = takes_and_gives_back(s1); // s1 被转移到函数中
// takes_and_gives_back,
// 将ownership还给s2
} // s2超出做用域,内存被回收,s1在以前已经失效
// takes_and_gives_back 接收一个字符串而后返回一个
fn takes_and_gives_back(a_string: String) -> String { // a_string 开始做用域
a_string // a_string 被返回,ownership转移到函数外
}
复制代码
这样作是能够实现咱们的需求,可是有点太麻烦了,幸亏Rust也以为这样很麻烦。它为咱们提供了另外一种方法:引用(references)。
引用的方法很简单,只须要加一个&
符。
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()
}
复制代码
这种形式能够在没有ownership的状况下访问某个值。其原理以下图:
这个例子和咱们在前面写的例子很类似。仔细观察会发现一些端倪。主要有两点不一样:
咱们把函数中接收引用的参数称为借用。就像实际生活中我写完了做业,能够借给你抄一下,但它不属于你,抄完你还要还给我。(友情提示:非紧急状况不要抄做业)
另外还须要注意,个人做业能够借给你抄,可是你不能改我写的做业,我原本写对了你给我改错了,之后我还怎么借给你?因此,在calculate_length中,s是不能够修改的。
若是我发现我写错了,让你帮我改一下怎么办?我受权给你,让你帮忙修改,你也须要表示能帮我修改就能够了。Rust也有办法。还记得咱们前面介绍的可变变量和不可变变量吗?引用也是相似,咱们可使用mut关键字使引用可修改。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
复制代码
这样,咱们就能在函数中对引用的值进行修改了。不过这里还要注意一点,在同一做用域内,对于同一个值,只能有一个可修改的引用。这也是由于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);
复制代码
Rust编译器会在第一个print语句以后判断出r1和r2不会再被使用,此时r3尚未建立,它们的做用域不会有交集。因此这段代码是合法的。
对于可操做指针的编程语言来说,最使人头疼的问题也许就是空指针了。一般状况是,在回收内存之后,又使用了指向这块内存的指针。而Rust的编译器帮助咱们避免了这个问题(再次感谢Rust编译器)。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
复制代码
来看一下上面这个例子。在dangle函数中,返回值是字符串s的引用。可是在函数结束时,s的内存已经被回收了。因此s的引用就成了空指针。此时就会报expected lifetime parameter的编译错误。
除了引用以外,还有另外一种没有ownership的数据类型叫作Slice。Slice是一种使用集合中一段序列的引用。
这里经过一个简单的例子来讲明Slice的使用方法。假设咱们须要获得给你字符串中的第一个单词。你会怎么作?其实很简单,遍历每一个字符,若是遇到空格,就返回以前遍历过的字符的集合。
对字符串的遍历方法我来剧透一下,as_bytes函数能够把字符串分解成字节数组,iter是返回集合中每一个元素的方法,enumerate是提取这些元素,而且返回(元素位置,元素值)这样的二元组的方法。这样是否是能够写出来了。
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()
}
复制代码
来,感觉下这个例子,虽然它返回的是第一个空格的位置,可是只要会字符串截取,仍是能够达到目的的。不过不能剧透字符串截取了,否则暴露不出问题。
这么写的问题在哪呢?来看一下main函数。
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear();
}
复制代码
这里在获取空格位置后,对字符串s作了一个clear操做,也就是把s清空了。但word仍然是5,此时咱们再去对截取s的前5个字符就会出问题。可能有人认为本身不会这么蠢,可是你愿意相信你的好(zhu)伙(dui)伴(you)也不会这么作吗?我是不相信的。那怎么办呢?这时候slice就要登场了。
使用slice能够获取字符串的一段字符序列。例如&s[0..5]
能够获取字符串s的前5个字符。其中0为起始字符的位置下标,5是结束字符位置的下标加1。也就是说slice的区间是一个左闭右开区间。
slice还有一些规则:
&s[0..2]
和&s[..2]
等价&s[3..len]
和&s[3..]
等价&s[0..len]
和&s[..]
等价这里须要注意的是,咱们截取字符串时,其边界必须是UTF-8字符。
有了slice,就能够解决咱们的问题了
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[..]
}
复制代码
如今咱们在main函数中对s执行clear操做时,编译器就不一样意了。没错,又是万能的编译器。
除了slice除了能够做用于字符串之外,还能够做用于其余集合,例如:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
复制代码
关于集合,咱们之后会有更加详细的介绍。
本文介绍的Ownership特性对于理解Rust来说很是重要。咱们介绍了什么是Ownership,Ownership的转移,以及不占用Ownership的数据类型Reference和Slice。
怎么样?是否是感受今天的坑很是给力?若是以前在地下一层的话,那如今已经到地下三层了。因此请各位注意安全,有序降落。