尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Rust通用编程概念:变量、类型、函数与控制流全解析

Rust通用编程概念:变量、类型、函数与控制流全解析 这篇笔记是Rust学习记录系列的第三篇主题是C3阶段的通用编程概念。前面两篇我们搞定了工具链、项目结构和cargo的基本玩法现在终于要进入真正的语法世界了。Rust的通用编程概念涵盖变量、数据类型、函数、控制流这些所有语言都有的东西但Rust在这套“基础套餐”里注入了很多独特的设计思路——不可变性、强类型、表达式导向这些恰恰是它后来那些所有权、借用检查机制的地基。如果你刚接触Rust想系统梳理知识点或者已经写过一点Rust但总觉得对“为什么这样设计”没吃透这篇应该能帮你把这些概念串起来。我尽量按踩过的坑来讲不搞教科书式罗列读完你可以直接照着写代码。1. 变量与可变性从第一个let开始理解Rust的设计哲学1.1 默认不可变为什么Rust要“反着来”任何语言教程都从变量讲起Rust也不例外但它的第一个反常识设计就是变量默认不可变。fn main() { let x 5; // x 6; // 编译报错cannot assign twice to immutable variable }刚接触时很多人会不适应我在初学阶段就反复碰壁总想直接给变量重新赋值。但静下心想想Rust这样做真不是故意为难人而是把“可变”变成了一种需要显式声明的东西。当你写下let mut y 5这行代码时等于在告诉编译器、也告诉读代码的人这里的数据是会被修改的请注意。这个设计直接降低了代码的认知负担。你看到一个let变量第一反应就是它的值从头到尾不会变不必去追踪它在哪个函数里被偷偷改过。在实际开发里很多bug都源于一个变量在代码不同位置被改了多次最终状态难以预测。Rust用“默认不可变”从源头砍掉了这一整类问题。那如果就是想改呢很简单声明时加mut关键字let mut counter 0; counter 1;这就是Rust的态度不是禁止可变而是让可变“显式化”。你越早接受这个设定后面读所有权相关的代码就越顺畅因为不可变变量在后续所有权的转移语义里也会给你省下很多心智成本。1.2 const常量与let绑定的关键差异除了letRust 还有const用来声明常量。初学者往往把两者混淆觉得反正都是“不能变”区别到底在哪const MAX_POINTS: u32 100_000;常量和不可变变量有三点核心差异。第一常量必须显式标注类型不能让编译器去推断第二常量只能被赋值为编译期就能确定的表达式不能是函数调用的运行结果第三常量在整个程序生命周期内都有效它的作用域不局限于某个函数内部更像是全局的“约定值”。我建议把经常用到的魔法数字都提成常量比如端口号、超时时间、最大重试次数。这不仅是代码规范问题更重要的是当你需要调整这些数值时只需要改一处声明而不是满项目搜索散落的数字字面量。Rust社区普遍使用全大写字母加下划线的命名风格中间的数字分隔符100_000也让人一眼就能看出位数这个习惯值得保留。1.3 变量遮蔽同一个名字两种人生变量遮蔽Shadowing是我在通用概念阶段觉得最妙的一个特性它允许你声明一个与之前同名的新变量新变量会“遮蔽”旧变量。let spaces ; let spaces spaces.len(); println!(spaces length: {}, spaces);第一次看到这段代码时我有点懵一个变量怎么能一会儿是字符串一会儿又是数字但仔细一想这正是Rust比很多语言优雅的地方。在其他语言里你可能需要写成spacesStr和spacesLen两个名字来区分但在Rust里你可以复用同一个名字因为旧绑定已经被遮蔽编译器不会再让它参与后续计算。这里要特别注意遮蔽和mut的本质区别mut是原地修改同一个内存绑定变量类型不能改变而遮蔽是创建一个全新的绑定完全可以是不同类型。概括来说遮蔽适合“对同一个值做一系列变换但每一步都希望保留不可变性”的场景。不过也别滥用遮蔽太频繁会让人难以追踪当前到底用的是哪个值适度使用就好。2. 数据类型体系标量、复合与类型推导2.1 整数家族位宽选择隐藏的细节Rust的整数类型非常丰富有 i8、i16、i32、i64、i128、isize以及对应的无符号版本 u8、u16、u32、u64、u128、usize。默认推断是 i32这在大多数场景下是性能和内存的平衡点。数字分隔符和多种进制字面量是Rust很贴心的地方let decimal 98_222; let hex 0xff; let octal 0o77; let binary 0b1111_0000; let byte bA; // u8类型值65 let big: u64 1_000_000;选类型时有个容易踩的坑是整数溢出。在debug模式下整数溢出会让程序直接panic但在release模式下溢出会执行“环绕”逻辑即自动按二进制补码回绕而不是报错。比如 u8 类型的 255 再加 1debug下崩溃release下变成 0。这就不声不响地埋下了隐患。处理溢出的场景可以显式调用wrapping_add、saturating_add等方法让行为可控。在实际编码中我的经验是能用小类型就不要贪大对性能敏感的场景类型宽度直接决定内存占用和带宽消耗但如果是业务逻辑代码无脑选 i32/u32 或 usize 也完全够用不必过度优化到每个字段都精打细算。2.2 浮点、布尔与字符小类型也有讲究浮点类型只有 f32 和 f64默认是 f64。因为现代CPU对64位浮点的处理速度并不比32位慢反而精度更高。写科学计算、图形渲染这类对精度敏感的程序时优先f64只有在嵌入式或者内存极度受限的场景才考虑f32。这里需要提醒一句关于浮点比较的坑。由于浮点数的二进制表示天然存在精度误差直接判断两个浮点是否相等往往得到意想不到的结果。例如 0.1 0.2 并不严格等于 0.3这在任何语言里都一样。如果你的业务逻辑需要比较浮点数通常的做法是比较它们的差的绝对值是否小于某个极小阈值而不是直接用。布尔类型bool只有 true 和 false 两个值这个没什么特别。Rust的字符类型char则比较特殊它是Unicode标量值占4个字节能表示中文、emoji这些内容。注意它和字符串是两码事字符串是UTF-8编码的字节序列而char是一个独立的Unicode码点。很多后端同学第一次在Rust里处理中文字符串时会发现“取第几个字符”远没有其他语言那么直接就是因为字符和字节在Rust里分得特别清楚。2.3 元组与数组固定大小的复合数据复合类型是组合多个值的方式Rust最基础的两个就是元组tuple和数组array。元组用圆括号定义可以容纳不同类型的数据let tup: (i32, f64, u8) (500, 6.4, 1); let (x, y, z) tup; // 解构 let first tup.0; // 下标访问元组特别适合用来返回多个关联值。比如一个函数要同时返回状态码和描述信息其他语言可能得定义一个结构体但在Rust里直接返回(i32, String)就够了等业务复杂了再升级成结构体也不迟。使用解构时要注意如果你只关心其中某个元素可以用_占位符忽略其他值这是Rust非常常用的模式。数组用方括号定义所有元素必须是同一类型长度固定let arr [1, 2, 3, 4, 5]; let first arr[0];数组存储在栈上而不是堆上所以访问速度极快。但它不适合动态增删一旦长度确定就无法改变。需要动态长度时标准库提供了Vec我们暂时不用深入先把数组用明白。有一点必须时刻记住数组越界访问会直接panic不会像C语言那样给你一个未定义行为。这在调试时是好事程序会在出错的第一时间崩溃并告诉你索引越界而不是等到数据被破坏后才后知后觉。3. 函数与表达式Rust的“值思维”3.1 函数声明与参数类型标注不能省Rust的函数用fn关键字声明命名风格是蛇形命名法snake_case。函数参数必须显式标注类型这点和Go、TypeScript类似但和C的模板推导场景不太一样——Rust要求每个参数的类型在编译期就完全确定。fn main() { let result add(3, 5); println!(result {result}); } fn add(x: i32, y: i32) - i32 { x y }参数标注类型的好处是函数签名本身就是文档调用时一眼就能看到需要什么类型、返回什么类型。对于从脚本语言转过来的朋友刚开始可能会觉得繁琐但这也是Rust编译器能给出精确报错信息的基础——它知道每个位置的数据类型所以能在编译期拦截大量类型不匹配的问题。3.2 语句与表达式分号就是分水岭Rust对语句和表达式的区分非常严格这一点常常让新手困惑。简单说语句是执行操作但不返回值的代码表达式是计算并产生值的代码。函数体由表达式组成最后一个表达式的值就是函数的返回值。fn main() { let y { let x 3; x 1 // 注意没有分号 }; println!(y {y}); // 打印4 }上面代码里let y { ... }花括号内的代码块本身是一个表达式它的值是块内最后一个表达式x 1的结果。如果给x 1加上分号它就从表达式变成语句整个代码块就会返回()也就是空元组赋值给y时就会报类型不匹配。我到现在还记得自己第一次写Rust函数时习惯性地在返回表达式末尾加了分号结果编译器报了一串类型错误。后来才理解分号是语句的终结符表达式加分号就变成语句语句没有返回值。这算是Rust阶段最需要“洗脑”的一个思维方式。3.3 返回值隐藏的return与清晰的意图Rust函数用-标注返回类型返回值通常是函数体最后一个表达式不用写return。return关键字的作用是提前返回比如在条件分支中直接跳出函数fn check_age(age: i32) - String { if age 0 { return String::from(invalid); } if age 18 { String::from(adult) } else { String::from(minor) } }这种“表达式即返回值”的风格让代码非常紧凑逻辑分支也可以作为表达式的值赋给变量。但要注意如果你在分支里混用了带分号和不带分号的写法很容易造成类型不匹配。我的经验是写函数时先想清楚每个分支最后要返回什么再决定哪里加 return 哪里不加。简单函数直接靠最后一个表达式返回复杂逻辑用return提前退出反而更清晰。4. 控制流条件、循环与范围4.1 if表达式没有三元运算符的替代方案Rust的if是一种表达式不是传统意义上的语句。条件必须是bool类型不会像C语言那样把非零数字自动当作真值。let number 7; if number 5 { println!(condition was true); } else { println!(condition was false); } let size if number 10 { small } else { large };很多语言都有三元运算符condition ? a : bRust没有但它的if表达式完全可以替代这个需求。把if的结果直接赋给变量代码读起来反而比嵌套三元运算符更清晰。需要注意所有分支返回的类型必须一致这一点在编译期就会强制检查。如果你在一个分支里返回了数字另一个分支返回了字符串编译器会直接报 mismatched types 错误。4.2 loop、while与for三种循环怎么选Rust提供了三种循环loop无限循环、while条件循环、for遍历循环。let mut counter 0; let result loop { counter 1; if counter 10 { break counter * 2; } }; println!(result {result}); // 20 let mut n 3; while n 0 { println!({n}); n - 1; } let arr [10, 20, 30, 40, 50]; for element in arr { println!({element}); }loop配合break可以返回一个值这在需要“循环到某个条件时取出计算结果”的场景非常有用。while适合条件不固定、在运行过程中变化的循环。但日常写代码最推荐的还是for因为它直接作用于集合或范围天然避免了索引越界的问题。范围表达式1..4是左闭右开区间只能取到1、2、3如果写成1..4则是闭区间取到4。这个边界细节非常容易记混我建议你在写遍历时先想清楚下标边界再选用哪种写法。4.3 循环标签与break返回值进阶用法当你遇到嵌套循环需要从内层循环直接跳出外层循环时可以用循环标签outer: for i in 1..5 { for j in 1..5 { if i * j 10 { break outer; } println!(i {i}, j {j}); } }这个特性在处理矩阵搜索、二维数组遍历时特别有用能省去一堆布尔标志位的麻烦。break除了能跳出指定循环还能携带返回值我前面已经演示过。两者结合你甚至可以在跳出多层循环的同时带回一个结果值这在其他语言里通常要写额外的变量或者直接抛异常才能实现。Rust用表达式思维把这件事做得很干净也没有额外的运行时成本。5. 通用概念里埋下的伏笔所有权、借用与生命周期5.1 变量作用域所有权规则的“预备课”学完通用概念后回看你会发现很多前期细节都是在为所有权系统做铺垫。变量作用域就是最直接的一个例子。在Rust里每个变量只在特定范围内有效超出作用域就会被自动清理。fn main() { { let s String::from(hello); println!({s}); } // 这里s被销毁内存被释放 // println!({s}); // 编译错误s不在作用域内 }很多语言都有作用域的概念但Rust的不同之处在于它把“离开作用域”和“释放内存”明确关联起来并且这一行为在编译期就能确定。这就是所有权规则的第一课每个值都有且只有一个所有者所有者离开作用域值就被清理。有了这个基础后面学习变量赋值、函数传参时的所有权转移就不会太突兀。5.2 引用与参数传递借用检查的前奏在通用概念阶段函数参数传递是值传递一个变量传入函数后发生了什么这个问题藏着一个大坑。对于i32这类简单类型传入函数相当于复制了一份原变量不受影响但对于String、Vec这类复杂类型传入函数会把所有权移进去原变量就不再可用——这就是所有权的移动语义。fn main() { let s String::from(hello); takes_ownership(s); // println!({s}); // 编译错误所有权已转移s不可用 } fn takes_ownership(s: String) { println!({s}); }想在调用后继续使用原值就得用引用也就是借用。借用不转移所有权只是暂时把数据“借”给函数用。这就是后面要重点讲的借用检查器发挥作用的地方。Rust内置的借用检查规则会在编译期保证两点同一时刻不能有多个可变借用不能同时存在可变借用和不可变借用。这种严格的规则在初期会带来一些挫败感但一旦适应你会发现它能消灭一整类数据竞争问题。5.3 为什么Rust在嵌入式与异步领域越来越火聊到热门话题Rust这几年在嵌入式开发、异步编程、系统工具领域的热度确实一路走高。搜“esp32 rust开发”“rust async”这类内容的朋友也越来越多背后原因其实和通用概念阶段学到的东西一脉相承。Rust无垃圾回收器内存安全靠编译期的规则保障这意味着它能做到和C/C类似的运行时性能同时又不会有悬垂指针、野指针这些幽灵般的问题。在嵌入式场景里内存只有几十KB一切资源都紧张Rust的零成本抽象和精细控制能力就特别有优势。在异步场景里Rust的futures模型兼顾高并发和内存安全服务端框架的性能表现也相当亮眼。回到学习路径上讲先把这个阶段的基础概念抓牢后面无论是深入系统编程还是转向嵌入式、做网络服务都不至于被语法之外的概念卡住。6. 实操环境与常见问题速查6.1 开发环境搭建与调试建议如果你是第一次接触Rust推荐直接使用官方推荐的rustup工具链管理器来安装。装完后会自带cargo它是Rust的构建系统和包管理器类似Node生态里的npm。新建项目只需要一条命令cargo new my_project cd my_project cargo run写代码我推荐VS Code配合 rust-analyzer 插件这个插件提供补全、跳转、类型信息、编译错误内联提示对学习阶段的帮助非常大。想调试的话再装一个 CodeLLDB 插件就可以在VS Code里打断点、看变量体验和IDE调试差不多。很多教程提到“rust vscode 如何调试”其实就是这两个插件配合起来用不需要额外安装复杂的调试环境。6.2 新手最容易踩的5个编译错误我把自己和身边朋友初学阶段最常遇到的编译错误整理成了表格当你看到类似报错时可以直接对照编译错误提示截取关键部分常见原因解决办法cannot assign twice to immutable variable尝试对不可变变量重新赋值给变量加mut或用let重新绑定expectedi32, found()函数体末尾表达式多了分号返回了单元类型去掉表达式末尾末尾的分号mismatched typesif分支返回值类型不一致或函数返回类型不一致确保各分支返回类型统一this operation will panic at runtime数组越界访问、整数溢出等检查索引范围与数值范围必要时用get()方法expectedbool, found integerif条件写成了数字Rust不会自动转换真值改为比较表达式如if x ! 06.3 通用概念阶段的学习方法建议学完变量、类型、函数、控制流你已经掌握了用Rust写“串行小工具”的全部语法基础。我强烈建议你在这个阶段不要急着往下冲而是把每章后面的小练习动手做一遍。比如实现一个摄氏温度与华氏温度的互转函数、生成第n个斐波那契数、打印圣诞歌的循环歌词。这些题目看起来简单却能帮你把变量遮蔽、表达式返回值、循环控制这些知识点真正揉进肌肉记忆里。有个具体的学习技巧是故意“破坏”代码把mut去掉、把表达式加上分号、把循环条件改错观察编译器的报错信息。Rust的编译器提示做得业界顶级你多读几遍报错会越来越习惯它的表达方式。别怕报错报错是编译器在帮你排雷。遇到看不懂的错误就逐一拆解错误信息中的关键术语再回翻本文对应的知识点这样积累的知识比看十遍教程都扎实。学到这里我对Rust最大的感受是它把很多其他语言里“约定俗成、容易出问题”的东西变成了编译器强制执行的规则。写Rust前期确实会觉得缚手缚脚但一旦你领会了这套“先用规则保护你再给你发挥空间”的设计哲学再看其他语言写出的代码反而会本能地去找那些潜在的内存和并发隐患。后面开始啃所有权、引用和切片时带着这套通用概念的底子你会发现一切推进得比想象中顺利很多。
返回列表