[译]理解 Rust 中的所有权

本文译者为 360 奇舞团前端开发工程师

原文标题:Understanding ownership in Rust

原文作者:Ukpai Ugochi

原文链接:https://blog.logrocket.com/understanding-ownership-in-rust/


在 Stack Overflow 进行的开发人员调查中,Rust 连续第五年成为最受欢迎的编程语言。开发者喜爱 Rust 的原因有很多,其中之一就是它的内存安全保证。

Rust 通过称为所有权的特性来保证内存安全。所有权与其他语言中的垃圾收集器的工作方式不同,因为它只包含编译器需要在编译时检查的一组规则。如果不遵守所有权规则,编译器将不会编译。borrow checker确保你的代码遵循所有权规则。

对于没有垃圾收集器的语言,你需要显式分配和释放内存空间。当涉及大型代码库时,这很快就会变得乏味和具有挑战性。

值得庆幸的是, 内存管理由 Rust 编译器使用所有权模型处理。Rust编译器会自动插入一个 drop 语句来释放内存。它使用所有权模型来决定在哪里释放内存;当所有者超出范围时,内存将被释放。

fn main() {
    {
    let x = 5 ;
    // x 被丢弃在这里,因为它超出了范围
    }
}

什么是栈和堆?

栈和堆都是可供你的代码在运行时使用的内存存储段。对于大多数编程语言,开发人员通常不关心栈和堆上的内存分配情况。但是,由于 Rust 是一种系统编程语言,因此值的存储方式(在栈或堆中)对于语言的行为方式至关重要。

内存是如何存储在栈中的呢?假设桌子上有一叠书,这些书的排列方式是最后一本书放在书堆的顶部,第一本书在底部。理想情况下,我们不想从书堆下面滑出最下面的书,从上面挑一本书来阅读会更容易。

这正是内存在栈中的存储方式;它使用后进先出的方法。在这里,它按照获取值的顺序存储值,但以相反的顺序删除它们。同样重要的是要注意存储在栈中的所有数据在编译时都具有已知大小。

堆中的内存分配与栈中的内存分配方式不同。假设你要为朋友买一件衬衫,但是你不知道朋友穿的衬衫具体尺码,但经常看到他,你认为他可能是M码或L码。虽然你不完全确定,但你购买大号是因为即使他是中号,你的朋友仍然能穿上它。这是栈和堆之间的一个重要区别:我们不需要知道存储在堆中的值的确切大小。

与栈相比,堆中没有组织。将数据推入和推出栈很容易,因为一切都是有组织的并遵循特定的顺序。系统理解,当你将一个值压入栈时,它会停留在顶部,而当你需要从栈中取出一个值时,你正在检索存储的最后一个值。

然而,堆中的情况并非如此。在堆上分配内存需要动态地搜索一块足够大的内存空间来满足分配要求,并且返回指向该内存位置的地址。检索值时,需要使用指针来找到存储该值的内存位置。

在堆上分配看起来像书籍索引,其中存储在堆中的值的指针存储在栈中。但是,分配器还需要搜索一个足够大的空白空间来包含该值。

函数的局部变量存储在函数栈中,而数据类型(如String、Vector、Box等)存储在堆中。了解 Rust 的内存管理以确保应用程序按预期运行非常重要。

所有权规则

所有权有三个基本规则来预测内存如何存储在栈和堆中:

1.每个 Rust 值都有一个称为其“所有者”的变量:

let x = 5 ; // x is the owner of the value "5"

2.每个值一次只能有一个所有者

3.当所有者超出范围时,该值将被删除:

fn main () { 
    { // // scope begins
        let s = String :: from ( "hello" ); // s comes into scope
    }  
    // the value of s is dropped at this point, it is out of scope
}

所有权如何运作

在我们的介绍中,我们建立了一个事实,即所有权不像垃圾收集器系统。大多数编程语言要么使用垃圾收集器,要么要求开发人员自己分配和释放内存。

在所有权中,我们为自己请求内存,当所有者超出范围时,该值将被删除并释放内存。这正是所有权规则第三条所解释的。为了更好地理解这是如何工作的,让我们看一个例子:

fn main () {
    { 
        // a is not valid here
        let a = 5 ; // a is valid here
        // do stuff with a
    } 
    println!("{}", a) // a is no longer valid at this point, it is out of scope
}

这个例子非常简单;这就是栈中内存分配的工作原理。a由于我们知道它的值将占用的确切空间,因此在栈上分配了一块内存 ( ) 5。然而,情况并非总是如此。有时,你需要为一个在编译时不知道其大小的可增长值分配内存空间。

对于这种情况,内存是在堆上分配的,你首先必须请求内存,如下例所示:

fn main () { 
    { 
        let mut s = String :: from ( "hello" ); // s is valid from this point forward
        push_str ( ", world!" ); // push_str() appends a literal to a String
    	println !( "{}" , s ); // This will print `hello, world!`
    } 
    // s is no longer valid here
}

我们可以根据需要附加任意多的字符串,s因为它是可变的,因此很难知道编译时所需的确切大小。因此在我们的程序中我们需要一个字符串大小的内存空间:

克隆和复制

在本节中,我们将研究所有权如何影响 Rust 中的某些功能,从clone和copy功能开始。

对于具有已知大小的值(如 )integers,将值复制到另一个值会更容易。例如:_

fn main() {
    let a = "5" ; 
    let b = a ; // copy the value a into b
    println !( "{}" , a ) // 5 
    println !( "{}" , b ) // 5 
}

因为a存储在栈中,所以更容易复制它的值来为 制作另一个副本b。对于存储在堆中的值,情况并非如此:

fn main () { 
    let a = String :: from ( "hello" ); 
    let b = a ; // copy the value a into b
    println !( "{}" , a ) // This will throw an error because a has been moved or ownership has been transferred
    println !( "{}" , b ) // hello 
}

当你运行该命令时,你将收到一个错误 error[E0382]: borrow of moved value: "a"move

所有权和函数

将值传递给函数遵循相同的所有权规则,这意味着它们一次只能有一个所有者,并且一旦超出范围就释放内存。让我们看看这个例子:

fn main() {
    let s1 = givesOwnership(); // givesOwnership moves its return

    let s2 = String::from("hello"); // s2 comes into scope
    let s3 = takesAndGivesBack(s2); // s2 is moved into takesAndGivesBack, 
                                    // which also moves its return value into s3

} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  // moved, so nothing happens. s1 goes out of scope and is dropped.


fn givesOwnership() -> String { // givesOwnership will move its
                                // return value into the function
                                // that calls it

    let someString = String::from("hello");  // someString comes into scope

    someString                               // someString is returned and
                                             // moves out to the calling
                                             // function
}

// takesAndGivesBack will take a String and return one
fn takesAndGivesBack(aString: String) -> String { // aString comes into
                                                      // scope

    aString  // aString is returned and moves out to the calling function
}

切片

引用序列中彼此相邻的元素,而不是引用整个集合。因此,可以使用切片类型。但是,此功能不具有引用和借用之类的所有权。

让我们看看下面的例子。在此示例中,我们将使用切片类型来引用连续序列中值的元素:

fn main() {
    let s = String::from("Nigerian");
    // &str type
    let a = &s[0..4];// doesn't transfer ownership, but references/borrow the first four letters.
    let b = &s[4..8]; // doesn't transfer ownership, but references/borrow the last four letters.
    println!("{}", a); // prints Nige

    println!("{}", b); // prints rian
    
    let v=vec![1,2,3,4,5,6,7,8];

    // &[T] type
    let a = &v[0..4]; // doesn't transfer ownership, but references/borrow the first four element.
    let b = &v[4..8]; // doesn't transfer ownership, but references/borrow the last four element.
    println!("{:?}", a); // prints [1, 2, 3, 4]
    println!("{:?}", b); // prints [5, 6, 7, 8]
    
}

结论

所有权是 Rust 的一个重要特性。掌握所有权的概念,有利于编写可扩展的代码。很多人喜欢 Rust 的原因就是因为这个特性,一旦你掌握了它,你就可以更高效的编写代码。

在本文中,我们介绍了所有权的基础知识及其规则,以及如何应用它们。除此之外,还介绍了 Rust 的一些不涉及到所有权的特性以及如何巧妙地使用它们。对于 Rust 的所有权特性感兴趣的读者朋友,可以查看相关文档。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

b0cb357d9882edd6e214b1e462a9b0f3.png

猜你喜欢

转载自blog.csdn.net/qiwoo_weekly/article/details/130376953