【Rust】从积木到城堡:手把手玩转Rust语法(万字详解)


开场白:为什么Rust语法像乐高积木?

如果你问我:“Rust语法难吗?” 我会答:“它就像乐高——单个积木(语法元素)简单,组合起来却能拼出惊人复杂度,但只要掌握‘拼接规则’,就能从玩具车进阶到城堡。”

Rust的语法设计藏着三个小心思:

  • 严而不僵:编译期严格检查,但不限制你实现创意;
  • 简而不凡:基础语法简洁,但组合后能处理复杂场景;
  • 通而不杂:核心概念(如所有权)贯穿始终,学懂一个通一片。

这篇文章会带着你在“语法乐园”里闯关,每个关卡用生活例子+代码实操拆解知识点,最后还能拼出一个小项目。坐稳了,咱们发车!

第一关:变量与数据类型——给数据找个“家”

1. 变量:给值贴标签的艺术

变量就像给盒子贴标签——你可以把“苹果”放进贴了“水果”的盒子,也能换个“零食”标签装饼干。但Rust的变量有个怪脾气:默认“贴死”标签,想换内容得打招呼

fn main() {
    // 普通变量:默认“不可变”(immutable),像密封罐,装进去就不能换
    let 密封罐 = "饼干";
    // 密封罐 = "糖果"; // 编译报错:不能给不可变变量二次赋值
    
    // 加mut关键字:变成“可变”变量,像敞口杯,能随时换内容
    let mut 敞口杯 = "可乐";
    敞口杯 = "雪碧"; // 没问题,敞口杯可以换饮料
    println!("现在杯子里是:{}", 敞口杯); // 输出:现在杯子里是:雪碧
}


注:
输出的前面是是 Rust 编译器给出的警告提示,原因如下:

  • 警告 1:unused variable: 密封罐你定义了变量 密封罐 但没有在后续代码中使用它。Rust 会对未使用的变量发出警告,如果你确实不需要使用这个变量,可以按照提示给它加下划线前缀(如 let _密封罐 = “饼干”;),这样编译器就不会警告了。
  • 警告 2:value assigned to 敞口杯 is never read:你给 敞口杯 赋值了 “可乐”,但还没读取这个值就又把它改成了 “雪碧”。Rust 会对这种 “赋值后未读取就覆盖” 的操作发出警告,因为这可能是代码逻辑的疏漏。
    这些只是警告,不影响程序运行,但遵循提示可以让代码更规范哦。
    生活类比:不可变变量像火车票(只能坐指定车次),可变变量像公交卡(能反复充值使用)。
    Q:为什么默认不可变?
    A:减少意外修改,尤其多人协作时,你不用猜“这个值会不会被别人改了”。

2. 数据类型:给盒子定大小

Rust是“静态类型语言”——编译时就确定每个变量的类型。就像快递盒必须标尺寸,Rust的“盒子”(变量)也得明确能装多大的数据。

(1)标量类型:单个值的“小盒子”

像一颗糖、一粒纽扣,只能装单个值。

  • 整数:没有小数点的数字,分有符号(i)和无符号(u),后面跟位数(8-128)。比如i32(32位有符号,范围-2³¹到2³¹-1)、u8(8位无符号,范围0-255)。
let 年龄: u8 = 18; // 年龄不会是负数,用u8够了
let 温度: i32 = -5; // 温度可能为负,用i32
  • 浮点数:带小数点,f32(单精度)和f64(双精度,默认)。
let 身高: f64 = 1.75; // 默认用f64,精度更高
  • 布尔值:就两种可能,true或false。
    let 考试及格: bool = true;
  • 字符:用单引号,支持 Unicode(中文、emoji都行)。
let: char = '张';
let 表情: char = '😂'; // 占4字节,比C的char强

(2)复合类型:装多个值的“收纳盒”

像抽屉、货架,能装一组值。

  • 元组(Tuple):固定长度,不同元素可不同类型,像“杂物盒”。
// 定义元组:括号+逗号分隔,类型可选(编译器能推断)
let 个人信息 = ("小明", 18, 1.75);

// 访问元素:用点+索引(从0开始)
println!("姓名:{}", 个人信息.0); // 姓名:小明
println!("年龄:{}", 个人信息.1); // 年龄:18

// 解构元组:一次性拿出所有元素(超实用!)
let (姓名, 年龄, 身高) = 个人信息;
println!("{}今年{}岁,身高{}米", 姓名, 年龄, 身高);
  • 数组(Array):固定长度,所有元素同类型,像“整齐的货架”。
// 定义数组:中括号+分号+长度(类型标注格式:[类型; 长度])
let 周一到周五 = ["周一", "周二", "周三", "周四", "周五"];
let scores: [i32; 3] = [90, 85, 95]; // 3个i32类型的分数

// 访问元素:中括号+索引
println!("周三是周几?{}", 周一到周五[2]); // 周三是周几?周三

// 数组切片:取部分元素(后续讲引用时细说)
let 前两科分数 = &scores[0..2]; // 取索引0和1的元素


元组vs数组:元组适合“不同类型的一组相关值”(如坐标(x,y,z)),数组适合“同类型的一组值”(如成绩列表)。

第二关:控制流——给程序“指路”

程序像迷宫,控制流就是路标。Rust的控制流规则简单,但组合起来能走通复杂逻辑。

1. if-else:二选一的岔路口

像“如果下雨就带伞,否则带墨镜”,根据条件走不同路。

fn main() {
    let 分数 = 85;
    
    // if条件必须是bool类型(Rust不自动转换,这点很严谨!)
    if 分数 >= 60 {
        println!("及格啦!");
    } else if 分数 >= 50 {
        println!("差一点,下次加油");
    } else {
        println!("不及格,要努力哦");
    }
    
    // 小技巧:if可以当表达式用(返回值给变量)
    let 评价 = if 分数 >= 90 { "优秀" } else if 分数 >= 70 { "良好" } else { "一般" };
    println!("评价:{}", 评价); // 输出:评价:良好
}


注意:和Python不同,Rust的if条件不用括号,但大括号必须有;且分支返回值类型必须一致(要么都返回字符串,要么都返回数字)。

2. 循环:重复做一件事

像“每天背单词,直到记住为止”,Rust有三种循环方式。

  • loop:无限循环,直到手动break(适合“不完成任务不罢休”的场景)。
let mut 计数 = 0;
loop {
    计数 += 1;
    println!("第{}次循环", 计数);
    if 计数 == 3 {
        break; // 循环3次后退出
    }
}
// 输出:第1次循环 → 第2次循环 → 第3次循环


如果程序已经陷入死循环无法正常退出,可以在运行程序的终端中,按 Ctrl + C,就会退出了。

  • while:条件为真就循环(适合“满足条件就继续”的场景)。
let mut 剩余生命值 = 3;
while 剩余生命值 > 0 {
    println!("还有{}条命", 剩余生命值);
    剩余生命值 -= 1;
}
println!("游戏结束");
// 输出:还有3条命 → 还有2条命 → 还有1条命 → 游戏结束
  • for:遍历集合(最常用,安全又简洁,避免数组越界)。
let 水果 = ["苹果", "香蕉", "橙子"];

// 遍历数组所有元素
forin 水果 {
    println!("我爱吃{}",);
}

// 用范围(range)生成数字序列(1..=5 表示1到5包含5)
for 数字 in 1..=3 {
    println!("数字:{}", 数字);
}


Q:为什么for最常用?
A:它会自动处理集合边界,不像while可能因索引错误崩溃,比如for 果 in 水果永远不会访问到第4个元素(数组只有3个)。

第三关:函数——代码的“乐高模块”

函数就像预制乐高模块——把常用操作打包,需要时直接拼上去。Rust的函数用fn定义,讲究“输入输出明确”。

1. 基础函数:有去有回

// 定义函数:fn 函数名(参数: 类型) -> 返回类型 { 逻辑 }
fn 加法(: i32,: i32) -> i32 {+// 最后一行表达式的结果就是返回值(不用写return,除非提前返回)
}

fn 打印欢迎词() { // 无参数无返回值(返回类型可省略,等价于-> ())
    println!("欢迎来到Rust世界!");
}

fn main() {
    let= 加法(2, 3);
    println!("2+3={}",); // 输出:2+3=5
    打印欢迎词(); // 输出:欢迎来到Rust世界!
}


小细节:Rust函数名用“蛇形命名法”(全小写,单词间下划线连接),比如print_hello,和变量名规则一致。

2. 函数参数:带类型的“原料”

参数必须指定类型(Rust不猜类型),多个参数用逗号分隔。就像做菜的原料表,必须写清“200克面粉”而不是“一点面粉”。

// 计算长方形面积:长×宽
fn 长方形面积(: f64,: f64) -> f64 {*}

fn main() {
    let 面积 = 长方形面积(5.2, 3.1);
    println!("面积是:{}", 面积); // 输出:面积是:16.12
}

3. 函数返回值:明确的“成品”

返回值类型用-> 类型声明,函数体最后一个表达式的结果自动返回。如果需要提前返回,用return。

// 判断是否为偶数:是则返回true,否则false
fn 是偶数(数字: i32) -> bool {
    if 数字 % 2 == 0 {
        return true; // 提前返回
    }
    false // 最后一行自动返回
}

fn main() {
    println!("4是偶数吗?{}", 是偶数(4)); // 输出:true
    println!("5是偶数吗?{}", 是偶数(5)); // 输出:false
}

第四关:所有权——Rust的“防丢包”神功

这是Rust最独特的概念,也是初学者的“第一道坎”。简单说,所有权就是“谁负责看管数据,不用了谁来收拾”,避免内存泄漏或重复释放(C/C++的常见坑)。

1. 核心规则:数据的“归属权”

  • 规则1:每个值在Rust中都有一个“所有者”(变量)。
  • 规则2:同一时间只有一个所有者。
  • 规则3:所有者离开作用域(比如函数结束),值会被自动“销毁”(释放内存)。

用“借书”来理解:

  • 你从图书馆借了一本书(所有者是你);
  • 你不能同时把书借给两个人(同一时间一个所有者);
  • 你还书后(离开作用域),书回到图书馆(被销毁,等待下一个人借)。
fn main() {
    // s1是字符串"hello"的所有者
    let s1 = String::from("hello");
    // s1把所有权转移给s2(就像把书送给s2)
    let s2 = s1;
    
    // println!("{}", s1); // 编译报错:s1已经没有所有权了(书被拿走了,你不能再看)
    println!("{}", s2); // 正确:s2现在是所有者
} // s2离开作用域,字符串被自动销毁(释放内存)


为什么要转移所有权? 避免“double free”错误(C/C++中两个指针释放同一块内存会崩溃)。Rust通过“同一时间一个所有者”从根源解决这个问题。

2. 克隆(Clone):复制一份数据

如果想让s1和s2都有“hello”,可以用clone——相当于复印一本书,两人各持一份。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // 克隆:深拷贝堆上的数据
    
    println!("s1: {}, s2: {}", s1, s2); // 正确:两者都有效
}


注意:clone会复制数据,对大数据来说开销大。简单类型(如整数、布尔)默认实现“Copy” trait,赋值时自动复制,不用手动clone:

fn main() {
    let x = 5;
    let y = x; // 整数自动Copy,x仍有效
    
    println!("x: {}, y: {}", x, y); // 输出:x: 5, y: 5
}

3. 引用(Reference):临时“借用”数据

如果只是想临时用一下数据,不想转移所有权(比如函数需要读取但不修改),可以用“引用”——就像借书看,看完还回去,原主人仍持有所有权。
引用用&表示,也叫“借用”(borrowing)。

// 函数参数是字符串的引用(&String),只借不用
fn 打印长度(s: &String) -> usize {
    s.len() // 可以读取,但不能修改(默认不可变引用)
}

fn main() {
    let s = String::from("hello");
    let 长度 = 打印长度(&s); // 传递引用(&s),s仍有所有权
    println!("字符串{}的长度是{}", s, 长度); // 正确:s还在
}

可变引用:允许临时修改

如果函数需要修改借用的数据,用&mut(可变引用),但有严格限制:

  • 同一时间,一个数据只能有一个可变引用;
  • 可变引用和不可变引用不能同时存在。
// 给字符串追加内容(需要可变引用)
fn 追加内容(s: &mut String) {
    s.push_str(", world");
}

fn main() {
    let mut s = String::from("hello");
    追加内容(&mut s); // 传递可变引用
    println!("{}", s); // 输出:hello, world
    
    // 同一时间只能有一个可变引用
    let r1 = &mut s;
    // let r2 = &mut s; // 编译报错:不能同时有两个可变引用
    
    // 可变引用和不可变引用不能共存
    let r3 = &s; // 不可变引用
    // let r4 = &mut s; // 编译报错:已有不可变引用,不能再借可变引用
}

Q:为什么这么严格?
A:防止数据竞争(多线程同时读写共享数据),这些规则在编译期检查,避免运行时错误。

4. 切片(Slice):引用数据的一部分

切片是对集合中一部分的引用,比如字符串的子串。用&[start…end]表示,不包含end索引。

fn main() {
    let s = String::from("hello world");
    
    // 取从索引0到5(不包含5)的子串
    let 前五个 = &s[0..5]; // 等价于&s[..5]
    // 取从索引6到结尾的子串
    let 后五个 = &s[6..11]; // 等价于&s[6..]
    println!("前五个:{},后五个:{}", 前五个, 后五个); // 输出:hello world
    
    // 字符串字面值其实是切片
    let 字面值 = "hello"; // 类型是&str(字符串切片)
}


切片的好处:避免手动管理索引,且切片是引用,不持有所有权,安全又高效。

第五关:结构体——自定义“复合数据块”

结构体(Struct)像“自定义数据盒子”,可以把不同类型的数据打包成一个整体。比如用结构体表示“用户”,包含姓名、年龄、邮箱等信息。

1. 定义结构体:设计盒子样式

// 定义结构体:struct 名称 { 字段名: 类型, ... }
struct User {
    用户名: String,
    年龄: u32,
    已验证: bool,
}

fn main() {
    // 创建结构体实例:字段按定义顺序或键值对赋值
    let 用户1 = User {
        用户名: String::from("张三"),
        年龄: 25,
        已验证: true,
    };
    
    // 访问字段:用点号
    println!("用户名:{},年龄:{}", 用户1.用户名, 用户1.年龄);
    
    // 修改字段:结构体必须是mut的
    let mut 用户2 = User {
        用户名: String::from("李四"),
        年龄: 30,
        已验证: false,
    };
    用户2.已验证 = true; // 标记为已验证
    println!("用户2是否验证:{}", 用户2.已验证);
}

2. 结构体更新语法:基于旧实例创建新实例

如果新实例和旧实例大部分字段相同,用…语法复用旧字段,只改不同的部分。

struct User {
    用户名: String,
    年龄: u32,
    已验证: bool,
}

fn main() {
    let 张三 = User {
        用户名: String::from("张三"),
        年龄: 25,
        已验证: true,
    };
    
    // 创建李四:复用张三的年龄和已验证状态,只改用户名
    let 李四 = User {
        用户名: String::from("李四"),
        ..张三 // 注意:..后面没有逗号
    };
    
    println!("李四的年龄:{}", 李四.年龄); // 输出:25(复用张三的年龄)
}

3. 元组结构体:没有字段名的结构体

适合给元组起个名字,强调其含义。比如用Point表示坐标,比单纯的元组(x, y)更清晰。

// 元组结构体:struct 名称(类型1, 类型2, ...)
struct Point(i32, i32, i32); // 三维坐标
struct 颜色(u8, u8, u8); // RGB颜色

fn main() {
    let 原点 = Point(0, 0, 0);
    let 红色 = 颜色(255, 0, 0);
    
    // 访问字段:用索引
    println!("原点x坐标:{}", 原点.0);
    println!("红色的R值:{}", 红色.0);
}

4. 结构体方法:给结构体“添功能”

方法是依附于结构体的函数,让结构体“自己会做事”。比如User结构体可以有打印信息方法,Point可以有计算距离方法。

struct Point {
    x: i32,
    y: i32,
}

impl Point { // impl块:给Point定义方法
    // 方法的第一个参数必须是&self(表示调用方法的实例)
    fn 距离原点(&self) -> i32 {
        self.x * self.x + self.y * self.y // 计算x²+y²(简化的距离平方)
    }
    
    // 关联函数:不依赖实例(类似静态方法),用结构体名调用
    fn 原点() -> Point {
        Point { x: 0, y: 0 }
    }
}

fn main() {
    let p = Point { x: 3, y: 4 };
    println!("到原点的距离平方:{}", p.距离原点()); // 输出:25(3²+4²)
    
    let 原点 = Point::原点(); // 调用关联函数
    println!("原点坐标:({}, {})", 原点.x, 原点.y); // 输出:(0, 0)
}


方法vs函数:方法是“结构体自己的功能”(比如“人会走路”),函数是“外部工具”(比如“用工具测量人的身高”)。

第六关:枚举与模式匹配——“多选一”的智慧

枚举(Enum)用于表示“只能是其中一种的类型”,比如交通灯只能是红、黄、绿,性别只能是男、女、其他。模式匹配(Pattern Matching)则是“按枚举的不同情况分别处理”,比一堆if-else更清晰。

1. 定义枚举:列出所有可能

// 定义枚举:enum 名称 { 变体1, 变体2, ... }
enum 交通灯 {,,
    绿,
}

// 枚举变体可以带数据
enum 消息 {
    文本(String), // 文本消息:带字符串
    整数(i32), // 整数消息:带整数
    通知 { 标题: String, 内容: String }, // 结构体样式的变体
}

fn main() {
    let= 交通灯::;
    let 短信 = 消息::文本(String::from("你好"));
    let 数字 = 消息::整数(100);
    let 提醒 = 消息::通知 {
        标题: String::from("会议"),
        内容: String::from("下午3点开会"),
    };
}

2. Option枚举:处理“可能没有值”的情况

Rust没有空值(null),但用Option枚举表示“可能有值,也可能没有”,避免空指针错误(C/C++的常见崩溃原因)。
Option定义在标准库中,你可以直接用:

enum Option<T> {
    Some(T), // 有值:T是任意类型
    None,    // 无值
}
fn main() {
    let 有值: Option<i32> = Some(5);
    let 无值: Option<i32> = None;
    
    // Option<T>不能直接和T类型运算,必须先处理None的情况(强制安全)
    let x: i32 = 5;
    // let sum = x + 有值; // 编译报错:i32和Option<i32>不能相加
}


Q:为什么这么设计?
A:显式区分“一定有值”和“可能无值”,让你在编译期就处理“无值”的情况,避免运行时崩溃。

3. 模式匹配:按情况处理

用match关键字,像“猜谜游戏”——把枚举的每个变体“对号入座”,确保所有情况都被处理。

enum 交通灯 {,,
    绿,
}

// 根据交通灯返回提示语
fn 灯提示(: 交通灯) -> &'static str {
    match{
        交通灯::=> "停止",
        交通灯::=> "准备",
        交通灯::绿 => "通行",
        // 必须列出所有变体,否则编译报错(确保无遗漏)
    }
}

fn main() {
    let= 交通灯::;
    println!("交通灯提示:{}", 灯提示()); // 输出:准备
}

匹配Option:处理可能的空值

// 尝试把Option<i32>转成i32,无值则返回0
fn 取数值(可能有值: Option<i32>) -> i32 {
    match 可能有值 {
        Some() =>, // 有值则返回值
        None => 0,      // 无值则返回0
    }
}

fn main() {
    let a = Some(5);
    let b = None;
    println!("a的值:{}", 取数值(a)); // 5
    println!("b的值:{}", 取数值(b)); // 0
}

if let:简化的模式匹配

如果只关心一种情况,用if let比match更简洁(相当于“只处理这一种,其他忽略”)。

let 可能有值: Option<i32> = Some(3);

// 只关心Some的情况
if let Some() = 可能有值 {
    println!("有值:{}",); // 输出:有值:3
} else {
    // 可选的else处理None
    println!("无值");
}
第七关:错误处理——优雅地“认错”
程序出错很正常,关键是怎么处理。Rust不用异常(Exception),而是用Result枚举和panic!宏,让错误处理更显式、更可控。
1. panic!:遇到无法恢复的错误时“崩溃”
像“系统内存耗尽”“文件损坏无法读取”,这种错误无法恢复,直接终止程序并打印错误信息。
fn main() {
    let 年龄 = 15;
    if 年龄 < 18 {
        panic!("年龄必须大于18岁,当前是{}", 年龄); // 崩溃并打印错误
    }
}
// 运行结果:thread 'main' panicked at '年龄必须大于18岁,当前是15', src/main.rs:4:9

第七关:错误处理——优雅地“认错”

程序出错很正常,关键是怎么处理。Rust不用异常(Exception),而是用Result枚举和panic!宏,让错误处理更显式、更可控。

1. panic!:遇到无法恢复的错误时“崩溃”

像“系统内存耗尽”“文件损坏无法读取”,这种错误无法恢复,直接终止程序并打印错误信息。

fn main() {
    let 年龄 = 15;
    if 年龄 < 18 {
        panic!("年龄必须大于18岁,当前是{}", 年龄); // 崩溃并打印错误
    }
}
// 运行结果:thread 'main' panicked at '年龄必须大于18岁,当前是15', src/main.rs:4:9


什么时候用panic!:逻辑错误(比如传入无效参数),确保程序在错误状态下不继续运行。

2. Result枚举:处理可恢复的错误

对于“文件不存在”“网络超时”这类可以处理的错误,用Result<T, E>枚举:

enum Result<T, E> {
    Ok(T),  // 成功:返回T类型的结果
    Err(E), // 失败:返回E类型的错误
}
比如读取文件时,可能成功(返回文件内容)或失败(返回错误原因):
use std::fs::File;

fn main() {
    // File::open返回Result<File, std::io::Error>
    let 文件 = File::open("test.txt");
    
    let 文件 = match 文件 {
        Ok(f) => f, // 成功:得到文件句柄
        Err(e) => {
            println!("打开文件失败:{}", e);
            return; // 处理错误后退出
        }
    };
    // 成功打开文件,继续操作...
}

简化错误处理:?运算符

如果希望“出错时直接返回错误”,用?运算符,比match更简洁(只能在返回Result的函数中使用)。

use std::fs::File;
use std::io::Read;

// 读取文件内容:返回Result<String, 错误类型>
fn 读文件() -> Result<String, std::io::Error> {
    let mut 文件 = File::open("test.txt")?; // 出错时直接返回错误
    let mut 内容 = String::new();
    文件.read_to_string(&mut 内容)?; // 出错时直接返回错误
    Ok(内容) // 成功返回内容
}

fn main() {
    match 读文件() {
        Ok(内容) => println!("文件内容:{}", 内容),
        Err(e) => println!("出错:{}", e),
    }
}


Q:?的作用
A:相当于“如果是Err就返回,否则取Ok的值继续”,大大简化错误处理代码。

第八关:泛型、特质与生命周期——代码的“复用术”

这部分是Rust的“进阶乐高件”,让你写出更通用、更灵活的代码,同时保持类型安全。

1. 泛型:“万能模具”,一套代码适配多种类型

像“万能螺丝刀”,一个工具适配多种螺丝。泛型让函数/结构体/枚举可以处理多种类型,而不用重复写代码。

泛型函数:一个函数处理多种类型

// 引入 Display trait(来自 std::fmt 模块)
use std::fmt::Display;

// 泛型函数添加 Display 约束:T 必须实现 Display 才能被 {} 打印
fn 打印<T: Display>(: T) {
    println!("值:{}",);
}

fn main() {
    打印(5);      // i32 实现了 Display → 正常打印
    打印("hello"); // &str 实现了 Display → 正常打印
    打印(3.14);   // f64 实现了 Display → 正常打印
}

泛型结构体:结构体字段可以是任意类型

// 定义泛型结构体:struct 名称<T> { 字段: T }
struct 盒子<T> {
    内容: T,
}

fn main() {
    let 整数盒子 = 盒子 { 内容: 5 };
    let 字符串盒子 = 盒子 { 内容: "hello" };
    
    println!("盒子里的整数:{}", 整数盒子.内容);
    println!("盒子里的字符串:{}", 字符串盒子.内容);
}


Q;泛型会影响性能吗?
A:不会。Rust在编译时会为每个具体类型生成专用代码(单态化),就像你手动写了多个版本的函数,只是编译器帮你做了。

2. 特质(Trait):定义“行为规范”

特质像“接口”,规定类型必须实现哪些方法,但不实现具体逻辑。比如“可打印”特质要求类型有打印方法,任何类型只要实现了打印,就属于“可打印”类型。

定义和实现特质

// 定义特质:trait 名称 { 方法声明; }
trait 可打印 {
    fn 打印(&self);
}

// 为结构体实现特质
struct{
    姓名: String,
}
impl 可打印 for{
    fn 打印(&self) {
        println!("我叫{}", self.姓名);
    }
}

// 为i32实现特质(可以为内置类型加特质!)
impl 可打印 for i32 {
    fn 打印(&self) {
        println!("数字是{}", self);
    }
}

fn main() {
    let 张三 ={ 姓名: String::from("张三") };
    张三.打印(); // 输出:我叫张三
    
    let 数字 = 100;
    数字.打印(); // 输出:数字是100
}

特质作为参数:要求类型有特定行为

// 函数参数要求实现可打印特质
fn 批量打印<T: 可打印>(列表: &[T]) {
    for 元素 in 列表 {
        元素.打印();
    }
}

fn main() {
    let 列表 = vec![1, 2, 3];
    批量打印(&列表); // 输出三个数字
    
    let 人列表 = vec![{ 姓名: String::from("张三") },{ 姓名: String::from("李四") },
    ];
    批量打印(&人列表); // 输出两个人的名字
} 

3. 生命周期:确保引用“活着”

生命周期是Rust的“租赁合同”——规定引用的有效期限,确保你不会使用“已过期”的引用(悬垂引用)。

悬垂引用问题

fn 错误例子() -> &str {
    let s = String::from("hello");
    &s // 错误:s在函数结束时销毁,返回的引用会指向无效内存
}

生命周期标注:告诉编译器引用的关系

用’a(撇号+a)表示生命周期,标注在函数参数和返回值上,说明“哪个参数的生命周期决定返回值的生命周期”。

// 函数返回两个字符串中较长的一个
// 'a是生命周期标注,说明返回的引用和x、y活得一样久
fn 较长的字符串<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = String::from("apple");
    let s2 = "banana"; // 字符串字面值的生命周期是'static(整个程序运行期间)
    
    let 长的 = 较长的字符串(&s1, s2);
    println!("较长的字符串:{}", 长的); // 输出:banana
}


生命周期的核心:编译器通过标注判断引用是否有效,避免悬垂引用,你不用手动管理内存释放时机。

第九关:模块与包管理——代码的“收纳术”

当代码量大了,需要分类整理,就像家里的物品要分抽屉放。Rust用模块(Module)和包(Crate)管理代码结构。

1. 模块:代码的“抽屉”

用mod定义模块,把相关的函数、结构体、枚举等放进去,控制访问权限(公共/私有)。

// 定义模块:mod 名称 { 内容 }
mod 数学工具 {
    // 私有函数:默认私有,只能在模块内使用
    fn(a: i32, b: i32) -> i32 {
        a + b
    }
    
    // 公共函数:用pub关键字,模块外可访问
    pub fn(a: i32, b: i32) -> i32 {
        a - b
    }
    
    // 公共结构体,字段默认私有
    pub struct{
        x: i32, // 私有字段
        pub y: i32, // 公共字段
    }
    
    impl{
        // 公共关联函数:创建点
        pub fn(x: i32, y: i32) -> Self {{ x, y }
        }
        
        // 公共方法:获取x(因为x是私有字段)
        pub fn get_x(&self) -> i32 {
            self.x
        }
    }
}
use 数学工具::; // 引入模块中的结构体,使用时不用写全名
fn main() {
    // 调用公共函数
    println!("5-3={}", 数学工具::(5, 3)); // 输出:2
    
    // 使用公共结构体
    let p =::(10, 20);
    // println!("x={}", p.x); // 错误:x是私有字段
    println!("x={}", p.get_x()); // 正确:通过公共方法访问
    println!("y={}", p.y); // 正确:y是公共字段
}


访问权限规则

  • 模块内的项默认私有,pub关键字使其公共;
  • 父模块不能访问子模块的私有项,但子模块可以访问父模块的项。

2. use关键字:简化模块访问

用use把模块中的项引入当前作用域,不用每次写完整路径。

mod 动物 {
    pub mod 哺乳动物 {
        pub struct{
            pub 名字: String,
        }
        impl{
            pub fn(&self) {
                println!("{}:汪汪!", self.名字);
            }
        }
    }
}

// 引入路径:直接到狗结构体
use 动物::哺乳动物::;

fn main() {
    let 旺财 ={ 名字: String::from("旺财") };
    旺财.(); // 输出:旺财:汪汪!
}

3. 包(Crate)与工作空间

  • 包(Crate):Rust的编译单元,要么是可执行程序(binary),要么是库(library)。
  • 工作空间(Workspace):管理多个相关的包,适合大型项目。

用cargo new创建包:

cargo new 我的程序 --bin  # 创建可执行包(默认)
cargo new 我的库 --lib     # 创建库包

库包的使用:在Cargo.toml中添加依赖,然后用use引入。

实战小项目:猜数字游戏

学了这么多语法,我们来拼一个小项目——猜数字游戏,巩固所学知识:

use std::io;
use std::cmp::Ordering;
use rand::Rng; // 需要在Cargo.toml添加rand依赖

fn main() {
    println!("猜数字游戏!");
    
    // 生成1-100的随机数
    let 秘密数字 = rand::thread_rng().gen_range(1..=100);
    
    loop {
        println!("请输入你猜的数字:");
        
        // 读取用户输入
        let mut 猜测 = String::new();
        io::stdin().read_line(&mut 猜测)
            .expect("读取输入失败");
        
        // 转换为整数(处理无效输入)
        let 猜测: u32 = match 猜测.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("请输入有效的数字!");
                continue;
            }
        };
        
        // 比较猜测和秘密数字
        match 猜测.cmp(&秘密数字) {
            Ordering::Less => println!("太小了!"),
            Ordering::Greater => println!("太大了!"),
            Ordering::Equal => {
                println!("猜对了!");
                break; // 猜对了退出循环
            }
        }
    }
}


用到的语法点

  • 变量(秘密数字、猜测)和可变性(mut);
  • 标准库使用(std::io输入、std::cmp::Ordering比较);
  • 循环(loop)和条件(match);
  • 错误处理(match处理parse的Result);
  • 外部依赖(rand库生成随机数)。

结语:语法是工具,解决问题是目的

Rust语法乍看复杂,但每个规则都在帮你“少犯错、写出更健壮的代码”。所有权防止内存泄漏,模式匹配确保逻辑无遗漏,特质让代码复用更安全……
记住:语法不是背诵的教条,而是解决问题的工具。就像乐高积木,你不用记住每块积木的形状,但要知道如何用它们拼出你想要的东西。
接下来,试着用Rust写个小工具吧——比如批量重命名文件、解析日志,在实践中消化这些语法,你会发现Rust的严谨带来的安全感,是其他语言难以替代的。
祝你在Rust的世界里玩得开心!

转载请说明出处内容投诉
CSS教程网 » 【Rust】从积木到城堡:手把手玩转Rust语法(万字详解)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买