RUST 图解 第 3 章:通用编程概念(Common Programming Concepts) 第 3 章通用编程概念Common Programming Concepts章节概要本章涵盖变量与可变性、标量与复合数据类型、函数定义、注释规范以及控制流if/loop/while/for等通用编程概念。本章图解目录3.1 变量与可变性Variables and Mutability3.2 数据类型Data Types3.3 函数Functions3.4 注释Comments3.5 控制流Control Flow正文内容Concepts本章涵盖了⼏乎在所有现代编程语⾔中都会出现的概念以及它们在 Rust 中的具体⼯作⽅式。虽然这些概念本身并⾮ Rust 所独有但我们将⽴⾜于 Rust 的语境来讨论它们并阐述围绕它们的使⽤惯例。具体来说本章将探讨变量与可变性Variables and Mutability数据类型Data Types函数Functions注释Comments控制流Control Flow关键字Keywords与其他语⾔类似Rust 拥有保留给语⾔⾃身使⽤的关键字集合例如 let、fn、mut 等。你不能将这些保留字⽤作变量名或函数名。变量与可变性Variables and Mutability正如在第 2 章提到的在 Rust 中变量默认是不可变的immutable。这是 Rust 给予你的众多提示之⼀引导你写出能充分利⽤ Rust 提供的安全性与简便并发优势的代码。当然你依然可以选择将变量声明为可变的。当⼀个变量是不可变的时⼀旦⼀个值绑定到⼀个名字上你就不能再修改该值。为了直观体会这⼀点使⽤ Cargo 新建⼀个名为 variables 的项⽬cargo new variablesBASHcd variables将 src/main.rs 的内容替换为以下代码这段代码⽬前⽆法通过编译// src/main.rs RUSTfnmain(){letx5;println!(The value of x is: {x});x6;println!(The value of x is: {x});}运⾏ cargo run编译器会输出不可变错误信息cargo runPLAINTEXTCompiling variables v0.1.0 (file:///projects/variables)error[E0384]: cannot assign twice to immutable variablex– src/main.rs:4:5|2|letx5;| - first assignment tox3|println!(The value of x is: {x});4|x6;| ^^^^^ cannot assign twice to immutable variablehelp: consider making this binding mutable|2|letmutx5;| 这个例⼦展示了编译器如何协助你发现程序中的隐患。错误信息明确指出不能对不可变变量 x 进⾏⼆次赋值。在Rust 中获得这类编译期错误⾄关重要因为“意外修改本以为不会变的值”是许多隐蔽 Bug 的根源。Rust 编译器保证当你声明⼀个值不会变时它就绝对不会改变因此你⽆需⾃⼰在脑海中追踪它的状态变化。但在某些场景下可变性可以让代码编写更便利。只需在变量名前加上mut关键字即可将其声明为可变变量// src/main.rs RUSTfnmain(){letmutx5;println!(The value of x is: {x});x6;println!(The value of x is: {x});}运⾏后成功输出The value of x is: 5 PLAINTEXTThe value of x is: 6加上 mut 还向代码未来的阅读者清晰传达了意图代码的其他部分将会改变这个变量的值。常量Declaring Constants类似于不可变变量常量constants也是绑定到名称上且不允许修改的值。但常量与不可变变量有⼏点核⼼区别常量不能使⽤mut常量不仅默认不可变它们永远不可变。声明⽅式与类型标注声明常量使⽤const关键字⽽⾮ let并且必须显式标注类型。作⽤域灵活常量可以在任何作⽤域中声明包括全局作⽤域这使得它们⾮常适合在代码的多处共享使⽤。编译期求值常量只能被设置为常量表达式constant expression⽽不能是只能在运⾏时计算得出的值。常量示例constTHREE_HOURS_IN_SECONDS:u3260*60*3;RUSTRust 约定常量的命名规范为全⼤写字⺟单词之间使⽤下划线隔开。编译器可以在编译期直接计算表达式 60 * 60*3的结果即10800这样书写既⽅便⼈类理解也便于后期维护。常量在程序运⾏的整个⽣命周期内在其声明的作⽤域中均有效。变量遮蔽Shadowing在第 2 章中你已经⻅识过你可以声明⼀个与之前变量同名的新变量。Rustaceans 称之为第⼀个变量被第⼆个变量遮蔽shadowed了。编译器在遇到该变量名时看到的将是第⼆个新变量直到它⾃身被再次遮蔽或其所在的作⽤域结束// src/main.rs RUSTfnmain(){letx5;letxx1;{letxx*2;println!(The value of x in the inner scope is: {x});}println!(The value of x is: {x});}运⾏后输出The value of x in the inner scope is: 12 PLAINTEXTThe value of x is: 6在内部花括号创建的作⽤域中第三个 let 语句再次遮蔽了 x将其值变为 12。离开内部作⽤域后内部的遮蔽结束外部的 x 恢复为 6。遮蔽Shadowing与可变变量mut的区别重新保持不可变遮蔽机制通过重复使⽤ let 关键字创建了⼀个全新的变量。我们可以对⼀个值进⾏⼏次变换计算但在这些变换结束后新变量依然保持不可变属性如果我们忘记写 let ⽽直接给它赋值编译器就会报错保护我们。可以改变变量类型因为遮蔽实际上是创建了新变量所以新变量可以拥有与旧变量不同的数据类型letspaces ;// 字符串类型str RUSTletspacesspaces.len();// 数字类型usize如果试图⽤ mut 来实现这⼀点则会报错letmutspaces ;RUSTspaces spaces.len(); // 编译报错⽆法修改变量本身的类型数据类型Data TypesRust 中的每⼀个值都有其特定的数据类型。Rust 是⼀⻔静态类型语⾔statically typed language这意味着它必须在编译期获知所有变量的类型。通常编译器能够根据值及其使⽤上下⽂⾃动推断类型。但在存在多种可能类型时例如从字符串使⽤ .parse()转换为数字必须显式给出类型注解letguess:u3242.parse().expect(Not a number!);RUST如果省略 : u32Rust 将⽆法确定解析为何种类型编译报错。Rust 的数据类型主要分为两⼤⼦集标量类型Scalar 和 复合类型Compound。标量类型Scalar Types标量类型代表单⼀的值。Rust 有 4 种主要的标量类型整数、浮点数、布尔值 和 字符。整数类型Integer Types整数是没有⼩数部分的数字。每个类型都有显式的⼤⼩分为有符号以 i 开头和⽆符号以 u 开头⻓度 有符号Signed ⽆符号Unsigned8-bit i8 u816-bit i16 u1632-bit i32默认类型 u3264-bit i64 u64128-bit i128 u128视系统架构⽽定 isize usize有符号类型采⽤⼆进制补码two’s complement表示存储范围为 -(2^(n-1)) 到 2^(n-1) - 1。⽆符号类型存储范围为 0 到 2^n - 1。isize 和 usize 的⼤⼩取决于⽬标运⾏平台的架构在 64 位系统上是 64 位在 32 位系统上是 32 位。它们的主要使⽤场景是对集合类型进⾏索引。默认整数类型Rust 的整型默认是i32它通常速度最快即使在 64 位系统上也表现优异。整数字⾯量书写⽅式⼗进制98_222允许下划线 _ 作为视觉分隔符⼗六进制0xff⼋进制0o77⼆进制0b1111_0000字节仅限 u8b’A’字⾯量还⽀持类型后缀例如 57u8。整数溢出Integer Overflow假设⼀个 u8 变量范围 0 到 255被赋予 256在 Debug 模式 下编译时Rust 包含溢出检查⼀旦发⽣溢出程序将在运⾏时 panic。在 Release 模式–release下编译时Rust 不包含导致 panic 的溢出检查。相反它执⾏⼆进制补码回绕wrapping256 变为 0257 变为 1。依赖溢出回绕⾏为被视为⼀种错误。若要显式处理可能的溢出可以使⽤标准库提供的特定⽅法wrapping_回绕、checked_发⽣溢出返回 None、overflowing_返回结果及指示是否溢出的布尔值和 saturating_饱和在最⼤/最⼩值。浮点数类型Floating-Point TypesRust 拥有两种浮点数原⽣类型均遵循 IEEE-754 标准f3232 位单精度f6464 位双精度默认类型因为在现代 CPU 上它的速度与 f32 相当但精度更⾼所有的浮点类型都是有符号的。letx2.0;// f64 RUSTlety:f323.0;// f32数值运算Numeric OperationsRust ⽀持基本的四则数学运算和取余、-、*、/、%。整数除法会向零截断truncate toward zero到最接近的整数例如 -5 / 3 的结果是 -1。布尔类型The Boolean Type布尔类型⽤bool表示⼤⼩为 1 个字节只有两个可能的值true 和 false。它主要⽤于条件控制流如 if 表达式。letttrue;RUSTletf:boolfalse;// 带有显式类型注解字符类型The Character Type字符类型⽤char表示使⽤单引号声明字符串字⾯量使⽤双引号letcz;RUSTletz:charℤ;letheart_eyed_cat;Rust 的 char 类型⼤⼩为 4 个字节表示⼀个 Unicode 标量值Unicode Scalar Value。它可以表示远多于ASCII 的字符包括拼⾳重⾳字⺟、中⽂、⽇⽂、韩⽂字符、emoji 表情符号以及零宽空格等。其取值范围从U0000 到 UD7FF以及 UE000 到 U10FFFF。复合类型Compound Types复合类型可以将多个值组合为⼀个类型。Rust 原⽣提供了两种基础复合类型元组Tuple 和 数组Array。元组类型The Tuple Type元组是将多个具有不同类型的值组合为⼀个复合类型的通⽤⽅式。元组具有固定⻓度⼀旦声明其⼤⼩不能增⻓或缩减。元组使⽤⼩括号包裹逗号分隔的值来创建lettup:(i32,f64,u8)(500,6.4,1);RUST提取元组中的值有两种⽅式模式匹配解构Destructuringlettup(500,6.4,1);RUSTlet(x,y,z)tup;println!(“The value of y is: {y}”); // 输出 6.4点号索引访问Dot notation使⽤ . 后跟索引值索引从 0 开始letfive_hundredtup.0;RUSTletsix_point_fourtup.1;letonetup.2;单元类型Unit Type没有任何值的元组有⼀个特殊名称叫 unit单元类型其值和类型均写作()。它表示空值或空的返回类型。如果表达式没有返回其他任何值它将隐式返回单元值 ()。2. 数组类型The Array Type数组也是多个值的集合但与元组不同数组中的每⼀个元素必须是相同的类型。Rust 中的数组同样具有固定⻓度。数组的值写在中括号内leta[1,2,3,4,5];RUST使⽤场景当希望数据被分配在栈stack上⽽不是堆heap上或者希望确保元素数量始终固定时数组⾮常有⽤例如⼀年中 12 个⽉份的名称。与 Vector 的区别标准库提供的 Vector动态数组允许在堆上动态伸缩如果不确定该⽤数组还是Vector通常应该选择 Vector将在第 8 章详述。数组类型注解与初始化语法类型写法为 [类型; ⻓度]例如 let a: [i32; 5] [1, 2, 3, 4, 5];。初始化包含相同初始值的数组let a [3; 5];等同于 let a [3, 3, 3, 3, 3];。数组元素访问与越界检查使⽤中括号及索引访问例如 a[0]。leta[1,2,3,4,5];RUSTletfirsta[0];letseconda[1];如果尝试访问越界的索引例如 a[10]Rust 会在运⾏时 panicindex out of bounds: the len is 5 but the index is 10 PLAINTEXT这是 Rust 内存安全原则 的体现。在许多底层语⾔如 C中越界访问不会被检查导致⾮法读取未分配或属于其他数据的内存缓冲区越界读 / buffer overread。Rust 通过在运⾏时进⾏边界检查并⽴即终⽌程序来避免这种内存安全漏洞。函数Functions在 Rust 中函数⽆处不在。使⽤fn关键字声明函数。Rust 代码约定使⽤ 蛇形命名法snake case 作为函数与变量名的规范⻛格所有字⺟均⼩写单词之间⽤下划线分隔。// src/main.rs RUSTfnmain(){println!(Hello, world!);another_function();}fnanother_function(){println!(Another function.);}Rust 不在乎你在哪⾥定义函数只要在调⽤者能看得⻅的作⽤域内定义了它即可定义在 main 之前或之后都可以。函数参数Parameters函数可以定义包含参数。在函数签名中必须显式声明每个参数的类型。这是 Rust 设计中的刻意决定强制参数类型注解意味着编译器⼏乎⽆需在其他地⽅推测函数意图并且能在发⽣不匹配时提供极其精准的错误提示。多个参数之间使⽤逗号隔开fnmain(){RUSTprint_labeled_measurement(5,h);}fnprint_labeled_measurement(value:i32,unit_label:char){println!(The measurement is: {value}{unit_label});}语句与表达式Statements and ExpressionsRust 是⼀⻔基于表达式expression-based的语⾔这是⼀个极其关键的概念区别语句Statements是执⾏某种动作并不返回值的指令。表达式Expressions进⾏计算并产⽣⼀个结果值。变量绑定 let y 6; 是⼀条语句。因为语句不返回值所以不能将 let 语句赋值给另⼀个变量在 C 或Ruby 中可以写 x y 6但在 Rust 中不⾏letx(lety6);// 编译报错 RUST表达式会计算出⼀个值。5 6 是⼀个求值为 11 的表达式。函数调⽤是表达式宏调⽤是表达式花括号创建的代码块 {} 也是表达式lety{RUSTletx3;x 1 // 注意结尾没有分号};println!(“The value of y is: {y}”); // y 的值是 4注意表达式末尾没有分号 如果你在表达式末尾加上了分号它就变成了语句语句不会返回值。具有返回值的函数Functions with Return Values函数可以向调⽤者返回值。在函数签名中我们在箭头-后声明返回值的类型。在 Rust 中函数的返回值等同于其函数体代码块中最后⼀个表达式的值。虽然可以使⽤ return 关键字显式提前返回但⼤多数惯⽤的 Rust 代码都是隐式返回最后⼀个表达式fnfive()-i32{RUST5 // 这是⼀个表达式作为返回值不要加分号}fnmain(){letxfive();println!(“The value of x is: {x}”); // 输出 5}再看⼀个示例fnplus_one(x:i32)-i32{RUSTx 1}如果你在 x 1 后⾯不⼩⼼加了⼀个分号 ;fnplus_one(x:i32)-i32{RUSTx 1; // 变成了⼀条语句}编译器会抛出类型不匹配错误error[E0308]: mismatched types PLAINTEXT– src/main.rs:7:24|7|fnplus_one(x:i32)-i32{| -------- ^^^ expectedi32, found()| || implicitly returns()as its body has no tail orreturnexpression8|x1;| - help: remove this semicolon to return this value因为语句不产⽣值代码块隐式返回了单元类型 ()与函数签名承诺的 i32 发⽣冲突编译器会贴⼼地建议你移除分号。注释Comments在 Rust 中惯⽤的单⾏注释以两条正斜杠 // 开始并⼀直持续到⾏尾// 这是⼀个独⽴的注释⾏ RUSTletlucky_number7;// 也可以位于代码⾏末尾对于跨越多⾏的注释每⾏均以 // 开头// 这是⼀段较为复杂的逻辑 RUST// 我们通过多⾏注释来详细解释// 具体的实现思路。此外Rust 还提供⽂档注释以 /// 开头这将在第 14 章详细讨论。控制流Control Flow控制代码执⾏流向的最常⻅结构是if表达式 和 循环loops。if 表达式if表达式允许根据条件分⽀执⾏代码// src/main.rs RUSTfnmain(){letnumber3;ifnumber5{println!(condition was true);}else{println!(condition was false);}}条件必须是bool类型与 JavaScript 或 Python 等语⾔不同Rust 不会⾃动尝试将⾮布尔类型转换为布尔值。如果写 if number {编译器会直接报错 expected bool, found integer。必须显式提供布尔条件例如 if number ! 0 {。使⽤ else if 处理多重条件fnmain(){RUSTletnumber6;ifnumber%40{println!(number is divisible by 4);}elseifnumber%30{println!(number is divisible by 3);}elseifnumber%20{println!(number is divisible by 2);}else{println!(number is not divisible by 4, 3, or 2);}}程序按顺序检查各个条件⼀旦发现第⼀个为 true 的分⽀就会执⾏对应分⽀的代码并跳出判断不会再去检查后续的分⽀即使 6 同样能被 2 整除。如果 else if 过多通常应重构为第 6 章的 match 结构。在 let 语句中使⽤ if因为 if 是⼀个表达式所以可以直接将其放在 let 语句等号右侧来为变量赋值fnmain(){RUSTletconditiontrue;letnumberifcondition{5}else{6};println!(“The value of number is: {number}”); // 输出 5}• 所有分⽀的返回类型必须相同变量必须有唯⼀定义的类型。如果写letnumberifcondition{5}else { “six” };编译将会失败因为 if 臂和 else 臂的返回类型不兼容integer 与 str。2. 循环Repetition with LoopsRust 提供了 3 种循环结构loop、while和for。loop循环loop关键字指示Rust⼀遍⼜⼀遍地反复执⾏代码块直到遇到中断或显式终⽌fnmain(){RUSTloop{println!(again!);}}终端可以通过快捷键 Ctrl-C 终⽌死循环。代码中可以通过break退出循环或通过continue跳过当前迭代直接进⼊下⼀次循环。从循环中返回值loop常⽤于需要重试可能失败的操作如轮询线程状态。你可以将需要传递给外部的结果值放在break之后返回fnmain(){RUSTletmutcounter0;letresultloop{counter1;ifcounter10{breakcounter*2;// 返回 20}};println!(“The result is {result}”); // 输出 20}循环标签Loop Labels消除多层嵌套歧义如果有嵌套的多层循环break 和 continue 默认只作⽤于最内层循环。你可以在循环前指定以单引号开头的循环标签loop label并结合 break/continue 精确跳出指定的外层循环fnmain(){RUSTletmutcount0;counting_up:loop{println!(count {count});letmutremaining10;loop{println!(remaining {remaining});ifremaining9{break; // 仅退出内层循环}ifcount2{breakcounting_up;// 退出外层 labeled 循环}remaining-1;}count1;}println!(End count {count});}while条件循环当需要在循环内判断条件时while 循环是内置的语法糖避免⼿动组合 loop、if、else 和 breakfnmain(){RUSTletmutnumber3;whilenumber!0{println!({number}!);number-1;}println!(LIFTOFF!!!);}for循环遍历集合虽然可以使⽤ while 结合下标索引来遍历数组或集合但这种做法容易出错例如索引写错导致越界 panic且运⾏时每次迭代都要做越界检查因此较慢。Rust 中更安全、简洁、⾼效且最常⽤的⽅式是for循环fnmain(){RUSTleta[10,20,30,40,50];forelementina{println!(the value is: {element});}}使⽤ for 循环彻底杜绝了越界或遗漏元素的可能同时编译⽣成的机器码效率更⾼。如果要实现指定次数的循环例如倒计时可以配合标准库的Range范围及其 .rev()反转⽅法fnmain(){RUSTfornumberin(1..4).rev(){println!({number}!);}println!(LIFTOFF!!!);}(1…4) ⽣成 1、2、3前闭后开区间.rev() 将其倒转为 3、2、1代码清晰且优雅。本章⼩结Summary本章内容⾮常充实你系统掌握了变量与可变性默认不可变、mut 声明可变、const 常量以及通过 shadowing 优雅转换数据。基本数据类型标量类型整数i32、溢出机制、浮点数f64、布尔型bool、字符char4 字节 Unicode。复合类型固定⻓度与异构的元组tuple、固定⻓度与同构的数组array 及其越界保护。函数与语义参数强类型注解深刻理解语句不返回值与表达式返回值的核⼼差异以及函数末尾⽆分号的隐式返回特性。4. 控制流强布尔类型的 if 表达式可赋值给 let以及 loop可带返回值与标签、while 和最惯⽤的for集合/区间遍历。练习建议尝试⾃⼰动⼿写⼏个⼩程序巩固这些知识摄⽒度与华⽒度温度互相转换。⽣成第 n 个斐波那契数。利⽤循环和重复特性打印圣诞颂歌《The Twelve Days of Christmas》的歌词。准备好后我们将在第 4 章迈⼊ Rust 独⼀⽆⼆的核⼼基⽯——所有权Ownership机制FF!!!);}(1..4) ⽣成 1、2、3前闭后开区间.rev() 将其倒转为 3、2、1代码清晰且优雅。 本章⼩结Summary 本章内容⾮常充实你系统掌握了 1. 变量与可变性默认不可变、mut 声明可变、const 常量以及通过 shadowing 优雅转换数据。 2. 基本数据类型 - 标量类型整数i32、溢出机制、浮点数f64、布尔型bool、字符char4 字节 Unicode。 - 复合类型固定⻓度与异构的元组tuple、固定⻓度与同构的数组array 及其越界保护。 3. 函数与语义参数强类型注解深刻理解语句不返回值与表达式返回值的核⼼差异以及函数末尾 ⽆分号的隐式返回特性。 4. 控制流强布尔类型的 if 表达式可赋值给 let以及 loop可带返回值与标签、while 和最惯⽤的 rust for 集合/区间遍历。练习建议尝试⾃⼰动⼿写⼏个⼩程序巩固这些知识摄⽒度与华⽒度温度互相转换。⽣成第 n 个斐波那契数。利⽤循环和重复特性打印圣诞颂歌《The Twelve Days of Christmas》的歌词。准备好后我们将在第 4 章迈⼊ Rust 独⼀⽆⼆的核⼼基⽯——所有权Ownership机制