Understanding Ownership
理解所有权
What you’ll learn: Rust’s ownership system, why
let s2 = s1invalidatess1unlike C# reference copying, the three ownership rules,Copyvs move types, borrowing with&and&mut, and how the borrow checker replaces garbage collection.
本章将学到什么: 理解 Rust 的所有权系统,理解为什么let s2 = s1会让s1失效而不是像 C# 那样复制引用,掌握三条所有权规则,分清Copy类型和移动类型,理解&、&mut借用,以及借用检查器如何替代垃圾回收。Difficulty: 🟡 Intermediate
难度: 🟡 进阶
Ownership is Rust’s most distinctive feature and usually the biggest conceptual jump for C# developers. The trick is to stop treating it as mysticism and instead read it as a set of concrete rules about who is responsible for a value at any moment.
所有权是 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 所有权规则
- Each value has exactly one owner unless shared ownership is made explicit with types like
Rc<T>orArc<T>.
每个值在默认情况下都只有一个拥有者,除非显式引入Rc<T>、Arc<T>这类共享所有权方案。 - When the owner goes out of scope, the value is dropped and cleanup happens deterministically.
拥有者离开作用域时,值就会被销毁,清理时机是确定的,不靠 GC 碰运气。 - Ownership can be transferred by moving the value.
所有权可以被转移,也就是常说的 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
}
}
这三条规则其实不绕,就是很严。
一旦接受“值在某个时刻只能有一个明确负责者”,很多报错都会突然变得有逻辑。Rust 不是故意刁难,而是在逼代码把资源归属说清楚。
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
}
这里最大的心理落差就在于:C# 的“赋值”经常只是多了一个指向同一对象的引用;Rust 的“赋值”在很多类型上代表所有权转交。
所以很多刚上手的人会觉得“怎么赋个值原变量就死了”,其实不是原变量死了,是责任被正式交出去了。
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
}
并不是所有 Rust 类型都会 move。
像整数、布尔、字符这类小而固定的值,通常实现了 Copy,赋值就是复制;而像 String、Vec<T>、大多数 struct 这类拥有资源的类型,默认就是 move。这条线要早点分清,不然后面会老在脑内打架。
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);
}
}
这类例子很适合把 move 想明白。
所谓 move,不一定意味着“底层内存真的被搬来搬去”,更准确地说,是变量和它所拥有资源之间的归属关系在变。
Borrowing Basics
借用基础
Borrowing in Rust is a bit like passing references in C#, except the compiler actually enforces the safety contract instead of trusting everyone to behave.
Rust 的借用有点像 C# 里的引用传递,但区别在于: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
}
这里要养成一个习惯:看到 &T 就想“只读借用”,看到 &mut T 就想“独占可变借用”。
这不是语法细枝末节,而是 Rust 代码阅读的基本功。很多 API 的语义,其实在参数类型上已经写得明明白白了。
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);
}
}
这几条规则看起来死板,但它们正是 Rust 能把数据竞争和悬垂引用挡在编译阶段的原因。
“多个只读可以同时存在,但可变借用必须独占”这条一旦吃透,后面大量借用检查器报错都会自己解开一半。
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 很喜欢把“什么时候能改,什么时候只能看”写成类型规则。
所以 API 设计也会跟着更清楚。返回 &[i32] 就是只读视图,返回 &mut Vec<i32> 就是明确放出独占修改权,没有模糊地带。
Move Semantics
Move 语义
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 的 move 语义
#![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
}
}
这一步经常能让人理解一个关键事实:Rust 不是“所有 struct 都不能复制”,而是“默认别偷偷复制可能很重或者会造成语义歧义的值”。
如果一个类型足够小、语义上也适合按位复制,就让它实现 Copy。如果不适合,那就老老实实走 move。
When Values Are Moved
值会在什么时候被 move
#![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
}
}
只要参数类型或赋值行为要求拿值本体,move 就会发生。
这也是为什么看函数签名特别重要。很多时候问题根本不在调用点,而在于被调用函数要的是拥有者,还是只借一下。
Avoiding Moves with Borrowing
用借用避免 move
#![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
}
}
Rust 不是“没有内存管理”,而是把很多管理工作前置到了编译期。
GC 的优势是省心,代价是运行时开销和清理时机不确定;RAII 的优势是确定性强、零运行时成本,代价是得先把所有权和借用这套脑回路练出来。
graph TD
subgraph "C# Memory Management"
CS_ALLOC["Object Allocation<br/>new Person()"]
CS_HEAP["Managed Heap<br/>托管堆"]
CS_REF["References point to heap<br/>引用指向堆对象"]
CS_GC_CHECK["GC periodically checks<br/>for unreachable objects<br/>周期性扫描不可达对象"]
CS_SWEEP["Mark and sweep<br/>标记清扫"]
CS_PAUSE["[ERROR] GC pause times<br/>可能有停顿"]
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/>单一拥有者"]
RUST_BORROW["Borrowing system<br/>&T, &mut T"]
RUST_SCOPE["Scope-based cleanup<br/>Drop trait"]
RUST_COMPILE["Compile-time verification<br/>编译期验证"]
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 by default<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
🏋️ 练习:修掉借用检查器报错
Challenge: Each snippet below contains a borrow-checker problem. 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 instead of taking ownership.format!()默认借用参数,并不会直接拿走所有权。- Primitive types such as
i32implementCopy, so indexing can copy the value instead of creating a long-lived borrow.
像i32这种基础类型实现了Copy,因此可以把索引结果直接复制出来,避免借用时间拉太长。 - Returning an owned
Stringis often exactly the right thing to do and avoids lifetime headaches.
返回一个拥有所有权的String往往就是最正确的做法,反而能避开一堆生命周期麻烦。