公司网站建设企划书,网站建设价格优 秀乐云seo,市政二级总承包资质承包范围,苏州公司注册地址所有权 所有权的认识移动#xff0c;克隆#xff0c;拷贝所有权与函数返回值与作用域 引用与借用可变引用悬垂引用#xff08;Dangling References#xff09; Slice类型 所有权的认识
所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制#… 所有权 所有权的认识移动克隆拷贝所有权与函数返回值与作用域 引用与借用可变引用悬垂引用Dangling References Slice类型 所有权的认识
所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制在程序运行时有规律地寻找不再使用的内存在另一些语言中程序员必须亲自分配和释放内存。
Rust 则选择了第三种方式通过所有权系统管理内存编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则程序都不能编译。在运行时所有权系统的任何功能都不会减慢程序。
所有权规则 首先让我们看一下所有权的规则。当我们通过举例说明时请谨记这些规则 Rust 中的每一个值都有一个 所有者owner。 值在任一时刻有且只有一个所有者。 当所有者变量离开作用域这个值将被丢弃。 移动克隆拷贝
fn main() {let s1 String::from(hello);let s2 s1;
}这种方式叫浅拷贝与OC里的概念一致示意图如下 两个指针指向同一地址引发一个问题
当 s2 和 s1 离开作用域它们都会尝试释放相同的内存。这是一个叫做 二次释放double free的错误也是之前提到过的内存安全性 bug 之一。两次释放相同内存会导致内存污染它可能会导致潜在的安全漏洞。
fn main() {let s1 String::from(hello);let s2 s1;println!({}, world!, s1);
}如果你在其他语言中听说过术语 浅拷贝shallow copy和 深拷贝deep copy那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了这个操作被称为 移动move而不是叫做浅拷贝。上面的例子可以解读为 s1 被 移动 到了 s2 中。那么具体发生了什么如图所示。 这样就解决了我们的问题因为只有 s2 是有效的当其离开作用域它就释放自己的内存完毕。
另外这里还隐含了一个设计选择Rust 永远也不会自动创建数据的 “深拷贝”。因此任何 自动 的复制都可以被认为是对运行时性能影响较小的。
如果我们 确实 需要深度复制 String 中堆上的数据而不仅仅是栈上的数据可以使用一个叫做 clone 的通用函数。第五章会讨论方法语法不过因为方法在很多语言中是一个常见功能所以之前你可能已经见过了。
这是一个实际使用 clone 方法的例子
fn main() {let s1 String::from(hello);let s2 s1.clone();println!(s1 {}, s2 {}, s1, s2);
}当出现 clone 调用时你知道一些特定的代码被执行而且这些代码可能相当消耗资源。你很容易察觉到一些不寻常的事情正在发生。
我们再来看这段代码
fn main() {let x 5;let y x;println!(x {}, y {}, x, y);
}这段代码似乎与我们刚刚学到的内容相矛盾没有调用 clone不过 x 依然有效且没有被移动到 y 中。
原因是像整型这样的在编译时已知大小的类型被整个存储在栈上所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说这里没有深浅拷贝的区别所以这里调用 clone 并不会与通常的浅拷贝有什么不同我们可以不用管它。
Rust 有一个叫做 Copy trait 的特殊注解可以用在类似整型这样的存储在栈上的类型上第十章将会详细讲解 trait。如果一个类型实现了 Copy trait那么一个旧的变量在将其赋值给其他变量后仍然可用。
Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy 注解将会出现一个编译时错误。要学习如何为你的类型添加 Copy 注解以实现该 trait请阅读附录 C 中的 “可派生的 trait”。
那么哪些类型实现了 Copy trait 呢你可以查看给定类型的文档来确认不过作为一个通用的规则任何一组简单标量值的组合都可以实现 Copy任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。如下是一些 Copy 的类型
所有整数类型比如 u32。 布尔类型bool它的值是 true 和 false。 所有浮点数类型比如 f64。 字符类型char。 元组当且仅当其包含的类型也都实现 Copy 的时候。比如(i32, i32) 实现了 Copy但 (i32, String) 就没有。
所有权与函数
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 的值已被移走// 没有特殊之处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 移出作用域。没有特殊之处当尝试在调用 takes_ownership 后使用 s 时Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在 main 函数中添加使用 s 和 x 的代码来看看哪里能使用它们以及所有权规则会在哪里阻止我们这么做。
返回值与作用域
返回值也可以转移所有权。下面展示了一个返回了某些值的示例
fn main() {let s1 gives_ownership(); // gives_ownership 将返回值// 转移给 s1let 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 被清理掉除非数据被移动为另一个变量所有。 虽然这样是可以的但是在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果我们想要函数使用一个值但不获取所有权该怎么办呢如果我们还要接着使用它的话每次都传进去再返回来就有点烦人了除此之外我们也可能想返回函数体中产生的一些数据。
我们可以使用元组来返回多个值
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() 返回字符串的长度(s, length)
}但是这未免有些形式主义而且这种场景应该很常见。幸运的是Rust 对此提供了一个不用获取所有权就可以使用值的功能叫做 引用references。
引用与借用
示例中的元组代码有这样一个问题我们必须将 String 返回给调用函数以便在调用 calculate_length 后仍能使用 String因为 String 被移动到了 calculate_length 内。相反我们可以提供一个 String 值的引用reference。引用reference像一个指针因为它是一个地址我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同引用确保指向某个特定类型的有效值。
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()
}首先注意变量声明和函数返回值中的所有元组代码都消失了。其次注意我们传递 s1 给 calculate_length同时在函数定义中我们获取 String 而不是 String。这些 符号就是 引用它们允许你使用值但不获取其所有权。 注意与使用 引用相反的操作是 解引用dereferencing它使用解引用运算符*。我们将会在第八章遇到一些解引用运算符并在第十五章详细讨论解引用。 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()
}**我们将创建一个引用的行为称为 借用borrowing。**正如现实生活中如果一个人拥有某样东西你可以从他那里借来。当你使用完毕必须还回去。我们并不拥有它。
fn main() {let s String::from(hello);change(s);
}fn change(some_string: String) {some_string.push_str(, world);
}当我们修改这个借来的变量会报错
$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow *some_string as mutable, as it is behind a reference-- src/main.rs:8:5|
7 | fn change(some_string: String) {| ------- help: consider changing this to be a mutable reference: mut String
8 | some_string.push_str(, world);| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ some_string is a reference, so the data it refers to cannot be borrowed as mutableFor more information about this error, try rustc --explain E0596.
error: could not compile ownership due to previous error可变引用
我们小小调整一下上面的错误代码就能够改正
fn main() {let mut s String::from(hello);change(mut s);
}fn change(some_string: mut String) {some_string.push_str(, world);
}首先我们必须将s 改为 mut。然后在调用change函数的地方创建一个可变引用 mut s并更新函数签名以接受一个可变引用 some_string: mut String。这就非常清楚地表明change 函数将改变它所借用的值。
可变引用有一个很大的限制如果你有一个对该变量的可变引用你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败
fn main() {let mut s String::from(hello);let r1 mut s;let r2 mut s;println!({}, {}, r1, r2);
}$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow s as mutable more than once at a time-- src/main.rs:5:14|
4 | let r1 mut s;| ------ first mutable borrow occurs here
5 | let r2 mut s;| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!({}, {}, r1, r2);| -- first borrow later used hereFor more information about this error, try rustc --explain E0499.
error: could not compile ownership due to previous error这个报错说这段代码是无效的因为我们不能在同一时间多次将 s 作为可变变量借用。第一个可变的借入在 r1 中并且必须持续到在 println 中使用它但是在那个可变引用的创建和它的使用之间我们又尝试在 r2 中创建另一个可变引用该引用借用与 r1 相同的数据。
这一限制以一种非常小心谨慎的方式允许可变性防止同一时间对同一数据存在多个可变引用。新 Rustacean 们经常难以适应这一点因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争data race类似于竞态条件它可由这三个行为造成
两个或更多指针同时访问同一数据。至少有一个指针被用来写入数据。没有同步数据访问的机制。
数据竞争会导致未定义行为难以在运行时追踪并且难以诊断和修复Rust 避免了这种情况的发生因为它甚至不会编译存在数据竞争的代码
一如既往可以使用大括号来创建一个新的作用域以允许拥有多个可变引用只是不能 同时 拥有
fn main() {let mut s String::from(hello);{let r1 mut s;} // r1 在这里离开了作用域所以我们完全可以创建一个新的引用let r2 mut s;
}Rust 在同时使用可变与不可变引用时也采用的类似的规则。这些代码会导致一个错误
fn main() {let mut s String::from(hello);let r1 s; // 没问题let r2 s; // 没问题let r3 mut s; // 大问题println!({}, {}, and {}, r1, r2, r3);
}我们 也 不能在拥有不可变引用的同时拥有可变引用。
不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了然而多个不可变引用是可以的因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如因为最后一次使用不可变引用println!)发生在声明可变引用之前所以如下代码是可以编译的
fn main() {let mut s String::from(hello);let r1 s; // 没问题let r2 s; // 没问题println!({} and {}, r1, r2);// 此位置之后 r1 和 r2 不再使用let r3 mut s; // 没问题println!({}, r3);
}悬垂引用Dangling References
在具有指针的语言中很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针dangling pointer所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下在 Rust 中编译器确保引用永远也不会变成悬垂状态当你拥有一些数据的引用编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用Rust 会通过一个编译时错误来避免
文件名src/main.rs
fn main() {let reference_to_nothing dangle();
}fn dangle() - String {let s String::from(hello);s
}$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier-- src/main.rs:5:16|
5 | fn dangle() - String {| ^ expected named lifetime parameter| help: this functions return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the static lifetime|
5 | fn dangle() - static String {| For more information about this error, try rustc --explain E0106.
error: could not compile ownership due to previous error这里的解决方法是直接返回 String
fn main() {let string no_dangle();
}fn no_dangle() - String {let s String::from(hello);s
}这样就没有任何错误了。所有权被移动出去所以没有值被释放。
小小的总结一下
在任意给定时间要么 只能有一个可变引用要么 只能有多个不可变引用。引用必须总是有效的。
Slice类型
slice 允许你引用集合中一段连续的元素序列而不用引用整个集合。slice 是一类引用所以它没有所有权。
这里有一个编程小习题编写一个函数该函数接收一个用空格分隔单词的字符串并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格则整个字符串就是一个单词所以应该返回整个字符串。
让我们推敲下如何不用 slice 编写这个函数的签名来理解 slice 能解决的问题
fn first_word(s: String) - ?first_word 函数有一个参数 String。因为我们不需要所有权所以这没有问题。不过应该返回什么呢我们并没有一个真正获取 部分 字符串的办法。不过我们可以返回单词结尾的索引结尾由一个空格表示。试试如示例中的代码。
文件名src/main.rs
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()
}⬆️ first_word 函数返回 String 参数的一个字节索引值
因为需要逐个元素的检查 String 中的值是否为空格需要用 as_bytes 方法将 String 转化为字节数组。 let bytes s.as_bytes();接下来使用 iter 方法在字节数组上创建一个迭代器 for (i, item) in bytes.iter().enumerate() {我们将在第十三章详细讨论迭代器。现在只需知道 iter 方法返回集合中的每一个元素而 enumerate 包装了 iter 的结果将这些元素作为元组的一部分来返回。enumerate 返回的元组中第一个元素是索引第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。
因为 enumerate 方法返回一个元组我们可以使用模式来解构我们将在第六章中进一步讨论有关模式的问题。所以在 for 循环中我们指定了一个模式其中元组中的 i 是索引而元组中的 item 是单个字节。因为我们从 .iter().enumerate() 中获取了集合元素的引用所以模式中使用了 。
在 for 循环中我们通过字节的字面值语法来寻找代表空格的字节。如果找到了一个空格返回它的位置。否则使用 s.len() 返回字符串的长度
if item b {return i;}}s.len()现在有了一个找到字符串中第一个单词结尾索引的方法不过这有一个问题。我们返回了一个独立的 usize不过它只在 String 的上下文中才是一个有意义的数字。换句话说因为它是一个与 String 相分离的值无法保证将来它仍然有效。考虑一下如下示例中使用了上一个示例中 first_word 函数的程序。
fn main() {let mut s String::from(hello world);let word first_word(s); // word 的值为 5s.clear(); // 这清空了字符串使其等于 // word 在此处的值仍然是 5// 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效
}这个程序编译时没有任何错误而且在调用 s.clear() 之后使用 word 也不会出错。因为 word 与 s 状态完全没有联系所以 word 仍然包含值 5。可以尝试用值 5 来提取变量 s 的第一个单词不过这是有 bug 的因为在我们将 5 保存到 word 之后 s 的内容已经改变。
我们不得不时刻担心 word 的索引与 s 中的数据不再同步这很啰嗦且易出错如果编写这么一个 second_word 函数的话管理索引这件事将更加容易出问题。它的签名看起来像这样
fn second_word(s: String) - (usize, usize) {现在我们要跟踪一个开始索引 和 一个结尾索引同时有了更多从数据的某个特定状态计算而来的值但都完全没有与这个状态相关联。现在有三个飘忽不定的不相关变量需要保持同步。
幸运的是Rust 为这个问题提供了一个解决方法字符串 slice。
字符串 slicestring slice是 String 中一部分值的引用它看起来像这样
fn main() {let s String::from(hello world);let hello s[0..5];let world s[6..11];
}对于 Rust 的 … range 语法如果想要从索引 0 开始可以不写两个点号之前的值。换句话说如下两个语句是相同的
#![allow(unused)]
fn main() {let s String::from(hello);let slice s[0..2];let slice s[..2];
}依此类推如果 slice 包含 String 的最后一个字节也可以舍弃尾部的数字。这意味着如下也是相同的
fn main() {let s String::from(hello);let len s.len();let slice s[3..len];let slice s[3..];
}也可以同时舍弃这两个值来获取整个字符串的 slice。所以如下亦是相同的
fn main() {let s String::from(hello);let len s.len();let slice s[0..len];let slice s[..];
}当我们有了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[..]
}