荆门网站建设514885,秦皇岛十大必去景点,网站建设相关工作总结,做哪些网站比较赚钱第四章认识所有权
所有权#xff08;系统#xff09;是 Rust 最为与众不同的特性#xff0c;它让 Rust 无需垃圾回收#xff08;garbage collector#xff09;即可保障内存安全。因此#xff0c;理解 Rust 中所有权如何工作是十分重要的。
4.1 所有权
所有运行的程序都… 第四章认识所有权
所有权系统是 Rust 最为与众不同的特性它让 Rust 无需垃圾回收garbage collector即可保障内存安全。因此理解 Rust 中所有权如何工作是十分重要的。
4.1 所有权
所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制在程序运行时不断地寻找不再使用的内存在另一些语言中程序员必须亲自分配和释放内存。Rust 则选择了第三种方式通过所有权系统管理内存编译器在编译时会根据一系列的规则进行检查。在运行时所有权系统的任何功能都不会减慢程序。
所有权规则 Rust 中的每一个值都有一个被称为其 所有者owner的变量。值有且只有一个所有者。当所有者变量离开作用域这个值将被丢弃。 字符串字面值不可变这里以String为例
fn main() {println!(Hello, world!);// String在堆上// from 函数基于字符串字面值来创建 Stringlet mut str String::from(hello);str.push_str(, world); // push_str() 在字符串后追加字面值println!({}, str);
}结果 对于 String 类型为了支持一个可变可增长的文本片段需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着
必须在运行时向操作系统请求内存。需要一个当我们处理完 String 时将内存返回给操作系统的方法。
第一部分由我们完成当调用 String::from 时它的实现 (implementation) 请求其所需的内存。这在编程语言中是非常通用的。 然而第二部分实现起来就各有区别了。在有 垃圾回收garbage collectorGC的语言中 GC 记录并清除不再使用的内存而我们并不需要关心它。没有 GC 的话识别出不再使用的内存并调用代码显式释放就是我们的责任了跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了将会出现无效变量。如果重复回收这也是个 bug。我们需要精确的为一个 allocate 配对一个 free。 Rust 采取了一个不同的策略内存在拥有它的变量离开作用域后就被自动释放。
fn main() {{let s String::from(hello); // 从此处起s 是有效的// 使用 s} // 此作用域已结束// s 不再有效
}注意在 C 中这种 item 在生命周期结束时释放资源的模式有时被称作 资源获取即初始化Resource Acquisition Is Initialization (RAII)。如果你使用过 RAII 模式的话应该对 Rust 的 drop 函数并不陌生。 变量与数据交互的方式一移动
fn main() {let x 5;let y x;
}基本变量赋值x和y都在栈中且值为5。
那么String类型呢
fn main() {let s1 String::from(hello);let s2 s1;
}String 由三部分组成如图左侧所示一个指向存放字符串内容内存的指针一个长度和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。 长度表示 String 的内容当前使用了多少字节的内存。容量是 String 从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的不过在当前上下文中并不重要所以现在可以忽略容量。
当将 s1 赋值给 s2String 的数据被复制了这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。 像C中的浅拷贝 如果 Rust 也拷贝了堆上的数据那么内存看起来就是这样的。如果 Rust 这么做了那么操作 s2 s1 在堆上数据比较大的时候会对运行时性能造成非常大的影响。 浅拷贝这就有了一个问题当 s2 和 s1 离开作用域他们都会尝试释放相同的内存。这是一个叫做 二次释放double free的错误也是之前提到过的内存安全性 bug 之一。两次释放相同内存会导致内存污染它可能会导致潜在的安全漏洞。
为了确保内存安全这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存Rust 则认为 s1 不再有效因此 Rust 不需要在 s1 离开作用域后清理任何东西。看看在 s2 被创建之后尝试使用 s1 会发生什么这段代码不能运行
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 的通用函数。
fn main() {let s1 String::from(hello);let s2 s1.clone();println!({}, world!, s1);println!({}, world!, s2);
}这段代码的实际结果就是如下图 只在栈上的数据拷贝
但这段代码似乎与我们刚刚学到的内容相矛盾没有调用 clone不过 x 依然有效且没有被移动到 y 中。
fn main() {let x 5;let y x;println!(x {}, y {}, x, y);
}原因是像整型这样的在编译时已知大小的类型被整个存储在栈上所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。换句话说这里没有深浅拷贝的区别所以这里调用 clone 并不会与通常的浅拷贝有什么不同可以不用管它。
所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制就像赋值语句一样。
简单的案例见注释
fn main() {let s String::from(hello); // s 进入作用域takes_ownership(s); // s 的值移动到函数里 ...// ... 所以到这里不再有效// println!({}, 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 移出作用域。不会有特殊操作复杂一点的
fn main() {let s String::from(hello); // s 进入作用域takes_ownership(s); // s 的值移动到函数里 ...// ... 所以到这里不再有效// println!({}, s); // 报错
}
fn takes_ownership(some_string: String) { // some_string 进入作用域let str String::from(some_string);println!({}, some_string); // 报错
}返回值与作用域
返回值也可以转移所有权
见注释
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(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 并移出给调用的函数
}变量的所有权总是遵循相同的模式将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时其值将通过 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)
}好 返回参数的所有权
见引用部分
4.4 引用与借用
下面是如何定义并使用一个calculate_length 函数它以一个对象的引用作为参数而不是获取值的所有权
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()
}符号就是 引用它允许你使用值但不获取其所有权。 注意与使用 引用相反的操作是 解引用dereferencing它使用解引用运算符*。 变量 s 有效的作用域与函数参数的作用域一样不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。
将获取引用作为函数参数称为 借用borrowing。
正如变量默认是不可变的引用也一样默认不允许修改引用的值。
fn main() {let s String::from(hello);change(s);
}fn change(some_string: String) {some_string.push_str(, world);
}
结果 可变引用
fn main() {// s也必须是mut的let mut s String::from(hello);change(mut s);
}// 注意形参的形式
fn change(some_string: mut String) {some_string.push_str(, world);
}不过可变引用有一个很大的限制在特定作用域中的特定数据有且只有一个可变引用。
fn main() {let mut s String::from(hello);let r1 mut s;let r2 mut s; // 错误println!({}, {}, r1, r2);}
结果 这个限制允许可变性不过是以一种受限制的方式允许。 也就是说r1、r2只能出现一个 这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争data race类似于竞态条件它可由这三个行为造成
两个或更多指针同时访问同一数据。至少有一个指针被用来写入数据。没有同步数据访问的机制。
数据竞争会导致未定义行为难以在运行时追踪并且难以诊断和修复Rust 避免了这种情况的发生因为它甚至不会编译存在数据竞争的代码
一如既往可以使用大括号来创建一个新的作用域以允许拥有多个可变引用。
fn main() {let mut s String::from(hello);// 这个可变引用在前面才可以{let r2 mut s;}let r1 mut s;println!({}, r1);
}类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误
fn main() {let mut s String::from(hello);let r1 s; // 没问题let r2 s; // 没问题let r3 mut s; // 大问题println!({}, {}, and {}, r1, r2, r3);}结果 不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了然而多个不可变引用是可以的因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如因为最后一次使用不可变引用在声明可变引用之前所以如下代码是可以编译的
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 中编译器确保引用永远也不会变成悬垂状态当你拥有一些数据的引用编译器确保数据不会在其引用之前离开作用域。
fn main() {let reference_to_nothing dangle();
}fn dangle() - String {let s String::from(hello);s
}返回局部变量的引用 错误信息引用了一个还未介绍的功能生命周期lifetimes。第十章会详细介绍生命周期。不过如果你不理会生命周期部分错误信息中确实包含了为什么这段代码有问题的关键信息
fn dangle() - String { // dangle 返回一个字符串的引用let s String::from(hello); // s 是一个新字符串s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。// 危险因为 s 是在 dangle 函数内创建的当 dangle 的代码执行完毕后s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String这可不对Rust 不会允许我们这么做。
这里的解决方法是直接返回 String
fn main() {}
fn no_dangle() - String {let s String::from(hello);s
}引用的规则
在任意给定时间要么只能有一个可变引用要么只能有多个不可变引用。引用必须总是有效的。
4.3 Slice
另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列而不用引用整个集合。
这里有一个编程小习题编写一个函数该函数接收一个字符串并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格则整个字符串就是一个单词所以应该返回整个字符串。
fn first_word(s : String) - ?
first_word 函数有一个参数 String。因为不需要所有权所以这没有问题。不过应该返回什么呢并没有一个真正获取 部分 字符串的办法。不过可以返回单词结尾的索引。
fn main() {let str String::from(hello world);let count first_word(str);println!({}, count);}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()
}
1first_word 函数返回 String 参数的一个字节索引值
2因为需要逐个元素的检查 String 中的值是否为空格需要用 as_bytes 方法将 String 转化为字节数组
let bytes s.as_bytes();
3接下来使用 iter 方法在字节数组上创建一个迭代器
for (i, item) in bytes.iter().enumerate() {只需知道 iter 方法返回集合中的每一个元素而 enumerate 包装了 iter 的结果将这些元素作为元组的一部分来返回enumerate 返回的元组中第一个元素是索引第二个元素是集合中元素的引用。
因为 enumerate 方法返回一个元组可以使用模式来解构就像 Rust 中其他任何地方所做的一样。所以在 for 循环中指定了一个模式其中元组中的 i 是索引而元组中的 item 是单个字节。因为从 .iter().enumerate() 中获取了集合元素的引用所以模式中使用了 。
在 for 循环中通过字节的字面值语法来寻找代表空格的字节。如果找到了一个空格返回它的位置。否则使用 s.len() 返回字符串的长度 if item b {return i;}
}s.len()现在有了一个找到字符串中第一个单词结尾索引的方法不过这有一个问题。返回了一个独立的 usize不过它只在 String 的上下文中才是一个有意义的数字。换句话说因为它是一个与 String 相分离的值无法保证将来它仍然有效。
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()
}fn main() {let mut s String::from(hello world);let word first_word(s); // word 的值为 5s.clear(); // 这清空了字符串使其等于 // word 在此处的值仍然是 5// 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效println!({}, word);
}这个程序编译时没有任何错误而且在调用 s.clear() 之后使用 word 也不会出错。因为 word 与 s 状态完全没有联系所以 word仍然包含值 5。可以尝试用值 5 来提取变量 s 的第一个单词不过这是有 bug 的因为在我们将 5 保存到 word 之后 s 的内容已经改变。
还可以这样声明函数
fn second_word(s: String) - (usize, usize) {这样也容易受String的影响。引出字符串 slice。
字符串slice
字符串 slicestring slice是 String 中一部分值的引用它看起来像这样
fn main() {let s String::from(hello world);let hello s[0..5];let world s[6..11];
}它不是对整个 String 的引用而是对部分 String 的引用。可以使用一个由中括号中的 [starting_index..ending_index] 指定的 range 创建一个 slice其中 starting_index 是 slice 的第一个位置ending_index 则是 slice 最后一个位置的后一个值。在其内部slice 的数据结构存储了 slice 的开始位置和长度长度对应于 ending_index 减去 starting_index 的值。所以对于 let world s[6..11]; 的情况world 将是一个包含指向 s 第 7 个字节从 1 开始的指针和长度值 5 的 slice。 对于 Rust 的 .. range 语法如果想要从第一个索引0开始可以不写两个点号之前的值。换句话说如下两个语句是相同的
fn main() {let s String::from(hello);let slice1: str s[0..2];let slice2 s[..2];println!({}, {}, {}, s, slice1, slice2);
}依此类推如果 slice 包含 String 的最后一个字节也可以舍弃尾部的数字。这意味着如下也是相同的
fn main() {let s String::from(hello);let len s.len();let slice1 s[3..len];let slice2 s[3..];println!({}, {}, {}, s, slice1, slice2);
}也可以同时舍弃这两个值来获取整个字符串的 slice。所以如下亦是相同的
fn main() {let s String::from(hello);let len s.len();let slice1 s[0..len];let slice2 s[..];println!({}, {}, {}, s, slice1, slice2);
}越界编译时检查不出来。
重新实现本小节开始的案例
fn main() {let s String::from(hello world);println!({}, first_word(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再输出
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 main() {let mut s String::from(hello world);let word first_word(s); // word 的值为 5s.clear(); // 这清空了字符串使其等于 // word 在此处的值仍然是 5// 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效println!({}, word);
}结果 回忆一下借用规则当拥有某值的不可变引用时就不能再获取一个可变引用。因为 clear 需要清空 String它尝试获取一个可变引用。Rust不允许这样做因而编译失败。
字符串字面值就是 slice
它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的str 是一个不可变引用。
字符串 slice 作为参数
在知道了能够获取字面值和 String 的 slice 后我们对 first_word 做了改进这是它的签名
fn first_word(s: String) - str {而更有经验的 Rustacean 会编写出示例 4-9 中的签名因为它使得可以对 String 值和 str 值使用相同的函数
fn first_word(s: str) - str {如果有一个字符串 slice可以直接传递它。如果有一个 String则可以传递整个 String 的 slice。定义一个获取字符串 slice 而不是 String 引用的函数使得我们的 API 更加通用并且不会丢失任何功能
fn first_word(s: str) - str {let bytes s.as_bytes();for (i, item) in bytes.iter().enumerate() {if item b {return s[0..i];}}s[..]
}
fn main() {let my_string String::from(hello world);// first_word 中传入 String 的 slicelet word first_word(my_string[..]);let my_string_literal hello world;// first_word 中传入字符串字面值的 slicelet word first_word(my_string_literal[..]);// 因为字符串字面值 **就是** 字符串 slice// 这样写也可以即不使用 slice 语法let word first_word(my_string_literal);
}其他类型的 slice
字符串 slice正如你想象的那样是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组
fn main() {let a [1, 2, 3, 4, 5];
}就跟我们想要获取字符串的一部分那样我们也会想要引用数组的一部分。我们可以这样做
fn main() {let a [1, 2, 3, 4, 5];let slice a[1..3];for i in slice {println!({} , i);}}总结
所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
参考认识所有权 - Rust 程序设计语言 简体中文版 (bootcss.com)