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

变量与可变性

你将学到: Rust 的变量声明与可变性模型 vs C# 的 var/const,原始类型映射,至关重要的 String 与 &str 区别,类型推断,以及 Rust 与 C# 在类型转换(casting/conversions)处理上的不同。

难度: 🟢 初级

C# 变量声明

// C# - 变量默认是可变的
int count = 0;           // 可变
count = 5;               // ✅ 正常运行

// readonly 字段 (仅限于类级别,不适用于局部变量)
// readonly int maxSize = 100;  // 初始化后不可变

const int BUFFER_SIZE = 1024; // 编译时常量 (可作为局部变量或字段)

Rust 变量声明

#![allow(unused)]
fn main() {
// Rust - 变量默认是不可变的 (immutable)
let count = 0;           // 默认不可变
// count = 5;            // ❌ 编译错误:无法对不可变变量进行二次赋值

let mut count = 0;       // 显式声明为可变 (mutable)
count = 5;               // ✅ 正常运行

const BUFFER_SIZE: usize = 1024; // 编译时常量
}

C# 开发者的关键思维转变

#![allow(unused)]
fn main() {
// 可以将 'let' 视为将 C# 的 readonly 字段语义应用到了所有变量上
let name = "John";       // 类似于 readonly 字段:一旦设置,不可更改
let mut age = 30;        // 类似于:int age = 30;

// 变量遮蔽 (Variable shadowing,Rust 特有)
let spaces = "   ";      // 类型为 String (或 &str)
let spaces = spaces.len(); // 现在它是一个数字 (usize)
// 这与“修改 (mutation)”不同 - 我们是在创建一个全新的变量并重用旧名称
}

实战示例:计数器

// C# 版本
public class Counter
{
    private int value = 0;
    
    public void Increment()
    {
        value++;  // 修改
    }
    
    public int GetValue() => value;
}
#![allow(unused)]
fn main() {
// Rust 版本
pub struct Counter {
    value: i32,  // 默认私有
}

impl Counter {
    pub fn new() -> Counter {
        Counter { value: 0 }
    }
    
    pub fn increment(&mut self) {  // 修改数据需要使用 &mut self
        self.value += 1;
    }
    
    pub fn get_value(&self) -> i32 {
        self.value
    }
}
}

数据类型对比

原始类型 (Primitive Types)

C# 类型Rust 类型大小范围/说明
byteu88 bits0 到 255
sbytei88 bits-128 到 127
shorti1616 bits-32,768 到 32,767
ushortu1616 bits0 到 65,535
inti3232 bits-2³¹ 到 2³¹-1
uintu3232 bits0 到 2³²-1
longi6464 bits-2⁶³ 到 2⁶³-1
ulongu6464 bits0 到 2⁶⁴-1
floatf3232 bitsIEEE 754
doublef6464 bitsIEEE 754
boolbool1 bittrue/false
charchar32 bitsUnicode 标量值 (Scalar)

大小相关类型 (极其重要!)

// C# - int 始终是 32 位
int arrayIndex = 0;
long fileSize = file.Length;
#![allow(unused)]
fn main() {
// Rust - 指定大小类型会匹配指针大小 (32 位或 64 位系统对应不同大小)
let array_index: usize = 0;    // 类似于 C 语言中的 size_t,用于索引
let file_size: u64 = file.len(); // 显式的 64 位
}

类型推断

// C# - var 关键字
var name = "John";        // 类型为 string
var count = 42;           // 类型为 int
var price = 29.99;        // 类型为 double
#![allow(unused)]
fn main() {
// Rust - 自动类型推断
let name = "John";        // 类型为 &str (字符串切片)
let count = 42;           // 类型为 i32 (默认整数类型)
let price = 29.99;        // 类型为 f64 (默认浮点数类型)

// 显式类型注解 (Explicit type annotations)
let count: u32 = 42;
let price: f32 = 29.99;
}

数组与集合概览

// C# - 引用类型,在堆 (heap) 上分配
int[] numbers = new int[5];        // 固定大小
List<int> list = new List<int>();  // 动态大小
#![allow(unused)]
fn main() {
// Rust - 多种选项
let numbers: [i32; 5] = [1, 2, 3, 4, 5];  // 栈 (stack) 数组,固定大小
let mut list: Vec<i32> = Vec::new();       // 堆上的向量 (vector),动态大小
}

字符串类型:String vs &str

这是令 C# 开发者最感困惑的概念之一,让我们通过对比来透彻理解。

C# 字符串处理

// C# - 简单的字符串模型
string name = "John";           // 字符串字面量
string greeting = "Hello, " + name;  // 字符串拼接
string upper = name.ToUpper();  // 方法调用

Rust 字符串类型

#![allow(unused)]
fn main() {
// Rust - 有两种主要的字符串类型

// 1. &str (字符串切片) - 类似于 C# 中的 ReadOnlySpan<char>
let name: &str = "John";        // 字符串字面量 (不可变,借用)

// 2. String - 类似于 StringBuilder 或可变字符串
let mut greeting = String::new();       // 创建空字符串
greeting.push_str("Hello, ");          // 追加内容
greeting.push_str(name);               // 追加内容

// 或者直接创建
let greeting = String::from("Hello, John");
let greeting = "Hello, John".to_string();  // 将 &str 转换为 String
}

应该使用哪一种?

场景使用类型C# 对应概念
字符串字面量&strstring 字面量
函数参数 (只读)&strstring 或 ReadOnlySpan<char>
拥有所有权的、可变的字符串StringStringBuilder
返回拥有所有权的字符串Stringstring

实战案例

// 该函数可以接受任何字符串类型
fn greet(name: &str) {  // 同时接受 String 和 &str
    println!("Hello, {}!", name);
}

fn main() {
    let literal = "John";                    // &str
    let owned = String::from("Jane");        // String
    
    greet(literal);                          // 正常运行
    greet(&owned);                           // 正常运行 (将 String 借用为 &str)
    greet("Bob");                            // 正常运行
}

// 返回拥有所有权的字符串的函数
fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)  // format! 宏返回一个 String
}

C# 开发者的理解思路

#![allow(unused)]
fn main() {
// &str 就像 ReadOnlySpan<char> —— 它是对字符串数据的“视图”
// String 就像一个你拥有所有权且可以修改的 char[]

let borrowed: &str = "我不拥有这段数据";
let owned: String = String::from("我拥有这段数据");

// 在两者之间转换
let owned_copy: String = borrowed.to_string();  // 复制并转为拥有者模式
let borrowed_view: &str = &owned;               // 从 String 借用一个视图
}

打印与字符串格式化

C# 开发者高度依赖 Console.WriteLine 和字符串内插 ($"")。Rust 的格式化系统同样强大,但使用的是宏和格式说明符。

基础输出

// C# 输出
Console.Write("无换行");
Console.WriteLine("带换行");
Console.Error.WriteLine("输出到 stderr");

// 字符串内插 (C# 6+)
string name = "Alice";
int age = 30;
Console.WriteLine($"{name} is {age} years old");
#![allow(unused)]
fn main() {
// Rust 输出 — 全部是宏 (注意末尾带有 !)
print!("无换行");                   // → 输出到 stdout,无换行
println!("带换行");                 // → 输出到 stdout 并带有换行
eprint!("输出到 stderr");           // → 输出到 stderr,无换行  
eprintln!("输出到 stderr 并换行");   // → 输出到 stderr 并带有换行

// 字符串格式化 (类似于 $"" 内插)
let name = "Alice";
let age = 30;
println!("{name} is {age} years old");     // 行内变量捕获 (Rust 1.58+)
println!("{} is {} years old", name, age); // 位置参数
}

格式说明符 (Format Specifiers)

// C# 格式说明符
Console.WriteLine($"{price:F2}");         // 固定小数:29.99
Console.WriteLine($"{count:D5}");         // 填充整数:00042
Console.WriteLine($"{value,10}");         // 右对齐,宽度 10
Console.WriteLine($"{value,-10}");        // 左对齐,宽度 10
Console.WriteLine($"{hex:X}");            // 十六进制:FF
Console.WriteLine($"{ratio:P1}");         // 百分比:85.0%
#![allow(unused)]
fn main() {
// Rust 格式说明符
println!("{price:.2}");          // 2 位小数:29.99
println!("{count:05}");          // 零填充,宽度 5:00042
println!("{value:>10}");         // 右对齐,宽度 10
println!("{value:<10}");         // 左对齐,宽度 10
println!("{value:^10}");         // 居中对齐,宽度 10
println!("{hex:#X}");            // 带有前缀的十六进制:0xFF
println!("{hex:08X}");           // 十六进制零填充:000000FF
println!("{bits:#010b}");        // 带有前缀的二进制:0b00001010
println!("{big}", big = 1_000_000); // 命名参数
}

Debug vs Display 打印

#![allow(unused)]
fn main() {
// {:?}  — Debug 特性 (面向开发者,可自动派生)
// {:#?} — “漂亮”打印模式的 Debug (有缩进,多行)
// {}    — Display 特性 (面向终端用户,必须手动实现)

#[derive(Debug)] // 自动生成 Debug 输出支持
struct Point { x: f64, y: f64 }

let p = Point { x: 1.5, y: 2.7 };

println!("{:?}", p);   // Point { x: 1.5, y: 2.7 }   — 紧凑调试信息
println!("{:#?}", p);  // Point {                     — 易读调试信息
                        //     x: 1.5,
                        //     y: 2.7,
                        // }
// println!("{}", p);  // ❌ ERROR: Point 未实现 Display
}
// C# 对应概念:
// {:?}  ≈ object.GetType().ToString() 或反射转储 (Reflection dump)
// {}    ≈ object.ToString()
// 在 C# 中你重写 ToString(); 在 Rust 中你实现 Display

快速参考

C# 功能项Rust 对应项输出效果
Console.WriteLine(x)println!("{x}")Display 格式化
$"{x}" (内插)format!("{x}")返回一个 String
x.ToString()x.to_string()要求实现 Display 特性
重写 ToString()impl Display面向用户的输出
调试视图{:?} 或 dbg!(x)开发者视角输出
String.Format("{0:F2}", x)format!("{x:.2}")格式化后的 String

类型转换与强制转换

C# 拥有隐式转换、显式强制转换 (int)x 以及 Convert.To*()。Rust 则更加严格 —— 不允许任何隐式的数值转换。

数值转换

// C# — 隐式和显式转换
int small = 42;
long big = small;              // 隐式加宽:OK
double d = small;              // 隐式加宽:OK
int truncated = (int)3.14;     // 显式缩窄:3
byte b = (byte)300;            // 默不作声的溢出:44
#![allow(unused)]
fn main() {
// Rust — 所有数值转换必须是显式的
let small: i32 = 42;
let big: i64 = small as i64;       // 加宽:使用 'as' 显式转换
let d: f64 = small as f64;         // 整数转浮点数:显式
let truncated: i32 = 3.14_f64 as i32; // 缩窄:3 (直接截断)
let b: u8 = 300_u16 as u8;        // 溢出:回绕至 44 (类似于 C# 的 unchecked)

// 使用 TryFrom 进行安全转换
use std::convert::TryFrom;
let safe: Result<u8, _> = u8::try_from(300_u16); // Err — 超出范围
let ok: Result<u8, _>   = u8::try_from(42_u16);  // Ok(42)

// 字符串解析 — 返回 Result,而不是 bool + out 参数
let parsed: Result<i32, _> = "42".parse::<i32>();   // Ok(42)
let bad: Result<i32, _>    = "abc".parse::<i32>();  // Err(ParseIntError)
}

字符串转换

// C#
int n = 42;
string s = n.ToString();          // "42"
int back = int.Parse(s);          // 42 或抛出异常
#![allow(unused)]
fn main() {
// Rust — 通过 Display 实现 to_string(),通过 FromStr 实现 parse()
let n: i32 = 42;
let s: String = n.to_string();            // "42" (使用 Display 特性)
let back: i32 = s.parse().unwrap();       // 42 或 panic

// &str ↔ String 转换 (这是 Rust 中最常见的转换)
let owned: String = "hello".to_string();    // &str → String
let owned2: String = String::from("hello"); // &str → String (等效)
let borrowed: &str = &owned;                // String → &str (零开销借用)
}

引用转换 (不支持继承转型!)

// C# — 向上转型 (Upcasting) 和向下转型 (Downcasting)
Animal a = new Dog();              // 向上转型 (隐式)
Dog d = (Dog)a;                    // 向下转型 (显式,可能报错)
if (a is Dog dog) { /* ... */ }    // 安全的向下转型
#![allow(unused)]
fn main() {
// Rust — 没有继承关系,因此没有向上/向下转型
// 请使用 Trait 对象实现多态:
let animal: Box<dyn Animal> = Box::new(Dog);

// 实践中,通常使用枚举 (Enums) 而非向下转型:
enum Animal {
    Dog(Dog),
    Cat(Cat),
}
match animal {
    Animal::Dog(d) => { /* 使用 d */ }
    Animal::Cat(c) => { /* 使用 c */ }
}
}

注释与文档

普通注释

// C# 注释
// 单行注释
/* 多行
   注释 */

/// <summary>
/// XML 文档注释
/// </summary>
public string Greet(string name) { ... }
#![allow(unused)]
fn main() {
// Rust 注释
// 单行注释
/* 多行
   注释 */

/// 文档注释 (类似于 C# 的 ///)
/// 这里的文档注释支持 Markdown 格式。
/// 
/// # 参数
/// 
/// * `name` - 用户名称的字符串切片
/// 
/// # 示例
/// 
/// ```
/// let greeting = greet("Alice");
/// assert_eq!(greeting, "Hello, Alice!");
/// ```
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
}

生成文档

# 生成文档 (类似于 C# 中的 XML 文档生成)
cargo doc --open

# 运行文档中的代码示例测试
cargo test --doc

练习

🏋️ 练习:类型安全的温度转换 (点击展开)

创建一个 Rust 程序,完成以下任务:

  1. 为摄氏度的绝对零度 (-273.15) 声明一个 const。
  2. 为已执行转换的次数声明一个 static 计数器(使用 AtomicU32)。
  3. 编写一个函数 celsius_to_fahrenheit(c: f64) -> f64,如果温度低于绝对零度,则返回 f64::NAN(表示拒绝该输入)。
  4. 通过遮蔽(shadowing)演示以下过程:将字符串 "98.6" 解析为 f64 类型,然后再进行转换。
🔑 参考答案
use std::sync::atomic::{AtomicU32, Ordering};

const ABSOLUTE_ZERO_C: f64 = -273.15;
static CONVERSION_COUNT: AtomicU32 = AtomicU32::new(0);

fn celsius_to_fahrenheit(c: f64) -> f64 {
    if c < ABSOLUTE_ZERO_C {
        return f64::NAN;
    }
    CONVERSION_COUNT.fetch_add(1, Ordering::Relaxed);
    c * 9.0 / 5.0 + 32.0
}

fn main() {
    let temp = "98.6";           // 类型为 &str
    let temp: f64 = temp.parse().unwrap(); // 遮蔽为 f64 类型
    let temp = celsius_to_fahrenheit(temp); // 遮蔽为华氏度结果
    println!("{temp:.1}°F");
    println!("转换次数: {}", CONVERSION_COUNT.load(Ordering::Relaxed));
}