商标设计网站排行,石牌桥网站建设,佛山著名网站建设公司,公众号制作开发公司所有权#xff08;系统#xff09;是 Rust 最为与众不同的特性#xff0c;它让 Rust 无需垃圾回收即可保障内存安全#xff0c;下面是所有权以及相关功能#xff1a;借用#xff08;borrowing#xff09;、slice 以及 Rust 如何在内存中布局数据。 通过所有权系统管理内…所有权系统是 Rust 最为与众不同的特性它让 Rust 无需垃圾回收即可保障内存安全下面是所有权以及相关功能借用borrowing、slice 以及 Rust 如何在内存中布局数据。 通过所有权系统管理内存编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则程序都不能编译。在运行时所有权系统的任何功能都不会减慢程序。
所有权
所有权规则
Rust 中的每一个值都有一个 所有者变量。值在任一时刻有且只有一个所有者。当所有者变量离开作用域这个值将被丢弃。
fn main() {{ // s 在这里无效,因为还没有声名let s hello world!;// 使用变量s} // 作用域结束,s 不再有效}当 s 离开作用域的时候。当变量离开作用域Rust 为我们调用一个特殊的函数。这个函数叫做 drop 在这里 String 的 作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop
变量与数据交互的方式一移动 // 直接拷贝值let x 10;let y x;println!({} {}, x,y);// 更改引用,不拷贝值let s1 String::from(hello);let s2 s1;println!({s1} {s2});从string类型内存布局可以看出 当我们将 s1 赋值给 s2 String 的数据被复制了这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。 如果直接拷贝堆上的数据那么操作 s2 s1 在堆上数据比较大的时候会对运行时性能造成非常大的影响。 过当变量离开作用域后Rust 自动调用 drop 函数并清理变量的堆内存。不过上图展示了两个数据指针指向了同一位置。这就有了一个问题当 s2 和 s1 离开作用域它们都会尝试释放相同的内存。这是一个叫做 二次释放double free的错误也是之前提到过的内存安全性 bug 之一。两次释放相同内存会导致内存污染它可能会导致潜在的安全漏洞。
如果使用以下代码 let s1 String::from(hello);let s2 s1;println!({s1});会产生如下错误,因为 Rust 禁止你使用无效的引用。
error[E0382]: borrow of moved value: s1-- src\main.rs:56:15|
54 | let s1 String::from(hello);| -- move occurs because s1 has type String, which does not implement the Copy trait
55 | let s2 s1;| -- value moved here
56 | println!({s1});| ^^^^ value borrowed here after move| note: this error originates in the macro $crate::format_args_nl which comes from the expansion of the macro println (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable|
55 | let s2 s1.clone();|
如果你在其他语言中听说过术语 浅拷贝shallow copy和 深拷贝deep copy那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了这个操作被称为 移动move而不是叫做浅拷贝。上面的例子可以解读为 s1 被 移动 到了s2 中。 这样就解决了我们的问题因为只有 s2 是有效的当其离开作用域它就释放自己的内存。另外这里还隐含了一个设计选择Rust 永远也不会自动创建数据的 “深拷贝”。因此任何自动 的复制都可以被认为是对运行时性能影响较小的。
变量与数据交互的方式二克隆
如果我们确实需要深度复制 String 中堆上的数据而不仅仅是栈上的数据可以使用一个叫做 clone 的通用函数。 let s1 String::from(hello);let s2 s1.clone();println!({s1} {s2});这里堆上的数据确实被复制了。
Rust 有一个叫做 Copy trait 的特殊注解可以用在类似整型这样的存储在栈上的类型上,如果一个类型实现了 Copy trait那么一个旧的变量在将其赋值给其他变量后仍然可用。 以下是实现了 Copy trait的特殊类型,后续会谈到。 • 所有整数类型比如 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 移出作用域。没有特殊之处返回值与作用域
返回值也可以转移所有权。
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 被清理掉除非数据被移动为另一个变量所有。
引用与借用 使用符号 与使用 引用相反的操作是 解引用dereferencing它使用解引用运算符 * 引用像一个指针因为它是一个地址我们可以由此访问储存于该地址的属于其他变量的数据。 与指针不同引用确保指向某个特定类型的有效值。下面是如何定义并使用一个新的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()}s1 语法让我们创建一个指向值 s1 的引用但是并不拥有它。因为并不拥有这个值所以当引用停止使用时它所指向的值也不会被丢弃。 我们将创建一个引用的行为称为 借用borrowing。正如现实生活中如果一个人拥有某样东西你可以从他那里借来。当你使用完毕必须还回去。我们并不拥有它。正如变量默认是不可变的引用也一样。默认不允许修改引用的值。
虽然是不可变的但是编译器已经给出了解决办法那就是和变量一样使用mut微调一下上面代码
fn main() {let mut s1 String::from(hello);// 传一个引用进去change(mut s1);println!({s1});}
fn change(s: mut String) {s.push_str(hello);
}可变引用有一个很大的限制如果你有一个对该变量的可变引用你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败
fn main() {let mut s1 String::from(hello);let r1 mut s1;let r2 mut s1;println!({r1} {r2});
}错误如下
error[E0499]: cannot borrow s1 as mutable more than once at a time-- src\main.rs:63:14|
62 | let r1 mut s1;| ------- first mutable borrow occurs here
63 | let r2 mut s1;| ^^^^^^^ second mutable borrow occurs here
64 |
65 | println!({r1} {r2});| ---- first borrow later used here第一个可变的借入在 r1 中并且必须持续到在 println 中使用它但是在那个可变引用的创建和它的使用之间我们又尝试在 r2 中创建另一个可变引用该引用借用与 r1 相同的数据。主要是为了限制在同一时间对统一数据存在多个可变引用避免了数据竞争。Rust 避免了这种情况的发生因为它甚至不会编译存在数据竞争的代码 数据竞争由三个行为造成 • 两个或更多指针同时访问同一数据。 • 至少有一个指针被用来写入数据。 • 没有同步数据访问的机制。 修改一下使用之后再进行赋值可变引用 let mut s1 String::from(hello);let r1 mut s1;println!({r1});let r2 mut s1;println!({r2});它们的作用域没有重叠所以代码是可以编译的。编译器可以在作用域结束之前判断不再使用的引用。
悬垂引用
在具有指针的语言中很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针dangling pointer所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下在 Rust 中编译器确保引用永远也不会变成悬垂状态当你拥有一些数据的引用编译器确保数据不会在其引用之前离开作用域。
fn main() {let reference_to_nothing dangle();
}
fn dangle() - String { // dangle 返回一个字符串的引用let s String::from(hello); // s 是一个新字符串s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。因为 s 是在 dangle 函数内创建的当 dangle 的代码执行完毕后s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String Rust 不会允许我们这么做。
引用的规则
概括一下之前对引用的讨论 • 在任意给定时间要么 只能有一个可变引用要么 只能有多个不可变引用。 • 引用必须总是有效的。
slice
slice 允许你引用集合中一段连续的元素序列而不用引用整个集合。slice 是一类引用所以它没有所有权。类似于切片
fn main() {let s String::from(hello world);let hello s[0..5];let world s[6..11];}字符串slice
知道了slice之后可以理解字符串了这里 s 的类型是 str 它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串 字面值是不可变的str 是一个不可变引用。
let s Hello, world!;其它类型slice
就跟我们想要获取字符串的一部分那样我们也会想要引用数组的一部分。我们可以这样做
let a [1, 2, 3, 4, 5];
let slice a[1..3];
assert_eq!(slice, [2, 3]);这个 slice 的类型是 [i32] 。它跟字符串 slice 的工作方式一样通过存储第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。