Skip to content

ownership

所有权

是rust的特色,由于所有权是一个新概念,因此需要花费一些时间来掌握它。

所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:

  • 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go

  • 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++

  • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查

    这里需要了解下数据结构中的栈和堆

    rust程序设计中文版本解释如下:
    
    栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出(last in, first out)。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 进栈(pushing onto the stack),而移出数据叫做 出栈(popping off the stack)。
    
    栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。
    想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。
    
    入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
    
    访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
    
    当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
    
    跟踪哪部分代码正在使用堆上的哪些数据,最大限度地减少堆上的重复数据量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
    

rust的所有权规则

  • Rust 中的每一个值都有一个被称为其 所有者owner)的变量。

  • 值在任一时刻有且只有一个所有者。

  • 当所有者(变量)离开作用域,这个值将被丢弃。

    这些规则的作用好处就有了以下几点:

    1. 防止双重释放:由于每个值只有一个所有者,当该值离开作用域时,它只会被删除一次。这避免了双重释放内存的风险。
    2. 有效的内存管理:不需要运行时垃圾收集器。当数据不再使用时,Rust确保它立即被清理。
    3. 数据竞争安全:由于同一时间只能有一个可变引用或任意数量的不可变引用,Rust在编译时防止了数据竞争。
    4. 清晰的代码意图:所有权和借用规则迫使开发者明确地考虑如何使用数据,这导致了更明确和更容易理解的代码意图。
    5. 无需手动内存管理:尽管Rust提供了手动内存管理的工具,但大多数情况下,你不需要担心分配和释放内存,Rust为你处理了这些。
    6. 并发友好:所有权规则使得并发编程更为安全,因为它在编译时检查潜在的数据竞争和竞态条件。

移动

在Rust中,当你将一个变量赋值给另一个变量时,原始数据实际上是被移动的,不是复制的(除非这个类型实现了Copy trait)。

let s1 = String::from("hello");
let s2 = s1;

// 此时s1已经不再有效,因为String的数据被移动到了s2
// println!("{}", s1); // 这会导致编译错误

绑定

在Rust中,"绑定"是一个核心概念,涉及变量与其关联的数据。当你将一个值赋给一个变量时,这个变量实际上是绑定到那个值的。这意味着变量与其值之间建立了一个关联。

绑定的种类:

  1. 模式绑定: 当你在模式匹配、结构体解构或迭代中使用变量时,你实际上是在创建绑定。例如,在模式匹配中:

    match value {
        Some(x) => println!("{}", x),  // 这里x绑定到Some内部的值
        None => (),
    }

    在上述代码中,x绑定到Some中的值。

  2. 函数参数绑定: 当你传递一个值到函数时,函数的参数会绑定到那个值。

    fn print_value(x: i32) {
        println!("{}", x);  // 这里x绑定到传递给函数的值
    }
  3. let绑定: 这是最常见的绑定形式。当你使用let关键字声明一个变量时,你创建了一个新的绑定。

    let x = 5;  // x绑定到值5

可变与不可变绑定:

  • 不可变绑定: 默认情况下,绑定是不可变的,这意味着你不能更改其绑定的值。

    let x = 5;
    // x = 6;  // 这会导致编译错误,因为x是不可变的
  • 可变绑定: 使用mut关键字,你可以创建一个可变绑定,这意味着你可以更改其绑定的值。

    let mut y = 5;
    y = 6;  // 这是允许的,因为y是可变的

克隆

如果你确实需要复制某些数据,而不是移动,可以使用.clone()方法(如果该类型支持克隆)。

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2); // 这是有效的

函数和所有权

当你将一个值传递给函数时,与赋值给变量的情况类似,该值也会被移动或复制。

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

let s = String::from("hello");
takes_ownership(s);
// s在这里不再有效,因为它的所有权已被移动到函数中

引用和借用

为了允许访问数据但不取得其所有权,Rust引入了引用和借用的概念。

fn calculate_length(s: &String) -> usize {
    s.len()
}

let s = String::from("hello");
let len = calculate_length(&s);
// s在这里仍然有效,因为我们只是传递了一个引用

需要注意的是,Rust默认不允许两个或多个可变引用存在于同一作用域,也不允许同时存在可变引用和不可变引用,以确保数据的安全性。

在借用之中当一个变量有引用存在时,不能移交它所绑定的数据的所有权

解释:它确保了内存安全和数据一致性。

首先,让我们明确什么是"有引用存在"。当我们说一个变量被借用,实际上是说这个变量的数据被其他变量引用了。

现在,考虑以下示例:

let mut s = String::from("hello");
let r = &s;  // s现在被r引用或借用

在上面的代码中,我们创建了一个String类型的变量s,然后我们创建了一个引用r,它引用(或借用)s的数据。

此时,根据Rust的规则,你不能改变s的值或移交它的所有权,因为它被r引用了。这样做是为了避免数据竞争、悬挂引用和其他可能的错误。

例如,以下操作都是不允许的:

let mut s = String::from("hello");
let r = &s;

// 试图修改s的值
s.push_str(", world"); // 这会导致编译错误,因为s正在被r引用

// 试图移交s的所有权
let s2 = s; // 这也会导致编译错误,因为s的所有权不能在被r引用时被移交

简而言之,当一个变量被其他变量引用(或借用)时,你不能更改它的值或移交它的所有权。这是为了确保所有引用始终指向有效和一致的数据,从而避免潜在的内存安全问题。

在这里面还要区分不可变引用可变引用

  1. 对于不可变引用 (&T):

    • 不能通过任何引用来修改该值。
    • 不能更改原始变量的值。
    • 不能移交原始变量的所有权。
    let s = String::from("hello");
    let r = &s; // 不可变引用
    
    // 下面的所有操作都是不允许的
    // s.push_str(", world"); // 试图修改s的值
    // let s2 = s; // 试图移交s的所有权
  2. 对于可变引用 (&mut T):

    • 可以通过这个可变引用来修改该值。
    • 但在存在活跃的可变引用时,你不能更改原始变量的值。
    • 不能移交原始变量的所有权。
    let mut s = String::from("hello");
    let r = &mut s; // 可变引用
    
    r.push_str(", world"); // 通过r修改s的值是允许的
    // s.push_str("!"); // 这是不允许的,因为s有一个活跃的可变引用
    // let s2 = s; // 这也是不允许的,因为s有一个活跃的可变引用

下面是我和chatgpt的一些对话来解决我的困惑

我:我现在有2个问题,第一,如果在引用期间对可变变量的原始值进行更改,那么引用会随原始值进行更改吗??

第二,如果不小心将某个变量设置为不可变,但在引用时需要修改它的值,可以重新定义它吗?

chatgpt:

  • 如果在引用期间对可变变量的原始值进行更改,那么引用会随原始值进行更改吗?

答案是:不,你不能在拥有活跃引用的同时修改原始变量的值,无论是不可变引用还是可变引用。如果你尝试这样做,Rust编译器会给出错误。引用的主要目的是确保数据安全性,防止意外的数据修改和数据竞争。因此,当一个活跃的引用存在时,你不能修改其指向的数据。

示例:

let mut s = String::from("hello");
let r = &mut s; // 创建一个可变引用

// s.push_str(", world"); // 这会导致编译错误,因为s有一个活跃的引用
  • 如果不小心将某个变量设置为不可变,但在引用时需要修改它的值,可以重新定义它吗?

答案是:是的,你可以为该变量重新分配一个新的值,这会隐式地使之前的所有引用都失效。但是,如果你的意思是你想在已经创建了引用的情况下重新定义或修改这个变量,那么你不能这样做,因为这会违反Rust的所有权和引用规则。

但如果没有任何活跃的引用,你可以轻松地给一个不可变变量重新赋值:

let s = String::from("hello");
let mut s = s; // 重新定义s为可变
s.push_str(", world"); // 现在这是允许的,因为s是可变的

在上述代码中,我们首先定义了一个不可变的String变量s。然后,我们重新定义了一个可变的s变量,并为其分配了原始s的值。这是完全有效的,因为我们没有违反任何规则或持有任何活跃的引用。

我:这里的活跃引用在怎么理解

chatgpt:"活跃引用"是指在当前作用域或上下文中有效并可以被使用的引用。当一个引用被创建并指向某个值时,这个引用就是活跃的,直到它超出其作用域或被显式地使其失效。

让我们通过几个例子来理解这个概念:

  • 基本作用域内的活跃引用
fn main() {
    let mut s = String::from("hello");
    {
        let r = &mut s; // r是一个活跃的可变引用
        // 在这个作用域中,r是活跃的
    }
    // 当我们离开上面的作用域时,r不再是活跃的
    s.push_str(", world"); // 这是允许的,因为现在没有活跃的引用
}
  • 在函数中的引用
fn main() {
    let mut s = String::from("hello");
    let r = &mut s;  // r是一个活跃的可变引用
    modify(r);
    // 在函数modify调用结束后,r仍然是活跃的,因为它在main函数的作用域中定义
    // s.push_str(", world"); // 这会导致编译错误,因为r仍然是活跃的
}

fn modify(some_string: &mut String) {
    some_string.push_str(", world");
}

在上述代码中,尽管modify函数已经返回,但在main函数中,r仍然是一个活跃的引用,因此我们不能直接修改s

  • 使引用失效

在某些情况下,通过重新分配值,你可以显式地使引用失效。

let mut s = String::from("hello");
let r1 = &s;  // r1是一个活跃的不可变引用
let r2 = &s;  // r2也是一个活跃的不可变引用
s = String::from("world");  // s被重新赋值,这使得r1和r2都失效

总之,"活跃引用"是指在其作用域内有效并可以被使用的引用。当引用超出其作用域或被显式地使其失效时,它就不再是活跃的。Rust编译器使用这些规则来确保内存安全和数据一致性。

我:这里的显式和隐式怎么理解

chatgpt:

显式 (Explicit)

当我们说某件事是"显式"的,意思是它在代码中被明确地指定或声明。你可以直接从代码中看到并理解这个操作或行为。它需要程序员直接介入并指明要发生的事情。

示例:

  1. 显式类型注解: 在Rust中,虽然大多数时候编译器可以推断变量的类型,但你也可以显式地指定它。

     let x: i32 = 5;  // i32类型被显式地指定
  2. 显式地调用函数: 当你明确地调用函数或方法。

    let y = String::from("hello");  // 显式地调用from函数

隐式 (Implicit)

当我们说某件事是"隐式"的,意思是它在代码中并没有明确地表示出来,但它仍然会发生。这通常是由编程语言的规则、编译器的推断或某些默认行为导致的。

示例:

  1. 隐式类型推断: 在Rust中,编译器经常会根据上下文推断变量的类型。

    let x = 5;  // x的类型隐式地被推断为i32
  2. 隐式实现trait: 在某些情况下,Rust的类型会隐式地实现某些trait,如Copy trait,这取决于其内容。

  3. 隐式转换: 在某些编程语言中,不同的数据类型之间可能会有隐式转换。例如,在某些语言中,浮点数可能会隐式地转换为整数。

我:活跃引用超出作用域我是不是可以理解为生命周期结束了

chatgpt回答:是的,你的理解是正确的。当一个引用超出其作用域时,可以说它的"生命周期"已经结束。

在Rust中,每个引用都有一个称为"生命周期"的概念,这是一个编译时的概念,表示引用在何时有效或"活跃"。生命周期用于预防悬挂引用或无效引用,确保所有的引用都是安全的。

当你在代码中创建一个引用时,它的生命周期从被创建的地方开始,直到其作用域结束。一旦超出了这个作用域,引用就不再有效,因此它的生命周期就结束了。

{
    let s = String::from("hello");
    let r = &s;  // r的生命周期从这里开始
    // 在这个范围内,r是有效的
}
// 一旦我们离开上面的块,r的生命周期就结束了,r不再有效

在某些复杂的情况下,Rust可能需要显式的生命周期注解来理解引用的有效期,但在很多常见的情况下,Rust可以自动推断出正确的生命周期。

\