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

English Original

引用 vs 指针

你将学到: Rust 引用与 C# 指针及不安全上下文 (unsafe contexts) 的对比;生命周期基础;以及为什么编译时安全证明比 C# 的运行时检查(边界检查、空守卫)更强大。

难度: 🟡 中级

C# 指针 (不安全上下文)

// C# 不安全指针 (极少使用)
unsafe void UnsafeExample()
{
    int value = 42;
    int* ptr = &value;  // 指向数值的指针
    *ptr = 100;         // 解引用并修改
    Console.WriteLine(value);  // 100
}

Rust 引用 (默认安全)

#![allow(unused)]
fn main() {
// Rust 引用 (始终安全)
fn safe_example() {
    let mut value = 42;
    let ptr = &mut value;  // 可变引用
    *ptr = 100;           // 解引用并修改
    println!("{}", value); // 100
}

// 无需 "unsafe" 关键字 —— 借用检查器确保了安全性
}

为 C# 开发者准备的生命周期基础

// C# - 可能会返回已失效的引用
public class LifetimeIssues
{
    public string GetFirstWord(string input)
    {
        return input.Split(' ')[0];  // 返回新字符串 (安全)
    }
    
    public unsafe char* GetFirstChar(string input)
    {
        // 这将非常危险 —— 返回一个指向托管内存的指针
        fixed (char* ptr = input)
            return ptr;  // ❌ 错误:方法结束后 ptr 就会失效
    }
}
#![allow(unused)]
fn main() {
// Rust - 生命周期检查防止悬垂引用 (Dangling References)
fn get_first_word(input: &str) -> &str {
    input.split_whitespace().next().unwrap_or("")
    // ✅ 安全:返回的引用与输入具有相同的生命周期
}

fn invalid_reference() -> &str {
    let temp = String::from("hello");
    &temp  // ❌ 编译错误:temp 的生命周期不够长
    // temp 会在函数结束时被丢弃 (Dropped)
}

fn valid_reference() -> String {
    let temp = String::from("hello");
    temp  // ✅ 正常:所有权转移给了调用者
}
}

内存安全:运行时检查 vs 编译时证明

C# - 运行时安全网

// C# 依赖运行时检查和 GC
public class Buffer
{
    private byte[] data;
    
    public Buffer(int size)
    {
        data = new byte[size];
    }
    
    public void ProcessData(int index)
    {
        // 运行时边界检查
        if (index >= data.Length)
            throw new IndexOutOfRangeException();
            
        data[index] = 42;  // 安全,但在运行时进行检查
    }
    
    // 即使有 GC,通过事件/静态引用仍可能导致内存泄漏
    public static event Action<string> GlobalEvent;
    
    public void Subscribe()
    {
        GlobalEvent += HandleEvent;  // 可能产生内存泄漏
        // 忘记取消订阅 —— 对象将无法被回收
    }
    
    private void HandleEvent(string message) { /* ... */ }
    
    // 空引用异常依然可能发生
    public void ProcessUser(User user)
    {
        Console.WriteLine(user.Name.ToUpper());  // 如果 user.Name 为 null 则抛出 NullReferenceException
    }
    
    // 数组访问可能在运行时失败
    public int GetValue(int[] array, int index)
    {
        return array[index];  // 可能抛出 IndexOutOfRangeException
    }
}

Rust - 编译时保证

#![allow(unused)]
fn main() {
struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    fn new(size: usize) -> Self {
        Buffer {
            data: vec![0; size],
        }
    }
    
    fn process_data(&mut self, index: usize) {
        // 当可以证明安全时,编译器会优化掉边界检查
        if let Some(item) = self.data.get_mut(index) {
            *item = 42;  // 安全访问,在编译时得到证明
        }
        // 或者使用带显式边界检查的索引:
        // self.data[index] = 42;  // 在调试模式下会崩溃,但内存是安全的
    }
    
    // 不可能出现内存泄漏 —— 所有权系统防止了它们
    fn process_with_closure<F>(&mut self, processor: F) 
    where F: FnOnce(&mut Vec<u8>)
    {
        processor(&mut self.data);
        // 当 processor 离开作用域时,它会自动被清理
        // 无法创建悬垂引用或导致内存泄漏
    }
    
    // 不可能出现空指针解引用 —— 因为没有空指针!
    fn process_user(&self, user: &User) {
        println!("{}", user.name.to_uppercase());  // user.name 不可能为 null
    }
    
    // 数组访问带有边界检查或显式的不安全操作
    fn get_value(array: &[i32], index: usize) -> Option<i32> {
        array.get(index).copied()  // 如果越界则返回 None
    }
    
    // 或者如果你确切知道自己在做什么,可以使用显式不安全操作:
    /// # Safety
    /// `index` 必须小于 `array.len()`。
    unsafe fn get_value_unchecked(array: &[i32], index: usize) -> i32 {
        *array.get_unchecked(index)  // 速度极快,但必须手动证明边界安全
    }
}

struct User {
    name: String,  // 在 Rust 中 String 不可能为 null
}

// 所有权防止“释放后使用” (Use-after-free)
fn ownership_example() {
    let data = vec![1, 2, 3, 4, 5];
    let reference = &data[0];  // 借用数据
    
    // drop(data);  // ❌ 错误:在被借用期间不能丢弃
    println!("{}", reference);  // 保证在此处安全
}

// 借用防止数据竞态 (Data Races)
fn borrowing_example(data: &mut Vec<i32>) {
    let first = &data[0];  // 不可变借用
    // data.push(6);  // ❌ 错误:在存在不可变借用时不能进行可变借用
    println!("{}", first);  // 保证没有数据竞态
}
}
graph TD
    subgraph "C# 运行时安全"
        CS_RUNTIME["运行时检查"]
        CS_GC["垃圾回收器"]
        CS_EXCEPTIONS["异常处理"]
        CS_BOUNDS["运行时边界检查"]
        CS_NULL["空引用异常 (NRE)"]
        CS_LEAKS["可能发生内存泄漏"]
        CS_OVERHEAD["性能开销"]
        
        CS_RUNTIME --> CS_BOUNDS
        CS_RUNTIME --> CS_NULL
        CS_GC --> CS_LEAKS
        CS_EXCEPTIONS --> CS_OVERHEAD
    end
    
    subgraph "Rust 编译时安全"
        RUST_OWNERSHIP["所有权系统"]
        RUST_BORROWING["借用检查器"]
        RUST_TYPES["类型系统"]
        RUST_ZERO_COST["零成本抽象"]
        RUST_NO_NULL["无空指针"]
        RUST_NO_LEAKS["无内存泄漏"]
        RUST_FAST["极致性能"]
        
        RUST_OWNERSHIP --> RUST_NO_LEAKS
        RUST_BORROWING --> RUST_NO_NULL
        RUST_TYPES --> RUST_ZERO_COST
        RUST_ZERO_COST --> RUST_FAST
    end
    
    style CS_NULL fill:#ffcdd2,color:#000
    style CS_LEAKS fill:#ffcdd2,color:#000
    style CS_OVERHEAD fill:#fff3e0,color:#000
    style RUST_NO_NULL fill:#c8e6c9,color:#000
    style RUST_NO_LEAKS fill:#c8e6c9,color:#000
    style RUST_FAST fill:#c8e6c9,color:#000

练习

🏋️ 练习:找出安全 Bug (点击展开)

这段 C# 代码中有一个细微的安全 Bug。找出它,然后编写对应的 Rust 版本并解释为什么 Rust 版本无法通过编译:

public List<int> GetEvenNumbers(List<int> numbers)
{
    var result = new List<int>();
    foreach (var n in numbers)
    {
        if (n % 2 == 0)
        {
            result.Add(n);
            numbers.Remove(n);  // Bug:在迭代时修改集合
        }
    }
    return result;
}
🔑 参考答案

C# Bug:在迭代时修改 numbers 会在运行时抛出 InvalidOperationException。这在代码审查中很容易被忽略。

fn get_even_numbers(numbers: &mut Vec<i32>) -> Vec<i32> {
    let mut result = Vec::new();
    for &n in numbers.iter() {
        if n % 2 == 0 {
            result.push(n);
            // numbers.retain(|&x| x != n);
            // ❌ 错误:不能将 `*numbers` 作为可变借用,
            //    因为它已经被作为不可变引用(由迭代器)借用了。
        }
    }
    result
}

// 惯用的 Rust 写法:使用 partition 或 retain
fn get_even_numbers_idiomatic(numbers: &mut Vec<i32>) -> Vec<i32> {
    let evens: Vec<i32> = numbers.iter().copied().filter(|n| n % 2 == 0).collect();
    numbers.retain(|n| n % 2 != 0); // 在迭代后删除偶数
    evens
}

fn main() {
    let mut nums = vec![1, 2, 3, 4, 5, 6];
    let evens = get_even_numbers_idiomatic(&mut nums);
    assert_eq!(evens, vec![2, 4, 6]);
    assert_eq!(nums, vec![1, 3, 5]);
}

关键洞察:Rust 的借用检查器在编译时就杜绝了整个“边迭代边修改”类别的 Bug。C# 在运行时捕获此类错误,而许多语言根本不捕获它们。