Rust的所有权机制是一个独特的功能,它使其与其他主流编程语言不同。这就是使生锈的记忆安全和高效的原因,同时避免需要垃圾收集器,这是Java,Go和Python等语言中的共同功能。了解所有权对于精通生锈至关重要。在本文中,我们将探讨以下主题:
- 堆栈和堆
- 可变范围
- 所有权
- 借用(引用)
- 可变参考
注意:本文对初学者友好。熟悉Rust的基本语法,函数中的可变声明以及基本数据类型。如果您需要快速刷新,可以检查此guide。
堆栈和堆
生锈中的内存可以分为两个主要部分:堆栈和堆。这两者都可以在编译时间和运行时可用,并且可以实现不同的目的。了解他们的力学对于编写有效的生锈代码至关重要。
堆
堆栈用于在编译时存储具有已知尺寸的数据。存储在堆栈上的数据大小是固定的,并且在运行时不能更改。
堆栈使用第一个,最后一个(FILO)机制进行操作,类似于堆叠板。添加的第一个板是删除的最后一个。
Rust自动将原始类型添加到堆栈中:
fn main() {
let num: i32 = 14;
let array = [1, 2, 4];
let character = 'a';
let tp = (1, true);
println!("number = {}, array = {:?}, character = {}, tp = {:?}", num, array, character, tp);
}
main
方法的堆栈布局如下:
地址 | 名称 | value |
---|---|---|
3 | TP | (1,true) |
2 | 字符 | 'a' |
1 | 数组 | [1,2,4] |
0 | num | 14 |
在此表中,第一个添加的num
变量将是函数范围结束时最后删除的。
注意:地址列使用数字作为表示,理想情况下,计算机中的地址不仅仅是数字。
堆
堆用于编译时大小未知的数据。 Vec
,String
和Box
之类的类型自动存储在堆上。
fn main() {
let num: i32 = 14;
let new_str = String::new();
}
堆上数据的内存映射如下:
stack mem
地址 | 名称 | value |
---|---|---|
1 | new_str | ptr1 |
0 | num | 14 |
heap mem
地址 | 名称 | value |
---|---|---|
ptr1 | “” |
将数据存储在堆上时,指针(例如ptr1
)首先存储在堆栈中。然后,该指针引用堆上的实际数据。与堆栈访问相比,此额外的步骤使堆访问更慢。
有了堆栈和堆的机制,让我们深入研究可变范围。
可变范围
变量范围定义了变量有效的代码中的范围。在生锈中,卷曲括号会产生块,并且这些块中的变量被瞄准了。
fn main() {
// everything here is scoped to this block
let hello = "Hello World";
// ...
}
在该块之外无法访问一个块中的变量:
fn main() {
let hello = "Hello World";
// inner block
{
let num = 20;
}
// This will fail to compile
println!("{} {}", hello, num);
}
从上方您不能在内部块外使用num
,因为它仅范围内范围为内部块。
Rust运行功能从上到下,根据需要分配堆栈和堆内存。当范围结束时,Rust在内部调用drop
方法来处理内存,这是理解Rust的内存管理方法的关键概念。
所有权
在这里,我们来到了文章的核心,我们在这里探索所有权及其对Rust编程的影响。在我们继续时记住这些所有权规则:
所有权规则
- 一次只能有一个价值的所有者。
- 每个值都有一个所有者。
- 如果所有者不在范围内,则值将删除。
让我们看看这是什么意思:
fn main() {
let array = vec![1,2,3];
let title = String::new();
let new_array = array;
let new_title = title;
println!("array {:?} title {} new_array {:?} new_title {}", array, title, new_array, new_title);
}
由于array
和title
均同时拥有多个变量,因此出现了“移动价值借用”的编译误差。这违反了所有权规则。
上面代码的Rust的内存布局如下:
stack mem
地址 | 名称 | value |
---|---|---|
3 | new_title | ptr2copy |
2 | new_array | ptr1copy |
1 | 标题 | ptr2 |
0 | 数组 | ptr1 |
heap mem
地址 | 名称 | value |
---|---|---|
ptr2和ptr2copy | “” | |
ptr1和ptr1copy | vec![1,2,3] |
从上面看到的,因为向量和字符串是非固定尺寸类型的,将它们分配到堆中,并且可以通过堆栈的指针访问。
ptr1Copy
指针是ptr1
Pointer的副本,ptr2Copy
指针是ptr2
指针的副本。
生锈了,然后称为drop
方法,drop
方法在堆栈中行走记住 filo 我们上面解释了吗?首先,将添加到new_title
的堆栈中的最后一项开始,然后发现它具有指针(ptr2Copy
)通过该指针进入堆内存并删除字符串值。它继续到堆栈new_array
上的下一个元素,该元素还具有指针ptr1Copy
,并从堆内存中删除了向量。它再次继续到下一个是title
,并具有指针ptr2
,但是当它遵循指针时,它意识到数据不再退出,因为它已经通过ptr2Copy
删除了,这为什么title
为title
丢弃了同一件事,array
variabe8 variabe a variable of the title
。
所有权规则仅适用于堆上的生锈类型。堆栈上的原始类型不遵循以下规则:
fn main() {
let array = [1,2,3];
let title = "hello";
let new_array = array;
let new_title = title;
println!("array {:?} title {} new_array {:?} new_title {}", array, title, new_array, new_title);
}
对于熟悉其他语言的读者,将值重新分配到新变量时似乎是直观的。我们如何在Rust中实现这一目标?
我们可以使用.clone
方法,该方法复制了堆值并将其存储在堆上的新位置:
请参阅下文:
fn main() {
let array = vec![1,2,3];
let title = String::new();
let new_array = array.clone();
let new_title = title.clone();
println!("array {:?} num {} new_array {:?} new_num {}", array, title, new_array, new_title);
}
从上面的代码起作用,让我们看看如何在堆栈上完成翻译和堆
stack mem
地址 | 名称 | value |
---|---|---|
3 | new_title | ptr2copy |
2 | new_array | ptr1copy |
1 | 标题 | ptr2 |
0 | 数组 | ptr1 |
heap mem
地址 | 名称 | value |
---|---|---|
ptr2copy | “” | |
ptr1copy | vec![1,2,3] | |
ptr2 | “” | |
ptr1 | vec![1,2,3] |
尽管这种方法有效,但可能会导致不必要的内存消耗和效率低下。
借用
在生锈中,借用(或引用)使我们可以使用价值而无需所有权。要引用一个值,我们使用&
符号。
fn main() {
let hello = String::from("hello");
let another_hello = &hello;
let arr = vec![1];
let another_arr = &arr;
println!("hello = {} another_hello = {} {:?} {:?}", hello, another_hello, another_arr, arr)
}
通过引用值,我们可以读取数据而无需复制数据,避免不必要的内存开销。
这也适用于函数参数。功能参数具有自己的范围,将其传递给它将自动将所有权移至参数。
fn main() {
let name = String::from("John");
say_hello(name);
println!("{}", name)
}
fn say_hello(name: String) {
println!("hello {}", name)
}
我们可以通过从名称所有者那里借入来解决此错误,请参见下文
fn main() {
let name = String::from("John");
say_hello(&name);
println!("{}", name)
}
fn say_hello(name: &String) {
println!("hello {}", name)
}
我们没有错误,到目前为止看起来不错。
所有这些,而我们仅阅读所有者的价值,如果我们想更新该价值而仍然没有所有权,该怎么办?
可变的参考
rust中的突变值需要mut
关键字,并且要通过参考突变值,我们使用&mut
关键字。
fn main() {
let mut name = String::from("John");
full_name(&mut name);
println!("Your full name is: {}", name);
}
fn full_name(name: &mut String) {
name.push_str(" Doe");
}
从full_name
函数上方可以更新名称价值,而无需拥有该值。
总是要记住的一件事是,在任何给定时间,您都可以有一个可变的参考或任何数量不变的参考文献。
因此以下代码将无法编译:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
即使我们只读了一个值得untable的参考锈蚀,即以下仍然无法编译
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{}, {}, {}", r1, r2, r3);
}
原因是要避免数据不一致,因为生锈无法保证在什么时候发生读数或数据更改会发生。
我们可以通过范围使上述工作。
fn main() {
let mut s = String::from("hello");
// inner scope
{
let r1 = &mut s;
println!("{}", r1);
}
let r2 = &mut s;
println!("{}", r2);
}
我们还可以阅读所有阅读参考文献并在突变之前使用它们。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
}
Rust强制我们正确考虑我们的代码,以避免编写难以修复的错误代码。
结论
在编程语言的世界中,由于其所有权机制,Rust是一个非凡的选择。这种机制虽然最初具有挑战性,但它是Rust记忆安全和效率的基石。通过严格遵守执行单个所有权的规则,Rust减轻了内存泄漏的风险,数据不一致和其他常见的编程陷阱。
通过本文,我们探讨了Rust所有权概念的复杂工作。我们从对堆栈和堆的基本理解开始,这是支持记忆分配的支柱。堆栈,用于固定尺寸数据的高效以及堆,可容纳动态数据,使生锈能够平衡性能和灵活性。
随着我们深入研究块级范围的重要性,可变范围的重要性变得显而易见。 Rust的严格范围管理不仅简化了代码,而且可以通过降落机制来自动化内存DEADLOCATION。
所有权是Rust的内存管理理念的核心,它教会了我们一个宝贵的教训,只有一个实体才能在任何给定时间拥有价值。该原则最初似乎是限制的,但这是针对数据竞赛和记忆问题的必不可少的保护。
我们通过利用借款来学习如何浏览这些所有权规则,该机制允许对价值进行控制而不会损害所有权。我们看到了参考文献(&
)和可变参考(&mut
)如何促进此过程,从而在不牺牲数据完整性的情况下可以阅读和操纵。
在经常为方便的情况下牺牲记忆安全的景观中,生锈是一种迫使开发人员采用最佳实践的语言。虽然所有权模型可能具有挑战性,但掌握它将使您成为更有效和有效的Rust程序员。
当您继续使用Rust旅行时,请记住,所有权不仅是技术性;这是一种心态。拥抱它,让它指导您的编码决策,您将解锁此语言的真正潜力。 Rust的所有权不仅是要克服的障碍。这是负责任地挥舞的超级大国。
因此,当您编写生锈代码时,请记住这些所有权原则,您将在创建经受时间考验的强大,高效和可靠的软件方面做得很好。