Rust array type
Rust 的数组类型
What you’ll learn: Rust’s core data structures — arrays, tuples, slices, strings, structs,
Vec, andHashMap. This is a dense chapter; focus on understandingStringvs&strand how structs work. You’ll revisit references and borrowing in depth in chapter 7.
本章将学到什么: Rust 里最常用的几类核心数据结构:数组、元组、切片、字符串、结构体、Vec和HashMap。这一章信息量比较大,先重点盯住String和&str的区别,以及结构体是怎么工作的。引用和借用会在第 7 章再深入展开。
- Arrays contain a fixed number of elements of the same type.
数组里装的是固定数量、相同类型的元素。- Like all other Rust types, arrays are immutable by default unless
mutis used.
和 Rust 里其他类型一样,数组默认也是不可变的,除非显式写mut。 - Arrays are indexed using
[]and the access is bounds-checked. Use.len()to get the array length.
数组用[]索引,而且会做边界检查。数组长度可以通过.len()取得。
- Like all other Rust types, arrays are immutable by default unless
fn get_index(y : usize) -> usize {
y+1
}
fn main() {
// Initializes an array of 10 elements and sets all to 42
let a : [u8; 3] = [42; 3];
// Alternative syntax
// let a = [42u8, 42u8, 42u8];
for x in a {
println!("{x}");
}
let y = get_index(a.len());
// Commenting out the below will cause a panic
//println!("{}", a[y]);
}
Rust array type continued
Rust 数组补充说明
- Arrays can be nested.
数组还可以继续嵌套数组。- Rust has several built-in formatters for printing. In the example below,
:?is the debug formatter, and:#?can be used for pretty printing. These formatters can also be customized per type later on.
Rust 内置了几种常用打印格式。下面例子里的:?是调试打印格式,:#?则是更适合阅读的 pretty print。后面也会看到,这些输出格式还能按类型自定义。
- Rust has several built-in formatters for printing. In the example below,
fn main() {
let a = [
[40, 0], // Define a nested array
[41, 0],
[42, 1],
];
for x in a {
println!("{x:?}");
}
}
Rust tuples
Rust 的元组
- Tuples have a fixed size and can group arbitrary types into one compound value.
元组也是固定大小,但它能把不同类型的值组合到一起。- Individual elements are accessed by position:
.0,.1,.2, and so on. The empty tuple()is called the unit value and is roughly the Rust equivalent of a void return value.
元组元素按位置访问,也就是.0、.1、.2这种写法。空元组()叫 unit value,大致可以看成 Rust 里的“空返回值”。 - Rust also supports tuple destructuring, which makes it easy to bind names to each element.
Rust 还支持元组解构,能很方便地把各个位置的值分别绑定到变量上。
- Individual elements are accessed by position:
fn get_tuple() -> (u32, bool) {
(42, true)
}
fn main() {
let t : (u8, bool) = (42, true);
let u : (u32, bool) = (43, false);
println!("{}, {}", t.0, t.1);
println!("{}, {}", u.0, u.1);
let (num, flag) = get_tuple(); // Tuple destructuring
println!("{num}, {flag}");
}
Rust references
Rust 的引用
- References in Rust are roughly comparable to pointers in C, but with much stricter rules.
Rust 的引用和 C 里的指针有点像,但规则严格得多,不是一个量级。- Any number of immutable references may coexist at the same time. A reference also cannot outlive the scope of the value it points to. That idea is the basis of lifetimes, which will be discussed in detail later.
同一时间可以存在任意多个不可变引用,而且引用的存活时间绝对不能超过它指向的值。这背后就是生命周期的核心概念,后面会单独细讲。 - Only one mutable reference to a mutable value may exist at a time, and it cannot overlap with other references.
可变引用则更严格:同一时刻只能有一个,而且不能和其他引用重叠。
- Any number of immutable references may coexist at the same time. A reference also cannot outlive the scope of the value it points to. That idea is the basis of lifetimes, which will be discussed in detail later.
fn main() {
let mut a = 42;
{
let b = &a;
let c = b;
println!("{} {}", *b, *c); // The compiler automatically dereferences *c
// Illegal because b and still are still in scope
// let d = &mut a;
}
let d = &mut a; // Ok: b and c are not in scope
*d = 43;
}
Rust slices
Rust 的切片
- References can be used to create views over part of an array.
引用还能用来从数组里切出一段视图,也就是切片。- Arrays have a compile-time fixed length, while slices can describe a range of arbitrary size. Internally, a slice is a fat pointer containing both a start pointer and a length.
数组长度在编译期就固定了,而切片只是“看向其中一段”的视图,长度可以变化。底层上,切片是一个胖指针,里面既有起始位置,也有长度信息。
- Arrays have a compile-time fixed length, while slices can describe a range of arbitrary size. Internally, a slice is a fat pointer containing both a start pointer and a length.
fn main() {
let a = [40, 41, 42, 43];
let b = &a[1..a.len()]; // A slice starting with the second element in the original
let c = &a[1..]; // Same as the above
let d = &a[..]; // Same as &a[0..] or &a[0..a.len()]
println!("{b:?} {c:?} {d:?}");
}
Rust constants and statics
Rust 的常量与静态变量
- The
constkeyword defines a constant value. Constant expressions are evaluated at compile time and typically get inlined into the final program.const用来定义常量值。常量会在编译期求值,通常会被直接内联进程序里。 - The
statickeyword defines a true global variable similar to what C/C++ programs use. A static has a fixed memory address and exists for the entire lifetime of the program.static则更像 C/C++ 里的全局变量:有固定地址,程序整个生命周期里都一直存在。
const SECRET_OF_LIFE: u32 = 42;
static GLOBAL_VARIABLE : u32 = 2;
fn main() {
println!("The secret of life is {}", SECRET_OF_LIFE);
println!("Value of global variable is {GLOBAL_VARIABLE}")
}
Rust strings: String vs &str
Rust 字符串:String 和 &str 的区别
- Rust has two string types with different jobs.
Rust 里有 两种 字符串类型,它们分工完全不同。Stringis owned, heap-allocated, and growable. You can roughly compare it to a manually managed heap buffer in C or to C++std::string.String是拥有型、堆分配、可增长的字符串。大致可以类比 C 里自己管理的堆缓冲区,或者 C++ 的std::string。&stris a borrowed string slice. It is lightweight, read-only, and closer in spirit toconst char*plus a length, or to C++std::string_view, except that Rust actually checks its lifetime so it cannot dangle.&str是借用来的字符串切片,轻量、只读,更接近“带长度的const char*”或者 C++ 的std::string_view。区别在于 Rust 真会检查生命周期,所以它不能悬空。- Rust strings are not null-terminated. They track length explicitly and are guaranteed to contain valid UTF-8.
Rust 字符串也不是靠结尾\0判断长度的,而是显式记录长度,并且保证内容是合法 UTF-8。
For C++ developers:
String≈std::string,&str≈std::string_view. Unlikestd::string_view, a Rust&stris guaranteed valid for its whole lifetime by the borrow checker.
给 C++ 开发者:String可以近似看成std::string,&str可以近似看成std::string_view。但&str比std::string_view更硬,因为借用检查器会保证它在整个生命周期里都有效。
String vs &str: owned vs borrowed
String 和 &str:拥有型与借用型
Production patterns: See JSON handling: nlohmann::json → serde for how string handling works with serde in production code.
生产代码里的用法: 可以顺手参考 JSON handling: nlohmann::json → serde,看看真实项目里字符串和 serde 是怎么配合的。
| Aspect | C char* | C++ std::string | Rust String | Rust &str |
|---|---|---|---|---|
| Memory | Manual malloc / free手动管理 | Owns heap storage 拥有堆内存 | Owns heap storage and auto-frees 拥有堆内存并自动释放 | Borrowed reference with lifetime checks 带生命周期检查的借用引用 |
| Mutability | Usually mutable through the pointer 通常可变 | Mutable 可变 | Mutable if declared mut写成 mut 才能改 | Always immutable 始终只读 |
| Size info | None, relies on '\0'靠终止符 | Tracks length and capacity 显式记录长度和容量 | Tracks length and capacity 显式记录长度和容量 | Tracks length as part of the fat pointer 长度包含在切片元数据里 |
| Encoding | Unspecified 编码不受约束 | Unspecified 编码不受约束 | Valid UTF-8 保证合法 UTF-8 | Valid UTF-8 保证合法 UTF-8 |
| Null terminator | Required 需要 | Required for c_str() interop和 C 交互时才需要 | Not used 不用 | Not used 不用 |
fn main() {
// &str - string slice (borrowed, immutable, usually a string literal)
let greeting: &str = "Hello"; // Points to read-only memory
// String - owned, heap-allocated, growable
let mut owned = String::from(greeting); // Copies data to heap
owned.push_str(", World!"); // Grow the string
owned.push('!'); // Append a single character
// Converting between String and &str
let slice: &str = &owned; // String -> &str (free, just a borrow)
let owned2: String = slice.to_string(); // &str -> String (allocates)
let owned3: String = String::from(slice); // Same as above
// String concatenation (note: + consumes the left operand)
let hello = String::from("Hello");
let world = String::from(", World!");
let combined = hello + &world; // hello is moved (consumed), world is borrowed
// println!("{hello}"); // Won't compile: hello was moved
// Use format! to avoid move issues
let a = String::from("Hello");
let b = String::from("World");
let combined = format!("{a}, {b}!"); // Neither a nor b is consumed
println!("{combined}");
}
Why you cannot index strings with []
为什么字符串不能直接用 [] 索引
fn main() {
let s = String::from("hello");
// let c = s[0]; // Won't compile! Rust strings are UTF-8, not byte arrays
// Safe alternatives:
let first_char = s.chars().next(); // Option<char>: Some('h')
let as_bytes = s.as_bytes(); // &[u8]: raw UTF-8 bytes
let substring = &s[0..1]; // &str: "h" (byte range, must be valid UTF-8 boundary)
println!("First char: {:?}", first_char);
println!("Bytes: {:?}", &as_bytes[..5]);
}
Rust 不允许像数组那样随手取 s[0],核心原因是 UTF-8 字符串里“第几个字符”和“第几个字节”根本不是一回事。
这条限制看起来麻烦,其实是在防止把多字节字符切坏。
Exercise: String manipulation
练习:字符串处理
🟢 Starter
🟢 基础练习
- Write a function
fn count_words(text: &str) -> usizethat counts whitespace-separated words.
写一个fn count_words(text: &str) -> usize,统计字符串里按空白字符分隔后的单词数量。 - Write a function
fn longest_word(text: &str) -> &strthat returns the longest word. Think about why the return type should be&strrather thanString.
再写一个fn longest_word(text: &str) -> &str,返回最长的单词。顺手想一想:为什么这里返回&str更合适,而不是String。
Solution 参考答案
fn count_words(text: &str) -> usize {
text.split_whitespace().count()
}
fn longest_word(text: &str) -> &str {
text.split_whitespace()
.max_by_key(|word| word.len())
.unwrap_or("")
}
fn main() {
let text = "the quick brown fox jumps over the lazy dog";
println!("Word count: {}", count_words(text)); // 9
println!("Longest word: {}", longest_word(text)); // "jumps"
}
Rust structs
Rust 的结构体
- The
structkeyword declares a user-defined structure type.struct关键字用来声明自定义结构体类型。- A struct can have named fields, or it can be a tuple struct with unnamed fields.
结构体既可以是带字段名的普通结构体,也可以是没有字段名的 tuple struct。
- A struct can have named fields, or it can be a tuple struct with unnamed fields.
- Unlike C++, Rust has no concept of data inheritance.
Rust 这里没有 C++ 那种“数据继承”概念,结构体之间不会靠继承来复用字段。
fn main() {
struct MyStruct {
num: u32,
is_secret_of_life: bool,
}
let x = MyStruct {
num: 42,
is_secret_of_life: true,
};
let y = MyStruct {
num: x.num,
is_secret_of_life: x.is_secret_of_life,
};
let z = MyStruct { num: x.num, ..x }; // The .. means copy remaining
println!("{} {} {}", x.num, y.is_secret_of_life, z.num);
}
Rust tuple structs
Rust 的元组结构体
- Tuple structs are similar to tuples except they define a distinct type.
tuple struct 看起来像元组,但它本身会形成一个新的独立类型。- Individual fields are still accessed as
.0,.1,.2, and so on. A common use is wrapping primitive types to prevent mixing semantically different values that happen to share the same underlying representation.
字段访问方式还是.0、.1这种形式。它最常见的用途之一,就是把同一种原始类型包成不同语义的新类型,防止用错地方。
- Individual fields are still accessed as
struct WeightInGrams(u32);
struct WeightInMilligrams(u32);
fn to_weight_in_grams(kilograms: u32) -> WeightInGrams {
WeightInGrams(kilograms * 1000)
}
fn to_weight_in_milligrams(w : WeightInGrams) -> WeightInMilligrams {
WeightInMilligrams(w.0 * 1000)
}
fn main() {
let x = to_weight_in_grams(42);
let y = to_weight_in_milligrams(x);
// let z : WeightInGrams = x; // Won't compile: x was moved into to_weight_in_milligrams()
// let a : WeightInGrams = y; // Won't compile: type mismatch (WeightInMilligrams vs WeightInGrams)
}
Note: The #[derive(...)] attribute automatically generates common trait implementations for structs and enums. You will see this repeatedly throughout the course.
说明: #[derive(...)] 属性可以自动为结构体和枚举生成常见 trait 实现。后面整本书里都会频繁看到它。
#[derive(Debug, Clone, PartialEq)]
struct Point { x: i32, y: i32 }
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // Debug: works because of #[derive(Debug)]
let p2 = p.clone(); // Clone: works because of #[derive(Clone)]
assert_eq!(p, p2); // PartialEq: works because of #[derive(PartialEq)]
}
The trait system will be covered in detail later, but #[derive(Debug)] is useful so often that it is worth adding to almost every struct and enum you create.
trait 系统后面会专门讲,但 #[derive(Debug)] 实在太常用了,基本新建一个结构体或枚举都可以先把它带上。
Rust Vec type
Rust 的 Vec 类型
Vec<T>is a dynamically sized heap buffer. It is comparable to manually managedmalloc/reallocarrays in C or to C++std::vector.Vec<T>是动态大小的堆缓冲区,大致相当于 C 里自己管扩容的堆数组,或者 C++ 的std::vector。- Unlike fixed-size arrays,
Veccan grow and shrink at runtime.
和固定大小数组不同,Vec在运行时可以扩容和缩容。 Vecowns its contents and automatically manages allocation and deallocation.Vec拥有里面的数据,也会自动处理内存分配和释放。
- Unlike fixed-size arrays,
- Common operations include
push()、pop()、insert()、remove()、len()andcapacity().
常见操作有push()、pop()、insert()、remove()、len()和capacity()。
fn main() {
let mut v = Vec::new(); // Empty vector, type inferred from usage
v.push(42); // Add element to end - Vec<i32>
v.push(43);
// Safe iteration (preferred)
for x in &v { // Borrow elements, don't consume vector
println!("{x}");
}
// Initialization shortcuts
let mut v2 = vec![1, 2, 3, 4, 5]; // Macro for initialization
let v3 = vec![0; 10]; // 10 zeros
// Safe access methods (preferred over indexing)
match v2.get(0) {
Some(first) => println!("First: {first}"),
None => println!("Empty vector"),
}
// Useful methods
println!("Length: {}, Capacity: {}", v2.len(), v2.capacity());
if let Some(last) = v2.pop() { // Remove and return last element
println!("Popped: {last}");
}
// Dangerous: direct indexing (can panic!)
// println!("{}", v2[100]); // Would panic at runtime
}
Production patterns: See Avoiding unchecked indexing for safe
.get()patterns from production Rust code.
生产代码里的安全写法: 可以对照 Avoiding unchecked indexing,那一节专门讲.get()这种更稳妥的访问方式。
Rust HashMap type
Rust 的 HashMap 类型
HashMapimplements generic key-value lookups, also known as dictionaries or maps.HashMap用来做通用的键值查找,也就是常说的字典或映射表。
fn main() {
use std::collections::HashMap; // Need explicit import, unlike Vec
let mut map = HashMap::new(); // Allocate an empty HashMap
map.insert(40, false); // Type is inferred as int -> bool
map.insert(41, false);
map.insert(42, true);
for (key, value) in map {
println!("{key} {value}");
}
let map = HashMap::from([(40, false), (41, false), (42, true)]);
if let Some(x) = map.get(&43) {
println!("43 was mapped to {x:?}");
} else {
println!("No mapping was found for 43");
}
let x = map.get(&43).or(Some(&false)); // Default value if key isn't found
println!("{x:?}");
}
Exercise: Vec and HashMap
练习:Vec 与 HashMap
🟢 Starter
🟢 基础练习
- Create a
HashMap<u32, bool>with several entries, making sure some values aretrueand others arefalse. Loop over the hashmap and place the keys into oneVecand the values into another.
创建一个HashMap<u32, bool>,里面放几组数据,注意有些值是true,有些是false。遍历这个 hashmap,把所有 key 放进一个Vec,把所有 value 放进另一个Vec。
Solution 参考答案
use std::collections::HashMap;
fn main() {
let map = HashMap::from([(1, true), (2, false), (3, true), (4, false)]);
let mut keys = Vec::new();
let mut values = Vec::new();
for (k, v) in &map {
keys.push(*k);
values.push(*v);
}
println!("Keys: {keys:?}");
println!("Values: {values:?}");
// Alternative: use iterators with unzip()
let (keys2, values2): (Vec<u32>, Vec<bool>) = map.into_iter().unzip();
println!("Keys (unzip): {keys2:?}");
println!("Values (unzip): {values2:?}");
}
Deep Dive: C++ references vs Rust references
深入对比:C++ 引用与 Rust 引用
For C++ developers: C++ programmers often assume Rust
&Tbehaves like C++T&. They look similar on the surface, but the semantics are very different. C developers can skip this section because Rust references are covered again in Ownership and Borrowing.
给 C++ 开发者: 很多人第一眼会把 Rust 的&T想成 C++ 的T&。表面上看确实像,但语义差别相当大。纯 C 开发者可以先跳过这里,Rust 引用的核心规则会在 Ownership and Borrowing 再讲一遍。
1. No rvalue references or universal references
1. 没有右值引用,也没有万能引用
In C++, && means different things depending on the context.
在 C++ 里,&& 这玩意儿看上下文能变出不同含义,这事本身就挺折腾人。
// C++: && means different things:
int&& rref = 42; // Rvalue reference — binds to temporaries
void process(Widget&& w); // Rvalue reference — caller must std::move
// Universal (forwarding) reference — deduced template context:
template<typename T>
void forward(T&& arg) { // NOT an rvalue ref! Deduced as T& or T&&
inner(std::forward<T>(arg)); // Perfect forwarding
}
In Rust, none of this exists. && is simply the logical AND operator.
Rust 里压根没有这套。 && 就只是逻辑与,别脑补更多戏份。
#![allow(unused)]
fn main() {
// Rust: && is just boolean AND
let a = true && false; // false
// Rust has NO rvalue references, no universal references, no perfect forwarding.
// Instead:
// - Move is the default for non-Copy types (no std::move needed)
// - Generics + trait bounds replace universal references
// - No temporary-binding distinction — values are values
fn process(w: Widget) { } // Takes ownership (like C++ value param + implicit move)
fn process_ref(w: &Widget) { } // Borrows immutably (like C++ const T&)
fn process_mut(w: &mut Widget) { } // Borrows mutably (like C++ T&, but exclusive)
}
| C++ Concept | Rust Equivalent | Notes |
|---|---|---|
T& lvalue reference | &T or &mut T | Rust 拆成共享借用和独占借用两类 语义比 C++ 更细 |
T&& rvalue reference | T by value | Take ownership directly 按值拿走就是所有权转移 |
| Universal reference | impl Trait or generic bounds | Generics replace forwarding tricks 靠泛型约束表达能力 |
std::move(x) | Usually just x | Move is the default 默认就是 move |
std::forward<T>(x) | No direct equivalent | Rust does not need that machinery 没有万能引用,也就没有这套转发戏法 |
2. Moves are bitwise — no move constructors
2. move 是按位移动,不存在 move 构造函数
In C++, moving is user-defined via move constructors and move assignment. In Rust, a move is fundamentally a bitwise copy of the bytes followed by invalidating the source binding.
C++ 的 move 是用户可定义行为;Rust 的 move 则更底层,就是把值的字节搬过去,再把原绑定判定为失效。
#![allow(unused)]
fn main() {
// Rust move = memcpy the bytes, mark source as invalid
let s1 = String::from("hello");
let s2 = s1; // Bytes of s1 are copied to s2's stack slot
// s1 is now invalid — compiler enforces this
// println!("{s1}"); // ❌ Compile error: value used after move
}
// C++ move = call the move constructor (user-defined!)
std::string s1 = "hello";
std::string s2 = std::move(s1); // Calls string's move ctor
// s1 is now a "valid but unspecified state" zombie
std::cout << s1; // Compiles! Prints... something (empty string, usually)
Consequences:
直接后果:
- Rust has no Rule of Five ceremony.
Rust 不需要一整套 Rule of Five 样板。 - There is no moved-from zombie state; the compiler just forbids access.
不存在“被 move 之后还能勉强访问但状态未定义”的僵尸对象。 - Moves do not raise
noexceptstyle questions; bitwise relocation itself does not throw.
也没有 C++ 里那种 move 到底会不会抛异常的包袱。
3. Auto-deref: the compiler sees through layers of indirection
3. 自动解引用:编译器会顺着一层层包装往里看
Rust can automatically dereference through pointer-like wrappers using the Deref trait. C++ 没有完全同等的语言级体验。
这也是为什么很多嵌套包装类型在 Rust 里看起来没那么吓人。
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
// Nested wrapping: Arc<Mutex<Vec<String>>>
let data = Arc::new(Mutex::new(vec!["hello".to_string()]));
// In C++, you'd need explicit unlocking and manual dereferencing at each layer.
// In Rust, the compiler auto-derefs through Arc → Mutex → MutexGuard → Vec:
let guard = data.lock().unwrap(); // Arc auto-derefs to Mutex
let first: &str = &guard[0]; // MutexGuard→Vec (Deref), Vec[0] (Index),
// &String→&str (Deref coercion)
println!("First: {first}");
// Method calls also auto-deref:
let boxed_string = Box::new(String::from("hello"));
println!("Length: {}", boxed_string.len()); // Box→String, then String::len()
// No need for (*boxed_string).len() or boxed_string->len()
}
Deref coercion also applies to function arguments.
函数参数匹配时,编译器也会自动做这类解引用转换。
fn greet(name: &str) {
println!("Hello, {name}");
}
fn main() {
let owned = String::from("Alice");
let boxed = Box::new(String::from("Bob"));
let arced = std::sync::Arc::new(String::from("Carol"));
greet(&owned); // &String → &str (1 deref coercion)
greet(&boxed); // &Box<String> → &String → &str (2 deref coercions)
greet(&arced); // &Arc<String> → &String → &str (2 deref coercions)
greet("Dave"); // &str already — no coercion needed
}
// In C++ you'd need .c_str() or explicit conversions for each case.
The deref chain: when Rust sees x.method(), it first tries the receiver as-is, then &T and &mut T, and if that still does not fit it follows Deref implementations one layer at a time. Function argument coercion is related, but it is a separate mechanism.
自动解引用链的核心逻辑: 调方法时,编译器会先尝试原类型,再尝试借用形式,实在不行再顺着 Deref 一层层往里找。函数参数的自动转换和它相关,但不是同一个机制。
4. No null references, no implicit optional references
4. 没有空引用,也没有隐式“可空引用”
// C++: references can't be null, but pointers can, and the distinction is blurry
Widget& ref = *ptr; // If ptr is null → UB
Widget* opt = nullptr; // "optional" reference via pointer
#![allow(unused)]
fn main() {
// Rust: references are ALWAYS valid — guaranteed by the borrow checker
// No way to create a null or dangling reference in safe code
let r: &i32 = &42; // Always valid
// "Optional reference" is explicit:
let opt: Option<&Widget> = None; // Clear intent, no null pointer
if let Some(w) = opt {
w.do_something(); // Only reachable when present
}
}
Rust 这里的态度很干脆:引用就是有效的引用。想表达“可能没有”,就老老实实写 Option<&T>。
别搞那种靠约定区分“这是可空指针还是正常对象”的老把戏。
5. References cannot be reseated in C++, but Rust bindings can be rebound
5. C++ 引用不能改绑,而 Rust 变量绑定可以重新绑定
// C++: a reference is an alias — it can't be rebound
int a = 1, b = 2;
int& r = a;
r = b; // This ASSIGNS b's value to a — it does NOT rebind r!
// a is now 2, r still refers to a
#![allow(unused)]
fn main() {
// Rust: let bindings can shadow, but references follow different rules
let a = 1;
let b = 2;
let r = &a;
// r = &b; // ❌ Cannot assign to immutable variable
let r = &b; // ✅ But you can SHADOW r with a new binding
// The old binding is gone, not reseated
// With mut:
let mut r = &a;
r = &b; // ✅ r now points to b — this IS rebinding (not assignment through)
}
Mental model: In C++, a reference is a permanent alias for one object. In Rust, a reference is still a normal value governed by binding rules. If the binding is mutable, it can be rebound to refer elsewhere; if the binding is immutable, it cannot.
心智模型: C++ 的引用更像“永久别名”;Rust 的引用则更像“带额外安全保证的普通值”。它遵守变量绑定规则,本身不是那种永远锁死的别名语义。