施工中
函数 functions
函数由名称, 参数, 返回类型, 函数体四部分构成, 用于通过函数体所描述的内容处理某些参数并 (可选地) 返回某个类型:
fn function_name(arguments) -> ReturnType {
function_body
}
fn simple_function() {
function_body
}
fn f(){}完整函数恰如 function_name 函数所述; 参数列表和返回值类型并非必须, 如 simple_function; Rust 中允许的最小函数如 f.
未使用的函数/变量
如果直接复制粘贴上述代码, Rust 语法检查器会警告称这几个函数未被使用. 如果刻意如此不使用某个函数或变量, 可在其名称开头加个下划线 _ 以避免警告:
fn _unused_function(...) -> ...{
...
}最简例子
下面引入一个最简的函数示例:
fn main() {
println!("hello world");
another_function();
}
fn another_function() {
println!("Another function");
}预期运行结果:
hello world
Another function上面的例子定义了一个不接收任何参数也不返回任何内容的函数 another_function, 作用在函数体里阐明: 打印文本 Another function.
命名规范
这里提一嘴 Rust 中函数名与变量名的命名规范, snake_case:
- 全部字母小写;
- 单词间用下划线连接.
参数
函数常需处理某些内容, 此时以函数参数的形式传入函数. 如下:
fn main() {
println!("hello world");
// another_function(); // 错误: 要求传入 1 个参数, 但传入了 0 个参数.
let x = 0;
another_function(x);
another_function(x + 1);
// vvvvv 下面的内容稍超纲 vvvvvv
let x: i32 = {
// .parse() 解析方法必须知道需要解析为什么类型, 从而不可推断. 此处选择指定为 **i32**.
let mut x = String::new();
// .read_line() 方法接收字符串的可变借用, 因此此处定义一个字符串.
// 同时请留意变量遮蔽与作用域.
io::stdin().read_line(&mut x).unwrap();
// 采用了 std::io 标准库的 .stdin() 方法用于经由控制台输入修改字符串内容.
// 标准库需要手动引入, 如何引入请参考代码检查器提示.
// read_line() 可能失败, 返回的是 Result<T, E> 枚举, 需要处理,
// 此处直接用 .unwrap() 偷懒处理, 业务逻辑中禁用.
x.trim().parse().unwrap()
// 通过写入, x 已经转为目标内容, 但我们需要 **i32** 类型的 x,
// 此处调用 .trim() 清除字符串前后的空白字符,
// 然后用 .parse() 解析为我们已经指定的 **i32** 类型.
// .parse() 同样可能失败, 也用 .unwrap() 临时处理.
// 注意末尾不使用分号关闭, 而这是该块的最后一个表达式,
// 从而该块返回这一表达式的值, 也就是被解析为 **i32** 的用户输入值.
};
// ^^^^^ 上面的内容稍超纲 ^^^^^
another_function(x);
}
fn another_function(x: i32) {
println!("get number: {}", x)
}预期输出
get number: 0
get number: 1
get number: 你输入的数这里的 another_function 接受一个 i32 类型的参数, 并将其打印出来. 可见函数定义一次即可多次使用, 每次调用时, 吃下某些东西, 改变某些东西, 返回某些东西.
TIP
上面所谓的超纲部分就是控制台输入一个数. 在 C 语言中的等价表述:
// #include <stdio.h>
int main() {
int a;
scanf("%d", &a);
printf("%d", a);
// return 0;
}显然 C 语言简洁得多 (用 C++ 也同样简洁). 实际上 Rust 的繁琐强制程序员主动处理了这些可能的隐患, 这却是 C 开发者尤其是 C 开发团队 (其中有若干新手的情况下) 所困扰的问题.
声明函数参数时必须指定参数的类型, 而传入多个参数则用逗号隔开:
fn foo(arg: Type, arg1: Type1) {...}语句和表达式, 作用域
我看到的教程:
语句以
;结尾, 没有返回值; 表达式不以;结尾, 有返回值.
实际上以 ; 结尾无非返回空类型 () 罢了:
let a = {0;}; // a: ()
let b = {0}; // b: i32
let c = 0; // c: i32
let d = 0;; // d: i32可能会尝试如 d 那样达到效果, 但实际上由于符号优先级, 第二个分号会被视作一个空语句, 与前面的赋值语句无关, 也就是等价于
let d = 0;
;希望将分号结尾的语句打包绑定给变量, 如 a 那样用作用域包裹起来即可.
作用域还牵涉到生命周期: 某一作用域定义的变量将在该作用域离开时自动销毁. 请看:
fn main() {
let k = 1;
println!("{k}");
let k = {
println!("{k}");
let k = String::from("hello");
println!("{k}");
let k = {
println!("{k}");
let k = (1.1, (2.2, {
let k = 3;
(k as f64) * 1.1
}));
println!("{:?}", k);
"aaa"
};
println!("{k}");
true
};
println!("{k}");
}
// println!("{k}"); // 错误: 此处无 k 之定义, 亦本就不可达.预期输出
1
1
hello
hello
(1.1, (2.2, 3.3000000000000003))
aaa
true无非一句话: 语句是以 ; 结尾的表达式, 返回空类型.
函数的返回值
前文所定义的函数都返回空类型, 函数
fn foo(args..) {...}本质乃语法糖:
fn foo(args..) -> () {...}其中的 -> () 即表示返回空类型.
那么如果我们希望整个函数返回其他类型呢? 同样使用 -> 来声明, 下面以 OutputType 代替具体返回类型:
fn foo(args..) -> OutputType {...}举例:
fn main() {
let x = "hello";
println!("{}", get_str(x));
}
fn get_str(input: &str) -> String {
String::from("Got Number that ") + input
}预期输出:
Got Number that hello+ 号
注意上文中能向 String 类型加 &str 类型, 是因为运算符 + 有一个在 (String, &str, String) 上的重载, 允许字符串类型变量右乘一个字符串切片引用类型变量, 并返回一个字符串类型变量.
交换律
有基本的数学功底应该理解, 交换律 并非天然存在. 举例, 对于普通三阶魔方, 记右面顺时针旋转为 , 上面顺时针旋转为 , 并定义两个操作顺序执行为乘法, 那么显然, , 不满足交换律.
此外, 二元运算也未必基于同一集合, 我们熟知的实数加法被定义为函数 , 但一些运算并非如此, 例如在域 上的向量空间 有数乘运算.
就是非对称二元运算.
一个函数体中, 其最后一个表达式视作其返回值, 如果类型与函数标签所指定的类型不匹配则报错:
fn foo() -> i32 {
"hello" // !! 类型不匹配!
}有时需要在函数体的中间提前返回内容, 可以调用 return 关键字:
fn foo() -> i32 {
// 干些啥
if ...{
return 1;
}
// 干些啥
0
}控制流
if关键字
if some_boolean_expr {
...
}
else if some_boolean_expr2 { // optional
...
}
else if some_boolean_expr3 { // optional
}
else { // optional
...
}学过其他语言的话, 这老三套并无什么需要过多介绍的, 无非如果则如何, 否则又如果则如何, 最后否则如何. 需要说明的点:
多个
else if可连用.用于判断的表达式必须返回 Bool 类型, 注意 i32 类型的数字
1从不会自动转换为True.单个分支必须为
if+条件表达式+执行语句块, 而语句块必须包裹以一对大括号{}. 换言之,rustif some_boolean_expr do something;这种 C/C++ 里的语法糖, Rust 不允许存在.
Rust 万物皆表达式,
if也不例外, 因此可以如此操作:rustlet x = if some_boolean_expr { "hello" } else { "world" };三目运算符
显然上例起到了一些语言中三目运算符的作用:
cx = someBooleanExpr ? "hello" : "world"; // 标准形式 => condition ? expr1 : expr 2该运算符很容易写出令人费解的屎山, 又不同于
goto能够用更高级的抽象轻松规避.类 C 语言, 包括 C/C++/C, 以及 Java, JavaScript, 均拥有上述经典三目运算符. 其中 JavaScript 允许嵌套, 而 C 要求两个分支仅需满足至少可隐式转换.
函数式语言及多范式语言的函数式范式的处理通常更现代些.
其中, Python 的解决方案是一种奇怪的语法:
Pythonx = "hello" if someBooleanExpr else "world"虽说号称更贴近自然语言, 但这其实是社区在千禧年前后几年左右脑互搏之后的成果, 未必最优, 因为并非绝大多数 Pythoner 赞成这一语法.
Scala, Kotlin, Rust 这些习惯于万物皆表达式的语言均支持直接以
if-else控制流表三目之意:Kotlinval x = if (a > b) a else bRuby 的
if表达式表达能力不弱, 但保留了传统的三目运算符:rubyx = if someBooleanExpr then "hello" else "world" end # 或者传统写法 x = someBooleanExpr ? "hello" : "world"一些表达能力稍弱的语言会考虑变通处理, 例如没有三目时的 Python 和 Lua:
Luax = (someBooleanExpr) and "hello" or "world"这种写法需要保证第一个分支的内容不是布尔值
False, 否则会出错. Lua 往往采用更安全的写法:Luafunction ternary(condition, T, F) if condition then return T else return F end end max = ternary(someCondition, "hello", "world")也有一些现代语言选择更激进地处理. 例如 Go 语言明确不支持三目运算符, 并鼓励完整使用
if控制流:Govar x string if someBooleanExpr { x = "hello" } else { x = "world" }据 Go 语言官方自称:
The reason ?: is absent from Go is that the language’s designers had seen the operation used too often to create impenetrably complex expressions. The if-else form, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.
大抵是 Go 团队所坚称的简洁性所致. 这并非什么好事, 至少 Go 团队在不想添加泛型支持一事上败给了社区. 个人认为极致简洁还不如去写 Brainfuck:
brainfuck这串鬼画符一定程度上也算实现了三目运算 , > , [ < [ - > > [ - < + > ] < < ] > [ - < < + > > ] < [ - < < + + + + + + [ - < + + + + + + > ] > > ] < ]Zig 则与 Rust 相近, 支持语句块返回值, 并要求不同分支类型一致:
zigconst x = if (someBooleanExpr) blk: { ... break :blk "hello"; } else "world";
分支返回值
所有分支的返回值必须为同一类型, 因为 Rust 是静态类型语言, 设计中预期所有变量的类型在编译期确定, 不一致的类型将导致特定变量的类型确定推迟到运行期:
let x = if true {
"hello" // &str
} else {
42 // i32
}这样的写法会导致编译器报错.
进一步说明
在动态类型语言 Python 中, 上述逻辑完全可行:
x = "hello" if True else 42那么 Rust 如果确实遇到此类需求该怎么办呢? 我们之后会介绍到, 考虑定义所有可能返回类型构成的枚举类型:
enum MyValue<'a> {
Text(&'a str),
Number(i32),
}
fn main() {
...
let x = if true {
MyValue::Text("Hello")
} else {
MyValue::Number(42)
};
}其中用到了生命周期描述符 <'a>, 用于处理引用类型 &str 的生命周期检查问题.
循环
loop 关键字
最基础的循环, 其他循环基于它实现.
fn main() {
let x = 1;
loop {
println!("{}", x);
println!("{}", " ");
}
}预期不加停止地持续输出:
1
无限循环固然在某些时候有用, 但显然我们暂时不需要, 须通过一些手段控制循环结束, 也就是 break 关键字:
fn main() {
let mut x = 0;
loop {
println!("{}", x);
x = x + 1;
if x == 42 {
break;
}
}
println!("打印完毕!");
}预期输出:
1
2
...
40
41
打印完毕!而有时候我们不需要直接嘎掉整个循环体, 而仅仅是希望跃过某次循环的剩余部分直接进入下一循环, 那么就采用 continue 关键字:
fn main() {
let mut 待定数 = 2;
let mut 素数计数 = 0;
loop {
if 待定数 == 1000 {
break;
}
if !是素数(待定数) {
待定数 = 待定数 + 1;
continue;
}
素数计数 = 素数计数 + 1;
println!("{待定数} 是素数!");
待定数 = 待定数 + 1;
}
}
fn 是素数(i32参数: i32) -> bool {
if i32参数 <= 1 {
return false;
}
let 优化上限 = (i32参数 as f64).sqrt() as i32;
// 转成浮点开根号再转回整形.
let mut 迭代索引 = 2;
loop {
if 迭代索引 > 优化上限 {
break;
}
if i32参数 % 迭代索引 == 0 {
return false;
}
迭代索引 = 迭代索引 + 1;
}
true
}为合数, 当且仅当存在 使得 .
证明细节
- (). 若 为合数, 则存在 , 使得 .
按题, 存在整数 , 使得
而若 , 则
与 矛盾, 故而必有:
- () 若存在 使得 , 则 为合数. 显然.
上述代码预期依次输出所有 中的素数.
此外作为万物皆表达式之一部分, loop 同样允许存在非空返回值, 通过 break <表达式>; 完成, 当然, 同样需要在所有分支返回同一类型:
fn main() {
let mut 待定数 = 2;
let mut 素数计数 = 0;
println!("总计素数: {}", loop {
if 待定数 == 1000 {
// vvvv 更改 vvvv
break 素数计数;
// ^^^^ 更改 ^^^^
}
if !是素数(待定数) {
待定数 = 待定数 + 1;
continue;
}
素数计数 = 素数计数 + 1;
待定数 = 待定数 + 1;
});
}
fn 是素数(i32参数: i32) -> bool {
if i32参数 <= 1 {
return false;
}
let 优化上限 = (i32参数 as f64).sqrt() as i32;
let mut 迭代索引 = 2;
loop {
if 迭代索引 > 优化上限 {
break;
}
if i32参数 % 迭代索引 == 0 {
return false;
}
迭代索引 = 迭代索引 + 1;
}
true
}预期输出
总计素数: 168while 关键字
while 布尔表达式 {
...
}很简明, 本质等价于
loop {
if 布尔表达式 {
break;
}
...
}不允许返回值.
for 关键字
语法如下:
for element in set.iterator {...}上文代码可通过 for 简化, 并去掉无用逻辑, 如下:
fn main() {
for i in 2..=1000{
if 是素数(i) {
println!{"{i} 是素数!"}
}
}
}
fn 是素数(i32参数: i32) -> bool {
let 优化上限 = (i32参数 as f64).sqrt() as i32;
for i in 2..=优化上限 {
if i32参数 % i == 0 {
return false;
}
}
true
}其中, 2..=1000 本质为语法糖, 创建了一个 i32 从 到 的迭代器, 末尾的 算在内. 实际上亦有不算在内的语法:
for i in 2..<1001 { ... }底层实现
for 循环语法本质仍为 loop 的语法糖:
for element in set.iter() {
println!("{element}");
}本质展开为
{
// 迭代器 --> 迭代器实例
let mut iter = IntoIterator::into_iter(set.iterator());
// loop 循环
loop {
// 调用 .next() 并匹配结果, 目的是在迭代结束时 break
let element = match iter.next() {
Some(element) => element,
None => break,
}
// 循环体, 此处代码如下
println!("{element}");
}
}深层 break
有时需要嵌套使用循环语句, 而又需要从内层循环直接跳出外层循环. 如果采用普通语法, 将显得极其臃肿而不优雅, 此时可以为循环添加标签:
// 查找二维数组中的特定值
let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
let target = 5;
'o: for i in 0..3 {
for j in 0..3 {
if matrix[i][j] == target {
println!("Found at ({}, {})", i, j);
break 'o; // 直接跳出两层循环
}
}
}其中的 break 'o 即跳出被标签 'o 标记的循环, loop, while, for 均可如此标记. 对于允许直接 break 出返回值的 loop, 如果同时需要如此, 语法为
break 'o out_val;