Tuples and Destructuring
元组与解构
What you’ll learn: Rust tuples compared with C#
ValueTuple, arrays and slices, structs versus classes, the newtype pattern for zero-cost domain safety, and destructuring syntax.
本章将学到什么: 对照理解 Rust 元组和 C#ValueTuple,理解数组、切片、struct 与 class 的差异,掌握 newtype 模式怎样以零成本提供领域类型安全,并熟悉解构语法。Difficulty: 🟢 Beginner
难度: 🟢 入门
C# has ValueTuple, and Rust tuples feel familiar at first glance, but Rust pushes tuples and destructuring much deeper into the language.
C# 从 ValueTuple 开始,已经让元组变得比较顺手;Rust 则把元组和解构进一步揉进了语言核心,所以后面会反复遇到它们。
C# Tuples
C# 元组
// C# ValueTuple (C# 7+)
var point = (10, 20); // (int, int)
var named = (X: 10, Y: 20); // Named elements
Console.WriteLine($"{named.X}, {named.Y}");
// Tuple as return type
public (int Quotient, int Remainder) Divide(int a, int b)
{
return (a / b, a % b);
}
var (q, r) = Divide(10, 3); // Deconstruction
Console.WriteLine($"{q} remainder {r}");
// Discards
var (_, remainder) = Divide(10, 3); // Ignore quotient
Rust Tuples
Rust 元组
#![allow(unused)]
fn main() {
// Rust tuples — immutable by default, no named elements
let point = (10, 20); // (i32, i32)
let point3d: (f64, f64, f64) = (1.0, 2.0, 3.0);
// Access by index (0-based)
println!("x={}, y={}", point.0, point.1);
// Tuple as return type
fn divide(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (q, r) = divide(10, 3); // Destructuring
println!("{q} remainder {r}");
// Discards with _
let (_, remainder) = divide(10, 3);
// Unit type () — the "empty tuple" (like C# void)
fn greet() { // implicit return type is ()
println!("hi");
}
}
Rust 元组最大的区别,就是它没给字段名留位置。
所以只要元组开始承载“有语义的业务字段”,就该警觉是不是该换成 struct 了。元组适合临时组合,struct 适合长期表达。
Key Differences
关键差异
| Feature 特性 | C# ValueTuple | Rust Tuple |
|---|---|---|
| Named elements 命名字段 | (int X, int Y)支持命名元素 | Not supported, use structs 不支持命名,语义化需求请上 struct |
| Max arity 最大元数 | Around 8 before nesting 大约 8 个后要靠嵌套 | No practical language limit for common use 语言层面更宽松,但一般别写太长 |
| Comparisons 比较 | Automatic 自动支持 | Automatic for common tuple sizes 常见长度自动支持 |
| Used as dict key 用作字典键 | Yes 可以 | Yes, if elements implement Hash可以,但元素要能 Hash |
| Return from functions 函数返回值 | Common 常见 | Common 也很常见 |
| Mutable elements 元素可变性 | Depends on surrounding variable usage 取决于变量本身 | Entire binding is immutable unless mut默认不可变,除非显式 mut |
Tuple Structs (Newtypes)
元组结构体与 Newtype
#![allow(unused)]
fn main() {
// When a plain tuple isn't descriptive enough, use a tuple struct:
struct Meters(f64); // Single-field "newtype" wrapper
struct Celsius(f64);
struct Fahrenheit(f64);
// The compiler treats these as DIFFERENT types:
let distance = Meters(100.0);
let temp = Celsius(36.6);
// distance == temp; // ❌ ERROR: can't compare Meters with Celsius
// Newtype pattern prevents unit-confusion bugs at compile time!
// In C# you'd need a full class/struct for the same safety.
}
// C# equivalent requires more ceremony:
public readonly record struct Meters(double Value);
public readonly record struct Celsius(double Value);
// Not interchangeable, but records add overhead vs Rust's zero-cost newtypes
newtype 是 Rust 做领域建模时非常锋利的一把刀。
它看起来只是“外面套一层壳”,实际上是在编译期把不同语义强行分开,避免把米、摄氏度、用户 ID、端口号这类东西混用。
The Newtype Pattern in Depth: Domain Modeling with Zero Cost
进一步理解 Newtype:零成本的领域建模
Newtypes do much more than prevent unit confusion. They are one of Rust’s primary tools for encoding business rules into types instead of repeating runtime validation everywhere.
Newtype 不只是防止单位混用,它更大的价值在于:把业务规则直接编码进类型里,而不是把校验逻辑散落到每一个调用点。
C# Validation Approach: Runtime Guards
C# 常见做法:运行时 Guard
// C# — validation happens at runtime, every time
public class UserService
{
public User CreateUser(string email, int age)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
throw new ArgumentException("Invalid email");
if (age < 0 || age > 150)
throw new ArgumentException("Invalid age");
return new User { Email = email, Age = age };
}
public void SendEmail(string email)
{
// Must re-validate — or trust the caller?
if (!email.Contains('@')) throw new ArgumentException("Invalid email");
// ...
}
}
Rust Newtype Approach: Compile-Time Proof
Rust 的 Newtype 做法:编译期证明
#![allow(unused)]
fn main() {
/// A validated email address — the type itself IS the proof of validity.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String);
impl Email {
/// The ONLY way to create an Email — validation happens once at construction.
pub fn new(raw: &str) -> Result<Self, &'static str> {
if raw.contains('@') && raw.len() > 3 {
Ok(Email(raw.to_lowercase()))
} else {
Err("invalid email format")
}
}
/// Safe access to the inner value
pub fn as_str(&self) -> &str { &self.0 }
}
/// A validated age — impossible to create an invalid one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Age(u8);
impl Age {
pub fn new(raw: u8) -> Result<Self, &'static str> {
if raw <= 150 { Ok(Age(raw)) } else { Err("age out of range") }
}
pub fn value(&self) -> u8 { self.0 }
}
// Now functions take PROVEN types — no re-validation needed!
fn create_user(email: Email, age: Age) -> User {
// email is GUARANTEED valid — it's a type invariant
User { email, age }
}
fn send_email(to: &Email) {
// No validation needed — Email type proves validity
println!("Sending to: {}", to.as_str());
}
}
Common Newtype Uses for C# Developers
C# 开发者常见的 Newtype 用法
| C# Pattern C# 里的常见问题 | Rust Newtype 对应的 Rust Newtype | What It Prevents 能防住什么 |
|---|---|---|
string for UserId, Email, etc.各种 ID 和邮箱全用 string | struct UserId(Uuid)struct UserId(Uuid) | Passing the wrong string to the wrong parameter 把错误字符串传给错误参数 |
int for Port, Count, Index端口、数量、索引都混用 int | struct Port(u16)struct Port(u16) | Prevents semantically unrelated ints from混用 避免语义不同的整数混在一起 |
| Guard clauses everywhere 到处写 guard 校验 | Constructor validation once 构造时统一校验一次 | Re-validation and missed validation 重复校验或漏校验 |
decimal for USD, EUR货币值都用同一个 decimal | struct Usd(Decimal)struct Usd(Decimal) | Stops currency mix-ups 防止美元欧元随手相加 |
TimeSpan for different semantics各种超时全用 TimeSpan | struct Timeout(Duration)struct Timeout(Duration) | Avoids mixing connection and request timeouts 避免连接超时和请求超时混用 |
#![allow(unused)]
fn main() {
// Zero-cost: newtypes compile to the same assembly as the inner type.
// This Rust code:
struct UserId(u64);
fn lookup(id: UserId) -> Option<User> { /* ... */ }
// Generates the SAME machine code as:
fn lookup(id: u64) -> Option<User> { /* ... */ }
// But with full type safety at compile time!
}
这就是 Rust 喜欢把正确性做进类型里的味道。
不是靠“记得调用 Validate()”,而是靠“没有合法类型就根本过不了接口”。这两者的可靠度差得不是一点半点。
Arrays and Slices
数组与切片
Understanding arrays, slices, and vectors separately is crucial. They solve different problems and Rust keeps the distinctions explicit.
数组、切片、向量这三样东西一定要拆开理解。它们解决的是不同层次的问题,而 Rust 正是故意把这种区别写得很明白。
C# Arrays
C# 数组
// C# arrays
int[] numbers = new int[5]; // Fixed size, heap allocated
int[] initialized = { 1, 2, 3, 4, 5 }; // Array literal
// Access
numbers[0] = 10;
int first = numbers[0];
// Length
int length = numbers.Length;
// Array as parameter (reference type)
void ProcessArray(int[] array)
{
array[0] = 99; // Modifies original
}
Rust Arrays, Slices, and Vectors
Rust 的数组、切片与向量
#![allow(unused)]
fn main() {
// 1. Arrays - Fixed size, stack allocated
let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Type: [i32; 5]
let zeros = [0; 10]; // 10 zeros
// Access
let first = numbers[0];
// numbers[0] = 10; // ❌ Error: arrays are immutable by default
let mut mut_array = [1, 2, 3, 4, 5];
mut_array[0] = 10; // ✅ Works with mut
// 2. Slices - Views into arrays or vectors
let slice: &[i32] = &numbers[1..4]; // Elements 1, 2, 3
let all_slice: &[i32] = &numbers; // Entire array as slice
// 3. Vectors - Dynamic size, heap allocated (covered earlier)
let mut vec = vec![1, 2, 3, 4, 5];
vec.push(6); // Can grow
}
切片 &[T] 这玩意一定要尽快熟。
它不是独立拥有数据的集合,而只是“对一段连续元素的借用视图”。很多更通用、更优雅的函数签名,最后都会落在切片上。
Slices as Function Parameters
把切片作为函数参数
// C# - Method that works with arrays
public void ProcessNumbers(int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(numbers[i]);
}
}
// Works with arrays only
ProcessNumbers(new int[] { 1, 2, 3 });
// Rust - Function that works with any sequence
fn process_numbers(numbers: &[i32]) { // Slice parameter
for (i, num) in numbers.iter().enumerate() {
println!("Index {}: {}", i, num);
}
}
fn main() {
let array = [1, 2, 3, 4, 5];
let vec = vec![1, 2, 3, 4, 5];
// Same function works with both!
process_numbers(&array); // Array as slice
process_numbers(&vec); // Vector as slice
process_numbers(&vec[1..4]); // Partial slice
}
这就是为什么 Rust 社区老爱说“参数尽量收切片”。
因为一旦签成 &[T],数组、向量、子区间都能复用;签死成 Vec<T> 反而会把接口绑窄,还平白无故多出所有权要求。
String Slices (&str) Revisited
再次回到字符串切片 &str
#![allow(unused)]
fn main() {
// String and &str relationship
fn string_slice_example() {
let owned = String::from("Hello, World!");
let slice: &str = &owned[0..5]; // "Hello"
let slice2: &str = &owned[7..]; // "World!"
println!("{}", slice); // "Hello"
println!("{}", slice2); // "World!"
// Function that accepts any string type
print_string("String literal"); // &str
print_string(&owned); // String as &str
print_string(slice); // &str slice
}
fn print_string(s: &str) {
println!("{}", s);
}
}
字符串切片本质上也是切片思想在文本上的体现。
所以前面搞懂了普通切片,再回头看 &str 就会舒服很多:它不是神秘特例,只是 UTF-8 文本上的借用视图。
Structs vs Classes
Struct 与 Class 对照
Structs in Rust fill many of the roles that classes cover in C#, but the storage model and method system differ quite a bit.
Rust 的 struct 能覆盖 C# class 很多职责,但它们在存储方式、所有权和方法组织上差异不小,别简单拿“Rust 的 struct 就是 C# 的 class”去硬套。
graph TD
subgraph "C# Class (Heap)"
CObj["Object Header<br/>+ vtable ptr"] --> CFields["Name: string ref<br/>Age: int<br/>Hobbies: List ref"]
CFields --> CHeap1["#quot;Alice#quot; on heap"]
CFields --> CHeap2["List<string> on heap"]
end
subgraph "Rust Struct (Stack)"
RFields["name: String<br/>ptr | len | cap<br/>age: i32<br/>hobbies: Vec<br/>ptr | len | cap"]
RFields --> RHeap1["#quot;Alice#quot; heap buffer"]
RFields --> RHeap2["Vec heap buffer"]
end
style CObj fill:#bbdefb,color:#000
style RFields fill:#c8e6c9,color:#000
Key insight: C# classes always live on the heap behind references. Rust structs live on the stack by default, while dynamically-sized internals such as
Stringbuffers orVeccontents live on the heap.
关键理解: C# 的 class 一般总是在堆上,再通过引用访问;Rust 的 struct 默认值本体在栈上,只有像String、Vec这种内部动态数据缓冲区才放到堆里。
C# Class Definition
C# 类定义
// C# class with properties and methods
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public List<string> Hobbies { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
Hobbies = new List<string>();
}
public void AddHobby(string hobby)
{
Hobbies.Add(hobby);
}
public string GetInfo()
{
return $"{Name} is {Age} years old";
}
}
Rust Struct Definition
Rust Struct 定义
#![allow(unused)]
fn main() {
// Rust struct with associated functions and methods
#[derive(Debug)] // Automatically implement Debug trait
pub struct Person {
pub name: String, // Public field
pub age: u32, // Public field
hobbies: Vec<String>, // Private field (no pub)
}
impl Person {
// Associated function (like static method)
pub fn new(name: String, age: u32) -> Person {
Person {
name,
age,
hobbies: Vec::new(),
}
}
// Method (takes &self, &mut self, or self)
pub fn add_hobby(&mut self, hobby: String) {
self.hobbies.push(hobby);
}
// Method that borrows immutably
pub fn get_info(&self) -> String {
format!("{} is {} years old", self.name, self.age)
}
// Getter for private field
pub fn hobbies(&self) -> &Vec<String> {
&self.hobbies
}
}
}
Rust struct 没有那种“默认 public 属性 + 默认 getter/setter”的味道。
字段是否公开、方法是否允许修改内部状态,都得明确写出来。代码会更啰嗦一点,但边界也会更清楚。
Creating and Using Instances
创建并使用实例
// C# object creation and usage
var person = new Person("Alice", 30);
person.AddHobby("Reading");
person.AddHobby("Swimming");
Console.WriteLine(person.GetInfo());
Console.WriteLine($"Hobbies: {string.Join(", ", person.Hobbies)}");
// Modify properties directly
person.Age = 31;
#![allow(unused)]
fn main() {
// Rust struct creation and usage
let mut person = Person::new("Alice".to_string(), 30);
person.add_hobby("Reading".to_string());
person.add_hobby("Swimming".to_string());
println!("{}", person.get_info());
println!("Hobbies: {:?}", person.hobbies());
// Modify public fields directly
person.age = 31;
// Debug print the entire struct
println!("{:?}", person);
}
Struct Initialization Patterns
Struct 初始化方式
// C# object initialization
var person = new Person("Bob", 25)
{
Hobbies = new List<string> { "Gaming", "Coding" }
};
// Anonymous types
var anonymous = new { Name = "Charlie", Age = 35 };
#![allow(unused)]
fn main() {
// Rust struct initialization
let person = Person {
name: "Bob".to_string(),
age: 25,
hobbies: vec!["Gaming".to_string(), "Coding".to_string()],
};
// Struct update syntax (like object spread)
let older_person = Person {
age: 26,
..person // Use remaining fields from person (moves person!)
};
// Tuple structs (like anonymous types)
#[derive(Debug)]
struct Point(i32, i32);
let point = Point(10, 20);
println!("Point: ({}, {})", point.0, point.1);
}
结构体更新语法 ..person 很方便,但它会 move 剩余字段。
这东西好用归好用,脑子里一定得记住:一旦源值里有非 Copy 字段,被搬走以后原变量通常就不能再用。
Methods and Associated Functions
方法与关联函数
Understanding the difference between methods and associated functions is essential because Rust uses receiver types explicitly.
方法和关联函数的区别一定要搞清楚,因为 Rust 会把“这个函数到底如何接收实例”写在签名里,不给模糊空间。
C# Method Types
C# 里的方法类型
public class Calculator
{
private int memory = 0;
// Instance method
public int Add(int a, int b)
{
return a + b;
}
// Instance method that uses state
public void StoreInMemory(int value)
{
memory = value;
}
// Static method
public static int Multiply(int a, int b)
{
return a * b;
}
// Static factory method
public static Calculator CreateWithMemory(int initialMemory)
{
var calc = new Calculator();
calc.memory = initialMemory;
return calc;
}
}
Rust Method Types
Rust 里的方法类型
#[derive(Debug)]
pub struct Calculator {
memory: i32,
}
impl Calculator {
// Associated function (like static method) - no self parameter
pub fn new() -> Calculator {
Calculator { memory: 0 }
}
// Associated function with parameters
pub fn with_memory(initial_memory: i32) -> Calculator {
Calculator { memory: initial_memory }
}
// Method that borrows immutably (&self)
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
// Method that borrows mutably (&mut self)
pub fn store_in_memory(&mut self, value: i32) {
self.memory = value;
}
// Method that takes ownership (self)
pub fn into_memory(self) -> i32 {
self.memory // Calculator is consumed
}
// Getter method
pub fn memory(&self) -> i32 {
self.memory
}
}
fn main() {
// Associated functions called with ::
let mut calc = Calculator::new();
let calc2 = Calculator::with_memory(42);
// Methods called with .
let result = calc.add(5, 3);
calc.store_in_memory(result);
println!("Memory: {}", calc.memory());
// Consuming method
let memory_value = calc.into_memory(); // calc is no longer usable
println!("Final memory: {}", memory_value);
}
Rust 把接收者写得很实在:&self、&mut self、self。
只要看见签名,就能知道这个方法是只读、可改,还是会直接把整个对象吃掉。这种清晰度后面会越来越值钱。
Method Receiver Types Explained
方法接收者类型说明
#![allow(unused)]
fn main() {
impl Person {
// &self - Immutable borrow (most common)
// Use when you only need to read the data
pub fn get_name(&self) -> &str {
&self.name
}
// &mut self - Mutable borrow
// Use when you need to modify the data
pub fn set_name(&mut self, name: String) {
self.name = name;
}
// self - Take ownership (less common)
// Use when you want to consume the struct
pub fn consume(self) -> String {
self.name // Person is moved, no longer accessible
}
}
fn method_examples() {
let mut person = Person::new("Alice".to_string(), 30);
// Immutable borrow
let name = person.get_name(); // person can still be used
println!("Name: {}", name);
// Mutable borrow
person.set_name("Alice Smith".to_string()); // person can still be used
// Taking ownership
let final_name = person.consume(); // person is no longer usable
println!("Final name: {}", final_name);
}
}
一旦把这三种 receiver 想成“看一下”“改一下”“拿走整个对象”,很多 API 设计会自然很多。
这套方式虽然比 C# 更显式,但恰恰因为显式,调用方更不容易踩坑。
Exercises
练习
🏋️ Exercise: Slice Window Average
🏋️ 练习:切片滑动平均
Challenge: Write a function that takes a slice of f64 values and a window size, then returns a Vec<f64> of rolling averages. Example: [1.0, 2.0, 3.0, 4.0, 5.0] with window 3 should become [2.0, 3.0, 4.0].
挑战: 写一个函数,接收一段 f64 切片和窗口大小,返回滑动平均值构成的 Vec<f64>。例如 [1.0, 2.0, 3.0, 4.0, 5.0] 配窗口 3,结果应为 [2.0, 3.0, 4.0]。
fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
// Your implementation here
todo!()
}
fn main() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let avgs = rolling_average(&data, 3);
println!("{avgs:?}"); // [2.0, 3.0, 4.0]
}
🔑 Solution
🔑 参考答案
fn rolling_average(data: &[f64], window: usize) -> Vec<f64> {
data.windows(window)
.map(|w| w.iter().sum::<f64>() / w.len() as f64)
.collect()
}
fn main() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let avgs = rolling_average(&data, 3);
assert_eq!(avgs, vec![2.0, 3.0, 4.0]);
println!("{avgs:?}");
}
Key takeaway: Slices already provide methods like .windows(), .chunks(), and .split(), which often replace manual index arithmetic entirely.
这一题最该记住的点: 切片本身已经有很多很好用的方法,比如 .windows()、.chunks()、.split(),很多时候根本没必要自己写一坨下标计算。
🏋️ Exercise: Mini Address Book
🏋️ 练习:迷你通讯录
Build a small address book using structs, enums, and methods:
用 struct、enum 和方法写一个小通讯录,要求如下:
- Define an enum
PhoneType { Mobile, Home, Work }.
定义枚举PhoneType { Mobile, Home, Work }。 - Define a struct
Contactwithname: Stringandphones: Vec<(PhoneType, String)>.
定义Contact结构体,包含name: String和phones: Vec<(PhoneType, String)>。 - Implement
Contact::new(name: impl Into<String>) -> Self.
实现Contact::new(name: impl Into<String>) -> Self。 - Implement
Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>).
实现Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)。 - Implement
Contact::mobile_numbers(&self) -> Vec<&str>that returns only mobile numbers.
实现Contact::mobile_numbers(&self) -> Vec<&str>,只返回手机号。 - In
main, create a contact, add two phones, and print the mobile numbers.
在main里创建联系人,添加两个号码,再打印手机号。
🔑 Solution
🔑 参考答案
#[derive(Debug, PartialEq)]
enum PhoneType { Mobile, Home, Work }
#[derive(Debug)]
struct Contact {
name: String,
phones: Vec<(PhoneType, String)>,
}
impl Contact {
fn new(name: impl Into<String>) -> Self {
Contact { name: name.into(), phones: Vec::new() }
}
fn add_phone(&mut self, kind: PhoneType, number: impl Into<String>) {
self.phones.push((kind, number.into()));
}
fn mobile_numbers(&self) -> Vec<&str> {
self.phones
.iter()
.filter(|(kind, _)| *kind == PhoneType::Mobile)
.map(|(_, num)| num.as_str())
.collect()
}
}
fn main() {
let mut alice = Contact::new("Alice");
alice.add_phone(PhoneType::Mobile, "+1-555-0100");
alice.add_phone(PhoneType::Work, "+1-555-0200");
alice.add_phone(PhoneType::Mobile, "+1-555-0101");
println!("{}'s mobile numbers: {:?}", alice.name, alice.mobile_numbers());
}