Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Understanding Ownership | 理解所有权

What you’ll learn: Rust’s ownership system - why let s2 = s1 invalidates s1 (unlike C# reference copying), the three ownership rules, Copy vs Move types, borrowing with & and &mut, and how the borrow checker replaces garbage collection.

你将学到什么: Rust 的所有权系统,以及为什么 let s2 = s1 会让 s1 失效(这不同于 C# 的引用复制); 三条所有权规则、CopyMove 类型的区别、使用 &&mut 的借用方式, 以及借用检查器如何替代垃圾回收机制。

Difficulty: Intermediate

难度: 中级

Ownership is Rust’s most unique feature and the biggest conceptual shift for C# developers. Let’s approach it step by step.

所有权是 Rust 最独特的特性,也是 C# 开发者在思维方式上需要完成的最大转变。我们一步一步来看。

C# Memory Model (Review) | C# 内存模型(回顾)

// C# - Automatic memory management
public void ProcessData()
{
    var data = new List<int> { 1, 2, 3, 4, 5 };
    ProcessList(data);
    // data is still accessible here
    Console.WriteLine(data.Count);  // Works fine
    
    // GC will clean up when no references remain
}

public void ProcessList(List<int> list)
{
    list.Add(6);  // Modifies the original list
}

Rust Ownership Rules | Rust 所有权规则

  1. Each value has exactly one owner (unless you opt into shared ownership with Rc<T>/Arc<T> - see Smart Pointers)
  2. 每个值都恰好有一个所有者(除非你显式使用 Rc<T>/Arc<T> 进行共享所有权,参见 智能指针
  3. When the owner goes out of scope, the value is dropped (deterministic cleanup - see Drop)
  4. 当所有者离开作用域时,值会被释放(这是确定性的清理机制,参见 Drop
  5. Ownership can be transferred (moved)
  6. 所有权可以被转移(move)
#![allow(unused)]
fn main() {
// Rust - Explicit ownership management
fn process_data() {
    let data = vec![1, 2, 3, 4, 5];  // data owns the vector
    process_list(data);              // Ownership moved to function
    // println!("{:?}", data);       // Error: data no longer owned here
}

fn process_list(mut list: Vec<i32>) {  // list now owns the vector
    list.push(6);
    // list is dropped here when function ends
}
}

Understanding “Move” for C# Developers | 面向 C# 开发者理解 “Move”

// C# - References are copied, objects stay in place
// (Only reference types - classes - work this way;
//  C# value types like struct behave differently)
var original = new List<int> { 1, 2, 3 };
var reference = original;  // Both variables point to same object
original.Add(4);
Console.WriteLine(reference.Count);  // 4 - same object
#![allow(unused)]
fn main() {
// Rust - Ownership is transferred
let original = vec![1, 2, 3];
let moved = original;       // Ownership transferred
// println!("{:?}", original);  // Error: original no longer owns the data
println!("{:?}", moved);    // Works: moved now owns the data
}

Copy Types vs Move Types | Copy 类型与 Move 类型

#![allow(unused)]
fn main() {
// Copy types (like C# value types) - copied, not moved
let x = 5;         // i32 implements Copy
let y = x;         // x is copied to y
println!("{}", x); // Works: x is still valid

// Move types (like C# reference types) - moved, not copied
let s1 = String::from("hello");  // String doesn't implement Copy
let s2 = s1;                     // s1 is moved to s2
// println!("{}", s1);           // Error: s1 is no longer valid
}
Copy 类型(类似 C# 的值类型)会被复制,而不是移动。
Move 类型(类似很多 C# 的引用类型使用场景)默认会转移所有权,而不是自动复制。

Practical Example: Swapping Values | 实用示例:交换值

// C# - Simple reference swapping
public void SwapLists(ref List<int> a, ref List<int> b)
{
    var temp = a;
    a = b;
    b = temp;
}
#![allow(unused)]
fn main() {
// Rust - Ownership-aware swapping
fn swap_vectors(a: &mut Vec<i32>, b: &mut Vec<i32>) {
    std::mem::swap(a, b);  // Built-in swap function
}

// Or manual approach
fn manual_swap() {
    let mut a = vec![1, 2, 3];
    let mut b = vec![4, 5, 6];
    
    let temp = a;  // Move a to temp
    a = b;         // Move b to a
    b = temp;      // Move temp to b
    
    println!("a: {:?}, b: {:?}", a, b);
}
}
在 Rust 中,交换不只是“引用换一下”这么简单,而是要明确考虑值的所有权由谁持有。

Borrowing Basics | 借用基础

Borrowing is like getting a reference in C#, but with compile-time safety guarantees.

借用有点像在 C# 中拿到一个引用,但 Rust 会在编译期提供更严格的安全保证。

C# Reference Parameters | C# 引用参数

// C# - ref and out parameters
public void ModifyValue(ref int value)
{
    value += 10;
}

public void ReadValue(in int value)  // readonly reference
{
    Console.WriteLine(value);
}

public bool TryParse(string input, out int result)
{
    return int.TryParse(input, out result);
}

Rust Borrowing | Rust 借用

// Rust - borrowing with & and &mut
fn modify_value(value: &mut i32) {  // Mutable borrow
    *value += 10;
}

fn read_value(value: &i32) {        // Immutable borrow
    println!("{}", value);
}

fn main() {
    let mut x = 5;
    
    read_value(&x);       // Borrow immutably
    modify_value(&mut x); // Borrow mutably
    
    println!("{}", x);    // x is still owned here
}

Borrowing Rules (Enforced at Compile Time!) | 借用规则(在编译期强制执行)

#![allow(unused)]
fn main() {
fn borrowing_rules() {
    let mut data = vec![1, 2, 3];
    
    // Rule 1: Multiple immutable borrows are OK
    let r1 = &data;
    let r2 = &data;
    println!("{:?} {:?}", r1, r2);  // Works
    
    // Rule 2: Only one mutable borrow at a time
    let r3 = &mut data;
    // let r4 = &mut data;  // Error: cannot borrow mutably twice
    // let r5 = &data;      // Error: cannot borrow immutably while borrowed mutably
    
    r3.push(4);  // Use the mutable borrow
    // r3 goes out of scope here
    
    // Rule 3: Can borrow again after previous borrows end
    let r6 = &data;  // Works now
    println!("{:?}", r6);
}
}
规则总结:
1. 可以同时存在多个不可变借用。
2. 同一时刻只能有一个可变借用。
3. 可变借用存在时,不能再有不可变借用。
4. 之前的借用结束后,才能重新借用。

C# vs Rust: Reference Safety | C# 与 Rust:引用安全性对比

// C# - Potential runtime errors
public class ReferenceSafety
{
    private List<int> data = new List<int>();
    
    public List<int> GetData() => data;  // Returns reference to internal data
    
    public void UnsafeExample()
    {
        var reference = GetData();
        
        // Another thread could modify data here!
        Thread.Sleep(1000);
        
        // reference might be invalid or changed
        reference.Add(42);  // Potential race condition
    }
}
#![allow(unused)]
fn main() {
// Rust - Compile-time safety
pub struct SafeContainer {
    data: Vec<i32>,
}

impl SafeContainer {
    // Return immutable borrow - caller can't modify
    // Prefer &[i32] over &Vec<i32> - accept the broadest type
    pub fn get_data(&self) -> &[i32] {
        &self.data
    }
    
    // Return mutable borrow - exclusive access guaranteed
    pub fn get_data_mut(&mut self) -> &mut Vec<i32> {
        &mut self.data
    }
}

fn safe_example() {
    let mut container = SafeContainer { data: vec![1, 2, 3] };
    
    let reference = container.get_data();
    // container.get_data_mut();  // Error: can't borrow mutably while immutably borrowed
    
    println!("{:?}", reference);  // Use immutable reference
    // reference goes out of scope here
    
    let mut_reference = container.get_data_mut();  // Now OK
    mut_reference.push(4);
}
}
Rust 的借用规则把很多 C# 中可能在运行时才暴露的问题,提前到了编译阶段。

Move Semantics | 移动语义

C# Value Types vs Reference Types | C# 值类型与引用类型

// C# - Value types are copied
struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

var p1 = new Point { X = 1, Y = 2 };
var p2 = p1;  // Copy
p2.X = 10;
Console.WriteLine(p1.X);  // Still 1

// C# - Reference types share the object
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;  // Reference copy
list2.Add(4);
Console.WriteLine(list1.Count);  // 4 - same object

Rust Move Semantics | Rust 的移动语义

#![allow(unused)]
fn main() {
// Rust - Move by default for non-Copy types
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn move_example() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;  // Move (not copy)
    // println!("{:?}", p1);  // Error: p1 was moved
    println!("{:?}", p2);    // Works
}

// To enable copying, implement Copy trait
#[derive(Debug, Copy, Clone)]
struct CopyablePoint {
    x: i32,
    y: i32,
}

fn copy_example() {
    let p1 = CopyablePoint { x: 1, y: 2 };
    let p2 = p1;  // Copy (because it implements Copy)
    println!("{:?}", p1);  // Works
    println!("{:?}", p2);  // Works
}
}

When Values Are Moved | 值会在什么时候发生移动

#![allow(unused)]
fn main() {
fn demonstrate_moves() {
    let s = String::from("hello");
    
    // 1. Assignment moves
    let s2 = s;  // s moved to s2
    
    // 2. Function calls move
    take_ownership(s2);  // s2 moved into function
    
    // 3. Returning from functions moves
    let s3 = give_ownership();  // Return value moved to s3
    
    println!("{}", s3);  // s3 is valid
}

fn take_ownership(s: String) {
    println!("{}", s);
    // s is dropped here
}

fn give_ownership() -> String {
    String::from("yours")  // Ownership moved to caller
}
}

Avoiding Moves with Borrowing | 用借用避免移动

#![allow(unused)]
fn main() {
fn demonstrate_borrowing() {
    let s = String::from("hello");
    
    // Borrow instead of move
    let len = calculate_length(&s);  // s is borrowed
    println!("'{}' has length {}", s, len);  // s is still valid
}

fn calculate_length(s: &String) -> usize {
    s.len()  // s is not owned, so it's not dropped
}
}
只要函数只需要“使用”一个值,而不需要“接管”它,就优先考虑借用而不是移动。

Memory Management: GC vs RAII | 内存管理:GC 与 RAII

C# Garbage Collection | C# 垃圾回收

// C# - Automatic memory management
public class Person
{
    public string Name { get; set; }
    public List<string> Hobbies { get; set; } = new List<string>();
    
    public void AddHobby(string hobby)
    {
        Hobbies.Add(hobby);  // Memory allocated automatically
    }
    
    // No explicit cleanup needed - GC handles it
    // But IDisposable pattern for resources
}

using var file = new FileStream("data.txt", FileMode.Open);
// 'using' ensures Dispose() is called

Rust Ownership and RAII | Rust 的所有权与 RAII

#![allow(unused)]
fn main() {
// Rust - Compile-time memory management
pub struct Person {
    name: String,
    hobbies: Vec<String>,
}

impl Person {
    pub fn add_hobby(&mut self, hobby: String) {
        self.hobbies.push(hobby);  // Memory management tracked at compile time
    }
    
    // Drop trait automatically implemented - cleanup is guaranteed
    // Compare to C#'s IDisposable:
    //   C#:   using var file = new FileStream(...)    // Dispose() called at end of using block
    //   Rust: let file = File::open(...)?             // drop() called at end of scope - no 'using' needed
}

// RAII - Resource Acquisition Is Initialization
{
    let file = std::fs::File::open("data.txt")?;
    // File automatically closed when 'file' goes out of scope
    // No 'using' statement needed - handled by type system
}
}
GC 的核心特点是“自动回收,但时间不确定”。
RAII 的核心特点是“离开作用域就立即清理,而且无需运行时扫描堆”。
graph TD
    subgraph "C# Memory Management"
        CS_ALLOC["Object Allocation<br/>new Person()"]
        CS_HEAP["Managed Heap"]
        CS_REF["References point to heap"]
        CS_GC_CHECK["GC periodically checks<br/>for unreachable objects"]
        CS_SWEEP["Mark and sweep<br/>collection"]
        CS_PAUSE["[ERROR] GC pause times"]
        
        CS_ALLOC --> CS_HEAP
        CS_HEAP --> CS_REF
        CS_REF --> CS_GC_CHECK
        CS_GC_CHECK --> CS_SWEEP
        CS_SWEEP --> CS_PAUSE
        
        CS_ISSUES["[ERROR] Non-deterministic cleanup<br/>[ERROR] Memory pressure<br/>[ERROR] Finalization complexity<br/>[OK] Easy to use"]
    end
    
    subgraph "Rust Ownership System"
        RUST_ALLOC["Value Creation<br/>Person { ... }"]
        RUST_OWNER["Single owner<br/>on stack or heap"]
        RUST_BORROW["Borrowing system<br/>&T, &mut T"]
        RUST_SCOPE["Scope-based cleanup<br/>Drop trait"]
        RUST_COMPILE["Compile-time verification"]
        
        RUST_ALLOC --> RUST_OWNER
        RUST_OWNER --> RUST_BORROW
        RUST_BORROW --> RUST_SCOPE
        RUST_SCOPE --> RUST_COMPILE
        
        RUST_BENEFITS["[OK] Deterministic cleanup<br/>[OK] Zero runtime cost<br/>[OK] No memory leaks<br/>[ERROR] Learning curve"]
    end
    
    style CS_ISSUES fill:#ffebee,color:#000
    style RUST_BENEFITS fill:#e8f5e8,color:#000
    style CS_PAUSE fill:#ffcdd2,color:#000
    style RUST_COMPILE fill:#c8e6c9,color:#000

Exercise: Fix the Borrow Checker Errors | 练习:修复借用检查器错误 (click to expand / 点击展开)

Challenge: Each snippet below has a borrow checker error. Fix them without changing the output.

挑战: 下面每个代码片段都包含一个借用检查器错误。请在不改变输出结果的前提下修复它们。

#![allow(unused)]
fn main() {
// 1. Move after use
fn problem_1() {
    let name = String::from("Alice");
    let greeting = format!("Hello, {name}!");
    let upper = name.to_uppercase();  // hint: borrow instead of move
    println!("{greeting} - {upper}");
}

// 2. Mutable + immutable borrow overlap
fn problem_2() {
    let mut numbers = vec![1, 2, 3];
    let first = &numbers[0];
    numbers.push(4);            // hint: reorder operations
    println!("first = {first}");
}

// 3. Returning a reference to a local
fn problem_3() -> String {
    let s = String::from("hello");
    s   // hint: return owned value, not &str
}
}
Solution | 参考答案
#![allow(unused)]
fn main() {
// 1. format! already borrows - the fix is that format! takes a reference.
//    The original code actually compiles! But if we had `let greeting = name;`
//    then fix by using &name:
fn solution_1() {
    let name = String::from("Alice");
    let greeting = format!("Hello, {}!", &name); // borrow
    let upper = name.to_uppercase();             // name still valid
    println!("{greeting} - {upper}");
}

// 2. Use the immutable borrow before the mutable operation:
fn solution_2() {
    let mut numbers = vec![1, 2, 3];
    let first = numbers[0]; // copy the i32 value (i32 is Copy)
    numbers.push(4);
    println!("first = {first}");
}

// 3. Return the owned String (already correct - common beginner confusion):
fn solution_3() -> String {
    let s = String::from("hello");
    s // ownership transferred to caller - this is the correct pattern
}
}

Key takeaways:

  • format!() borrows its arguments - it doesn’t move them
  • Primitive types like i32 implement Copy, so indexing copies the value
  • Returning an owned value transfers ownership to the caller - no lifetime issues

关键要点:

  • format!() 会借用参数,而不是移动参数
  • i32 这样的基本类型实现了 Copy,因此索引取得的是复制后的值
  • 返回拥有所有权的值时,所有权会转移给调用者,不会引发生命周期问题