写过 C# 或 Java 的开发者大概都有这样一段经历:想让某个变量保存条件判断的结果,得先在外面把它声明出来,再在每个分支内部逐个赋值,最后才拿去使用。次数多了自然觉得繁琐,不过多数人默认语言本来就该如此。Rust 的做法是把这类冗余写法直接去掉——判断语句自身就可以带回一个结果。这并非单纯的语法糖,它依托的是一条更底层的规则:在 Rust 里,能够产生值的位置远比想象中多。
if 分支自带返回值,能产值的位置到处都有
像 C#、Java 那样“先声明、分支各赋一次、随后使用”的三步曲,在 Rust 中被压缩成一个赋值动作:let max = if a > b { a } else { b };。花括号内最后那个表达式的求值结果,就构成了整个 if 的值。
与之相对,C# 依靠三目运算符 a > b ? a : b 完成同类工作,Go 干脆没有这种写法;Rust 认为既然整个 if 都能当值使用,三目运算符便没有存在必要了。{这是原文中"C# 有三目运算符 a > b ? a : b,Go 干脆没有;Rust 的答案是整个 if 都能产值,所以三目运算符在这里不需要存在"的信息,改写如前}
代价是编译器要额外把关:某个 if 若不作为值使用,各分支类型可以不同;一旦拿来赋值,两边的类型就必须完全对齐,一边 i32 一边 &str 会直接编译报错,因为被赋值的那个名字总要有确定类型。在 Rust 看来,这层限制反而能帮上忙——分支返回类型不一致,往往提示这段逻辑到了该拆分的时候。
else if 链可以照常书写,只是用作取值时,每一层都得有 else 作为兜底。否则整个表达式的类型无法确定,编译器不会允许你把这种结果放进变量。
函数签名必须写明类型,没有重载与默认参数
Rust 的函数,入参与返回值都得标出类型,例如 fn add(a: i32, b: i32) -> i32 { a + b }。参数类型不能省略,函数签名就是给调用方查看的契约,编译器不会替开发者去猜。返回值用 -> 表示,跟在参数表后面;函数体最末一行若没有分号,它的值就会被送回调用方,这一点的机理在下一节说明。
从 C# 迁移过来的人会察觉缺了不少东西:没有函数重载、没有默认参数、没有可选参数、也没有 params。若确实需要“多种版本”,常见做法是改用不同名字(标准库里 new、with_capacity 等命名屡见不鲜),或者动用泛型。作者认为原因绝非省事:一个函数名背地里挂着五六套签名,会让调用点长得一模一样却行为各不相同,而这恰恰是最难排查的 Bug 温床之一。
另有一条规则:函数接触不到自身之外的变量。C# 的方法能直接读取所属类的字段,Rust 的函数只认通过参数传进来的数据。写起来是麻烦一些,可换来的是“这个函数用到什么”可以一眼看完,隐含依赖被排除在外。
分号是这个模型里最关键的那个字符
常见的函数体和它的行为示例如下——let x = 5;、let y = x + 1; 都是以分号收尾;而 let z = x + 1 这种无分号的写法,只在块的最末尾才有合法地位。
x + 1 本身是一个表达式,类型为 i32;一旦补上分号,它就变成了一条语句,类型随之成为 (),发音为 unit,含义是“什么都没有”。
可以说,Rust 的分号承担了一次返回类型上的重定义:把 T 变为 ()。这一点直接框定了返回值惯例。例如 fn square(n: i32) -> i32 { n * n },末尾没有分号的表达式就是输出值;换成 fn greet() { println!("hi"); },因为里面全是带分号的语句,就会自带 () 作为返回值,当然这两个例子的返回值形态本来就不同。
return 在这里并不常见,它主要用于中途抢先收场,比如参数不合规时提前给出结果。
初学者翻车最频繁的地方就在这个多打出的分号上:若写成为 fn square(n: i32) -> i32 { n * n; },编译器会抛出一条 error[E0308],内容为 mismatched types,并提示说需要一个 i32 却发现了 (),同时还直接标出该分号该去掉的建议范围。整段诊断信息连续阅读才明白是在说错误位置定位与处理方法,原文章节记录的该类教训在这里立刻取得实例。
大括号块、临时集合与各级循环都能带回结果
还有一个略显反直觉的设置:被大括号包裹的代码块也是一个表达式。let value = { let a = 10; let b = 20; a + b }; 执行之后 value 得到 30。块内可以完成任意多步操作,末一位表达式的计算结果充当整个代码块的价值。这不是为了展示本领,而是说给变量安排初始值这件事本身,被允许寄托在任意复杂的运算之上,不再需要“先声明再另赋值”的旧三步走。
其中最简单的循环是 while,其判定条件为真就持续轮转,例如 let mut n = 3; while n > 0 { … n -= 1;} 这样的结构。
最常见的当属使用 for 进行的遍历。写 for i in 0..5 游走 0、1、2、3:不包含 5;改用 for i in 0..=5 就能把 5 放进范围。
0..5 自身是一个 Range,可以被保存在变量里一能作为参数传递:let range = 0..5; for i in range { … },如此即可遍历被“暂时赋名”的区间,而不必在第一行循环里再写一遍起止数字。
循环结构中出现了一个极其典型的错误归集后进入判断:loop 形式的循环结构,比较可行的状态出发的做法总结在这里处理掉它。语法上唯一独到的地方就是:如果有;中间无法退场效果本身属于结构性判断的目的,依然可能无返回;但这第一种汇总入口携带值回收这一专长。
loop 中间可以有机会,break count * 2;;设如此,值 20 就会显现。对外部的认识指向了一席之地:break 加在词句中的位置可以为代码带回标签,最终依据语言特性从 C# 角度看出普通循环得先依靠一个辅助储存位。但通过它们承载循环带值的招式上边有地方实例从:把一层中间各层的 'outer 接续到外层上方面貌其终结,自己跳下去然后循环 let max = if … { n; n; };它显示把总和封在一个环绕结构的上方时怎么落地。'outer 会被无条件宣告在循环基点上通过(例子 let found = 'outer: loop { … break 'outer n; … });拿一个具名的短牌自动一步直接冲断的闭环状写法,总比在 C# 里取用标志位或书写 goto 要清爽利落。
小结:一体化编程契约靠函数签名、分号形态、块与循环一气贯通
把这些散点连缀在一起,可以看到 Rust 的几处彼此契合的特征:
其一、在行为边界的约束方式上,必须要点明某一语言的标示手笔来自确定:签名要带类型写法,同时也没有重载、默认参数之类的替代口供,以及不能暗中检查对外部变量的引用状况;那些由各中名称差别而各自替代得加单独对待的项目一律约束进来;更不会偷看函数外部的任何变量。
其二、分号的效应如此单向:将原来代表其中状态的、等同于赋值用的、起读动作的语式改编成 from T → () 的一类结论;最末给出的表达式形态也不一样化约。
其三、块与条件 if 都有产出使用物件的表现(角色具足于所有有关位置的层面),可能的岔路意味着每一成分之间必须给定逻辑上不会跳返单边布局;若要拿出“什么”输入到哪里就顺畅很多。
其四、那三位循环(while,以及紧随 Range 的 for,与用来循环特有标记出结果的 loop,配可以穿孔跨跳的标记)有一套谱系的差异层次操作可供自由变化。
这个作品很基础,用一个筛选质数的代码例子就能汇总上面所提到:用 function 判断质数、怎样写入 return 进行功能阻断提前;用 while i * i <= n 的 loop 携带将结果回收自己试循环的主体当初始化候选值等于 50,出断在“第一个大于 50 的质数”跳出。而当需要将集合式逐一贴标签时,正如将某条 if 产什标示结果那般的结论 2-20 就是那段:“为所有 2..20 以内号码书写质数、合数标记,然后输出”!若实际运行,那首条能懂信息定格在 “第一个超过 50 的质数是 53”。
小结:三条主线归位——严格以占类型契约制导、确定的分号把表现限制摆在恰当的缝隙里、在大括号块和条件或者三分中任何可为态表达式;都在服务同一个目标:使 Rust 的代码逻辑在逐级演算获得某一数值时得以建立主干流程位置,然后再因为最后就总得被认真放置一遍的这一连串事件自然迈向它所展现状态的一条后续:用 let 放落结果之际的那定终点的接受事件究竟给出了什么定格,是下一步要谈起的问题缘起。
