全部权是 rust 语言独有的特性,它保证了在没有垃圾回收机制下的内存安全,因此理解 rust 的全部权是颇有必要的。接下来,咱们来讨论全部权和它的几个特性:借用、切片和内存结构。编程
Rust 的核心特性是全部权。各类语言都有它们本身管理内存的方式,有些是使用垃圾回收机制,有些是手动管理内存,而 rust 使用的是全部权机制来管理内存。数组
全部权规则以下:安全
不懂不要紧,跳过日后看。编程语言
rust 语言的变量做用域和其余语言是相似的,看例子:函数
{ // 变量 s 尚未被声明,s 在这里是无效的 let s = "hello"; // 变量 s 是这里声明的,从这里开始生效 // 从这里开始,可使用 s 作一些工做 } // 变量 s 超出做用域,s 从这里开始再也不生效
能够总结两点重要特性:学习
在章节三中学习的数据类型都是存储在内存的栈空间中,当它们的做用域结束时清空栈空间,咱们如今学习一下内存的堆空间中存储的数据是在什么时候被 rust 清空的。编码
咱们在这里使用 String 类型做为例子,固然只是简单的使用,具体的内容后文介绍。操作系统
let s = "hello";
这个例子是把 hello 字符串硬编码到程序中,咱们把它叫作 字符串文字
(string literals 我不知作别人是怎么翻译的,我实在想不到合适的词,先这样叫着吧),字符串文字很方便,可是它不能适用于任何场景,好比咱们想要输入一个文本串的时候,原理以下:翻译
let s = String::from("hello");
这种双冒号的语法细节下个章节再说,这里先聚焦于字符串,例子中建立的字符串是能够改变的,好比:指针
let mut s = String::from("hello"); s.push_str(", world!"); // push_str() 在上个字符串后追加一个字符串 println!("{}", s); // 这里会打印 `hello, world!`
字符串文字是在编译期间就有肯定的字符内容,因此文本能够直接硬编码到程序中,这就是字符串文字快捷方便的缘由。可是字符串文字又是不可变的,咱们没办法分配内存给编译期间未知大小及变化的字符串。
字符串类型则支持字符串的修改和增加,即便编译期间未知大小,也能够在堆内存分配一块空间用于存储数据,这意味着:
rust 则是另外一种方式:一旦变量超出做用域,程序自动返还内存,经过下面的例子来看这个概念:
{ let s = String::from("hello"); // s 从这里开始生效 // 利用 s 作一些事情 } // s 超出做用域,再也不生效
当 s 超出做用域,rust 会自动帮咱们调用一个特殊的函数—— drop 函数,它是用于返还内存的,当程序执行到 大括号右半块 } 的时候自动调用该函数。
在 rust 中,可使用不一样的方式在多个变量间交互相同的数据,好比:
let x = 5; // 把 5 绑定到 x 上 let y = x; // 把 x 的值复制给 y,此时,x 和 y 的值都是 5
再好比:
let s1 = String::from("hello"); // 建立一个字符串绑定到 s1 上 let s2 = s1; // 把 s1 的值移动给 s2,此时,s1 就失效了,只有 s2 是有效的
s1 为何失效,由于字符串是存储在堆内存中的,这里只是把栈内存中的 s1 的数据移动给 s2,堆内存不变,这种方式叫作浅克隆,也叫移动。若是想让 s1 仍然有效,可使用深克隆。
关于深克隆,咱们直接看例子吧:
let s1 = String::from("hello"); // 建立一个字符串绑定到 s1 上 let s2 = s1.clone(); // 把 s1 的值克隆给 s2,此时,s1 和 s2 都是有效的 println!("s1 = {}, s2 = {}", s1, s2); // 打印 s1 和 s2
下一章节再详细介绍这种语法。
咱们再回到前面的例子中:
let x = 5; // 把 5 绑定到 x 上 let y = x; // 把 x 的值复制给 y,此时,x 和 y 的值都是 5 println!("x = {}, y = {}", x, y); // 打印 x 和 y
这里没有使用 clone 这个方法,可是 x 和 y 都是有效的。由于 x 和 y 都是整型,整型存储在栈内存中,即便调用了 clone 方法,也是作相同的事。下面总结一下复制的类型:
这里直接放一个例子应该就说清楚了,以下:
fn main() { let s = String::from("hello"); // s 进入做用域 takes_ownership(s); // s 移动到函数里 // s 从这里开始再也不生效,若是还使用 s,则会在编译期报错 let x = 5; // x 进入做用域 makes_copy(x); // x 复制(移动)到函数里, // 但因为 x 是 i32 ,属于整型,仍有效 // 使用 x 作一些事情 } // x 超出做用域,因为 s 被移动到了函数中,这里再也不释放 s 的内存 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 超出做用域,可是整型不须要释放堆内存
这块也直接放个例子,以下:
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 { // gives_ownership 将移动它的返回值给调用者 let some_string = String::from("hello"); // 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 是返回值,移出调用函数 }
前面都是把值传入传出函数,这里咱们学习一下引用,看个例子:
fn main() { let s1 = String::from("hello"); // 建立一个字符串 s1 let len = calculate_length(&s1); // 把 字符串 s1 的引用传给函数 println!("The length of '{}' is {}.", s1, len); // 这里能够继续使用 s1 } fn calculate_length(s: &String) -> usize { // 接收到 字符串 s1 的引用 s s.len() // 这里返回函数的长度 } // s 超出做用域,可是这里没有字符串的全部权,不释放内存
&s1 语法是建立一个指向 s1 的值的引用,而不是 s1 自己,当引用超出做用域不会释放引用指向值的内存。被调用函数声明参数的时候,参数的类型也须要使用 & 来告知函数接收的参数是个引用。
在上述例子中,若是在 calculate_length 函数中修改字符串的内容,编译器会报错,由于传入的引用在默认状况下是不可变引用,若是想要修改引用的内容,须要添加关键字 mut,看例子:
fn main() { let mut s = String::from("hello"); change(&mut s); // mut 赞成函数修改 s 的值 } fn change(some_string: &mut String) { // mut 声明函数须要修改 some_string 的值 some_string.push_str(", world"); // 追加字符串 }
可变引用有一个很大的限制:在特定做用域中针对同一引用,只能有一个可变引用。好比:
let mut s = String::from("hello"); let r1 = &mut s; // 这是第一个借用的可变引用 let r2 = &mut s; // 这是第二个借用的可变引用,这里会编译不经过 println!("{}, {}", r1, r2);
这个限制的好处是编译器能够编译期间阻止数据竞争,数据竞争发生在以下状况:
数据竞争会形成不可预知的错误,并且在运行时修复是很困难的,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; // 不可变引用,没问题 let r2 = &s; // 不可变引用,没问题 let r3 = &mut s; // 可变引用,会发生大问题,由于后面还在使用 r1 和 r2 println!("{}, {}, and {}", r1, r2, r3);
当存在不可变引用时,就不能再借用可变引用了。不可变引用不会修改引用的值,因此能够借用多个不可变引用。可是若是不可变引用都不使用了,就又能够借用可变引用了,好比:
let mut s = String::from("hello"); let r1 = &s; // 不可变引用,没问题 let r2 = &s; // 不可变引用,没问题 println!("{} and {}", r1, r2); // r1 和 r2 从这里开始再也不使用了 let r3 = &mut s; // 可变引用,也没问题了,由于后面没使用 r1 和 r2 的了 println!("{}", r3);
在一些指针语言中,很容易就会错误地建立悬空指针。悬空指针就是过早地释放了指针指向的内存,也就是说,堆内存已经释放了,而指针还指向这块堆内存。在 rust 中,编译器能够保证不会产生悬空引用:若是有引用指向数据,编译器会确保引用指向的数据不会超出做用域,好比:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { // 这里但愿返回字条串的引用 let s = String::from("hello"); // 建立字符串 &s // 这里返回了字符串的引用 } // 这里会把字符串的内存释放,由于 s 在这里超出做用域
这个函数作个简单的修改就行了,看以下例子:
fn no_dangle() -> String { // 不返回字符串引用了,直接返回字符串 let s = String::from("hello"); s // 返回字符串 }
引用的规则以下:
另外一个没有全部权的数据类型是切片。切片是集合中相邻的一系列元素,而不是整个集合。
作个小小的编程题目:有一个函数,输入一个英文字符串,返回第一个单词。若是字符串没有空格,则认为整个字符串是一个单词,返回整个字符串。
咱们来思考一下这个函数结构:
fn first_word(s: &String) -> ? // 这里应该返回什么
在这个函数中,字符串引用做参数,函数没有字符串的全部权,那咱们应该返回什么呢?咱们不能返回字符串的一部分,那么,咱们能够返回第一个单词结束位置的索引。看实现:
fn first_word(s: &String) -> usize { // 返回一个无符号整型,由于索引不会小于 0 let bytes = s.as_bytes(); // 把字符串转换化字节类型的数组 // iter 方法用于遍历字节数组,enumerate 方法用于返回一个元组,元组的第 0 个元素是索引,第 1 个元素是字节数组的元素 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,可是咱们不能获得单词 hello 了,这里使用 word,编译器也不会报错,可是这真的是一个 bug }
还有一个问题,若是咱们想获得第二个单词,应该怎么办?函数声明应该是:
fn second_word(s: &String) -> (usize, usize) {
若是想获得不少个单词,又应该怎么办?
字符串切片就是字符串一部分的引用,以下:
let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11];
就是取字符串 s 中的一部分组成一个新的变量,取值区间左闭右开,就是说,包括左边的索引,不包括右边的索引。
若是左边索引是0,可省略:
let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; // 和上一行等价
若是右边索引是字符串末尾,可省略:
let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; // 和上一行等价
若是取整个字符串,能够把两边都省略:
let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &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[..] }
这样就直接返回了第一个单词的内容。若是要返回第二个单词,可写成:
fn second_word(s: &String) -> &str {
咱们再来看一下字符串清空的问题是否还存在:
fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // 不可变借用在这里声明 s.clear(); // 这里会报错,由于这里在修改 s 的内容 println!("the first word is: {}", word); // 不可变借用在这里使用 }
不可变借用的声明和使用之间是不能使用可变借用的。
前面咱们讨论过字符串文字的存储问题,如今咱们学习了切片,咱们能够理解字符串文字了:
let s = "Hello, world!";
s 的类型是 &str,这是一个指向了二进制程序特殊的位置的切片,这就是字符串文字不可变的原是,&str 是一个不可变引用
前面学习了字符串文字切片和字符串类型切片,咱们来提升 first_word 函数的质量。有经验的 rust 开发者会把函数参数类型写成 &str,由于这样可使得 &String 和 &str 使用相同的函数。好像不太好理解,直接上例子:
fn main() { let my_string = String::from("hello world"); // 建立字符串 // first_word 使用 &String切片 let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // 建立字符串 // first_word 使用 &str切片 let word = first_word(&my_string_literal[..]); // 由于字符串文字已是字符串切片了,能够不使用切片语法 let word = first_word(my_string_literal); }
咱们只举一个 i32 类型切片的例子吧:
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // 值是: [2, 3]
rust 使用了全部权、借用、切片,在编译期确保程序的内存安全。rust 语言提供了和其余编程语言相同的方式来控制内存,而不须要咱们编写额外代码来手动管理内存,当数据超出全部者的做用域就会被自动清理。
欢迎阅读单鹏飞的学习笔记