02. Tour of Rust — Rust 速览
本章快速浏览 Rust 的语法和核心概念。我们不会深入每个特性(后续章节会逐一展开),而是通过一系列可运行的示例建立整体印象。如果你是 Rust 新手,本章将帮助你理解各个语法元素的「形状」;如果你已有 Rust 经验,可以跳过本章。
创建第一个程序
cargo new hello
cd hellocargo new 会生成如下文件结构:
hello/
├── Cargo.toml # 项目元数据和依赖
└── src/
└── main.rs # 入口文件编辑 src/main.rs:
fn main() {
println!("Hello, world!");
}fn声明一个函数。main是程序的入口点(就像 C/C++ 中的main函数)。println!后面的!表示这是一个宏(macro)。Rust 宏是可以生成代码的代码。println!在编译时展开为格式化输出的代码。- 字符串字面量使用双引号。
- 大多数语句以分号结尾。
运行程序:
cargo run变量与绑定
Rust 中的变量默认是**不可变(immutable)**的:
fn main() {
let answer = 42;
// answer = 43; // 编译错误:不可变变量不能重新赋值
let mut guess = String::new();
guess = String::from("hello"); // OK:mut 允许修改
}let 不仅声明变量——它创建一个绑定(binding),将名称与值关联起来。Rust 的类型推断能力强大,大多数时候不需要显式标注类型:
let x = 42; // 推断为 i32
let y = 3.14; // 推断为 f64
let s = "hello"; // 推断为 &str
let v = vec![1, 2, 3]; // 推断为 Vec<i32>如果需要显式标注类型,语法是 let name: Type = value;:
let x: i32 = 42;
let s: &str = "hello";
let v: Vec<i32> = vec![1, 2, 3];变量可以使用 let **遮蔽(shadow)**同名变量,甚至可以改变类型:
let input = "42"; // &str
let input = input.trim(); // &str,遮蔽
let input: i32 = input.parse().unwrap(); // i32,遮蔽且改变类型这与 mut 不同:mut 改变同一个绑定的值,遮蔽创建一个新的绑定。
函数
fn gcd(mut n: u64, mut m: u64) -> u64 {
assert!(n != 0 && m != 0);
while m != 0 {
if m < n {
let t = m;
m = n;
n = t;
}
m = m % n;
}
n
}要点解读:
- 函数参数必须标注类型(Rust 不会推断参数类型)。
- 返回类型用
->标注。 - 函数体是表达式,最后一个表达式(不加分号)就是返回值。这里
n不带分号,所以是返回值。 return关键字存在,但 Rust 社区倾向于使用隐式返回——这让函数成为「一个计算并返回某物的表达式」。assert!宏在条件为false时 panic。
表达式 vs 语句:在 Rust 中,几乎所有东西都是表达式,都有值。if、loop、block 都有值:
let status = if cpu_temp <= 70.0 {
"OK"
} else if cpu_temp <= 85.0 {
"Warning"
} else {
"Critical"
};
// 注意:每个分支的类型必须一致基本类型速览
// 整数:有符号和无符号,位宽固定
let a: i8 = -128; // -128 到 127
let b: u8 = 255; // 0 到 255
let c: i32 = -1_000_000; // 下划线增强可读性
let d: usize = 42; // 指针大小(64位平台 = u64)
// 浮点数
let pi: f64 = 3.14159265358979;
let e: f32 = 2.71828;
// 布尔值
let is_ok: bool = true;
// 字符:32 位 Unicode 标量值
let heart_eyed_cat = '😻'; // char 可以存表情符号
let z = 'ℤ';
// 元组(tuple)
let t: (i32, f64, char) = (42, 3.14, 'π');
let (x, y, z) = t; // 解构
let first = t.0; // 索引访问
let unit = (); // 单元类型,不占空间
// 数组:固定长度,在栈上
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 1000]; // 1000 个零
// 向量:动态长度,在堆上
let mut v: Vec<i32> = vec![1, 2, 3];
v.push(4);
v.pop();
// 字符串切片 &str:固定、不可变
let greeting: &str = "Hello, 世界";
// 拥有所有权的字符串 String:动态、可变
let mut s: String = String::from("Hello");
s.push_str(", world!");
// 切片 [T]:对数组或向量的某一连续区域的引用
let slice: &[i32] = &v[1..3]; // [2, 3]控制流
// if 表达式
let num = 42;
let size = if num < 100 { "small" } else { "large" };
// loop:无限循环(但有 break)
let mut i = 0;
loop {
i += 1;
if i >= 10 { break; }
}
// while
let mut n = 5;
while n > 0 {
println!("{}", n);
n -= 1;
}
// for:遍历集合
for i in 0..5 {
println!("{}", i); // 0, 1, 2, 3, 4
}
let v = vec!["a", "b", "c"];
for item in &v {
println!("{}", item);
}
// match:模式匹配
let code = 200;
match code {
200 => println!("OK"),
301 | 302 => println!("Redirect"),
404 => println!("Not Found"),
500..=599 => println!("Server Error"),
_ => println!("Unknown"),
}match 是 Rust 中最强大的控制流结构之一。编译器会检查是否覆盖了所有可能的情况(穷尽性检查)。
错误处理:Result 与 Option
Rust 没有异常。函数通过返回类型显式表达可失败性:
// Option<T>:可能没有值
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
match divide(10.0, 0.0) {
Some(result) => println!("Result: {}", result),
None => println!("Cannot divide by zero"),
}
// Result<T, E>:可能失败
use std::fs::File;
match File::open("hello.txt") {
Ok(file) => println!("File opened: {:?}", file),
Err(error) => println!("Error: {}", error),
}? 运算符是简洁的错误传播语法:
fn read_username() -> Result<String, std::io::Error> {
let mut s = String::new();
// ? 在错误时立即返回 Err,否则提取 Ok 中的值
std::fs::File::open("username.txt")?.read_to_string(&mut s)?;
Ok(s)
}结构体与方法
#[derive(Debug)] // 自动生成格式化输出
struct Person {
name: String,
age: u8,
}
impl Person {
// 关联函数(构造函数通常叫 new)
fn new(name: String, age: u8) -> Self {
Person { name, age }
}
// 方法(第一个参数是 &self)
fn greet(&self) -> String {
format!("Hi, I'm {} and I'm {} years old.", self.name, self.age)
}
// 可变方法
fn have_birthday(&mut self) {
self.age += 1;
}
}
fn main() {
let mut alice = Person::new(String::from("Alice"), 30);
println!("{}", alice.greet());
alice.have_birthday();
println!("After birthday: {:?}", alice);
}注意:&self 是 self: &Self 的语法糖,表示借用(不可变引用)。&mut self 是可变借用。没有 & 前缀的 self 表示获取所有权——调用后原变量不可用。
泛型与 Trait
// 泛型函数
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
println!("{}", largest(&[3, 7, 1, 9, 2])); // 9
println!("{}", largest(&['a', 'z', 'm'])); // zT: PartialOrd + Copy 是 Trait 约束(Trait bound):要求类型 T 同时实现了 PartialOrd(可比较)和 Copy(可以按位复制)。
定义 Trait:
trait Summary {
fn summarize(&self) -> String;
// Trait 可以提供默认实现
fn summarize_short(&self) -> String {
String::from("(Read more...)")
}
}
struct Article {
title: String,
body: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}——{}", self.title, &self.body[..50.min(self.body.len())])
}
}文件 I/O 实战
让我们写一个完整的程序:统计文件中每个单词的出现频率。
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// 从命令行参数获取文件名,默认为 "input.txt"
let args: Vec<String> = env::args().collect();
let filename = args.get(1).map(String::as_str).unwrap_or("input.txt");
// 读取整个文件内容
let contents = match fs::read_to_string(filename) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading {}: {}", filename, e);
return Err(e);
}
};
// 统计词频
let mut word_count = HashMap::new();
for word in contents.split_whitespace() {
let word = word.trim_matches(|c: char| !c.is_alphabetic());
if !word.is_empty() {
*word_count.entry(word.to_lowercase()).or_insert(0) += 1;
}
}
// 按频率降序排列
let mut entries: Vec<_> = word_count.into_iter().collect();
entries.sort_by(|a, b| b.1.cmp(&a.1));
// 输出前 10 个
for (word, count) in entries.iter().take(10) {
println!("{:20} => {}", word, count);
}
Ok(())
}这个示例展示了 Rust 中常见的惯用写法:
HashMap::entry().or_insert()模式:查找或插入Vec::sort_by与闭包- 迭代器链:
iter().take(10) match进行错误分支- 字符串方法链:
trim_matches(|c: char| !c.is_alphabetic())
模块系统
// 声明模块
mod math {
// 默认私有
fn private_helper() -> i32 { 42 }
// pub 导出
pub fn add(a: i32, b: i32) -> i32 {
a + b + private_helper()
}
}
fn main() {
let sum = math::add(2, 3); // 可以访问 pub 函数
// math::private_helper(); // 错误:私有函数
}更大的项目通常将模块放在单独的文件或目录中,但基本原理相同:模块递归地定义可见性边界。
一个完整示例:并发 Web 服务器
以下是 Rust 并发编程的一个缩影——使用标准库的线程和通道构建简单的 TCP 服务器:
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\nHello from Rust!";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:7878")?;
println!("Listening on port 7878...");
// 为每个连接创建一个线程
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(|| handle_client(stream));
}
Err(e) => eprintln!("Connection failed: {}", e),
}
}
Ok(())
}这个服务器虽然简单,但体现了 Rust 的几个关键理念:
?操作符传播错误match穷尽地处理结果thread::spawn+ 闭包的并发模型- 所有资源(socket、线程)在作用域结束自动清理
小结
本章快速浏览了 Rust 的核心语法元素。关键要点:
- 变量默认不可变,
mut允许修改;let支持遮蔽。 - 函数参数必须标注类型,返回类型用
->;函数体是表达式,函数末尾的无分号表达式即为返回值。 - 控制流都是表达式:
if、loop、match都有值,可以用于赋值。 - 类型系统丰富且安全:整数位宽固定、
Option<T>代替空指针、Result<T, E>代替异常。 - 结构体和方法通过
impl块组织,&self/&mut self/self区分借用级别。 - 泛型和 Trait 提供静态多态,Trait 约束(
T: Trait)限制泛型参数的行为。 - 模块系统控制可见性,
pub导出,默认私有。 ?运算符是错误传播的惯用语法,减少样板代码。
下一章,我们将深入 Rust 的基本类型系统——这是理解 Rust 内存模型和安全保证的基础。