Skip to content

施工中

函数 functions

函数由名称, 参数, 返回类型, 函数体四部分构成, 用于通过函数体所描述的内容处理某些参数并 (可选地) 返回某个类型:

rust
fn function_name(arguments) -> ReturnType {
    function_body
}
fn simple_function() {
    function_body
}
fn f(){}

完整函数恰如 function_name 函数所述; 参数列表和返回值类型并非必须, 如 simple_function; Rust 中允许的最小函数如 f.

未使用的函数/变量

如果直接复制粘贴上述代码, Rust 语法检查器会警告称这几个函数未被使用. 如果刻意如此不使用某个函数或变量, 可在其名称开头加个下划线 _ 以避免警告:

rust
fn _unused_function(...) -> ...{
    ...
}

最简例子

下面引入一个最简的函数示例:

rust
fn main() {
    println!("hello world");

    another_function();
}

fn another_function() {
    println!("Another function");
}

预期运行结果:

text
hello world
Another function

上面的例子定义了一个不接收任何参数也不返回任何内容的函数 another_function, 作用在函数体里阐明: 打印文本 Another function.

命名规范

这里提一嘴 Rust 中函数名与变量名的命名规范, snake_case:

  • 全部字母小写;
  • 单词间用下划线连接.

参数

函数常需处理某些内容, 此时以函数参数的形式传入函数. 如下:

rust
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)
}

预期输出

text
get number: 0
get number: 1
get number: 你输入的数

这里的 another_function 接受一个 i32 类型的参数, 并将其打印出来. 可见函数定义一次即可多次使用, 每次调用时, 吃下某些东西, 改变某些东西, 返回某些东西.

TIP

上面所谓的超纲部分就是控制台输入一个数. 在 C 语言中的等价表述:

c
// #include <stdio.h>
int main() {
    int a;
    scanf("%d", &a);
    printf("%d", a);
    // return 0;
}

显然 C 语言简洁得多 (用 C++ 也同样简洁). 实际上 Rust 的繁琐强制程序员主动处理了这些可能的隐患, 这却是 C 开发者尤其是 C 开发团队 (其中有若干新手的情况下) 所困扰的问题.

声明函数参数时必须指定参数的类型, 而传入多个参数则用逗号隔开:

rust
fn foo(arg: Type, arg1: Type1) {...}

语句和表达式, 作用域

我看到的教程:

语句以 ; 结尾, 没有返回值; 表达式不以 ; 结尾, 有返回值.

实际上以 ; 结尾无非返回空类型 () 罢了:

rust
let a = {0;};   // a: ()
let b = {0};    // b: i32
let c = 0;      // c: i32
let d = 0;;     // d: i32

可能会尝试如 d 那样达到效果, 但实际上由于符号优先级, 第二个分号会被视作一个空语句, 与前面的赋值语句无关, 也就是等价于

rust
let d = 0;
;

希望将分号结尾的语句打包绑定给变量, 如 a 那样用作用域包裹起来即可.

作用域还牵涉到生命周期: 某一作用域定义的变量将在该作用域离开时自动销毁. 请看:

rust
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 之定义, 亦本就不可达.

预期输出

text
1
1
hello
hello
(1.1, (2.2, 3.3000000000000003))
aaa
true

无非一句话: 语句是以 ; 结尾的表达式, 返回空类型.

函数的返回值

前文所定义的函数都返回空类型, 函数

rust
fn foo(args..) {...}

本质乃语法糖:

rust
fn foo(args..) -> () {...}

其中的 -> () 即表示返回空类型.

那么如果我们希望整个函数返回其他类型呢? 同样使用 -> 来声明, 下面以 OutputType 代替具体返回类型:

rust
fn foo(args..) -> OutputType {...}

举例:

rust
fn main() {
    let x = "hello";
    println!("{}", get_str(x));
}

fn get_str(input: &str) -> String {
    String::from("Got Number that ") + input
}

预期输出:

text
Got Number that hello

+

注意上文中能 String 类型加 &str 类型, 是因为运算符 + 有一个在 (String, &str, String) 上的重载, 允许字符串类型变量右乘一个字符串切片引用类型变量, 并返回一个字符串类型变量.

交换律

有基本的数学功底应该理解, 交换律 并非天然存在. 举例, 对于普通三阶魔方, 记右面顺时针旋转为 rr, 上面顺时针旋转为 uu, 并定义两个操作顺序执行为乘法, 那么显然, urruur\ne ru, 不满足交换律.

此外, 二元运算也未必基于同一集合, 我们熟知的实数加法被定义为函数 + ⁣:R2R+\colon\mathbb{R}^2\to\mathbb{R}, 但一些运算并非如此, 例如在域 k\mathbb{k} 上的向量空间 VV 有数乘运算.

ScMul ⁣:k×VV,(a,v)av,\begin{align*} \operatorname{ScMul}\colon\mathbb{k}\times V\to V,\\ (a, \mathbf{v})\mapsto a\mathbf{v}, \end{align*}

就是非对称二元运算.

一个函数体中, 其最后一个表达式视作其返回值, 如果类型与函数标签所指定的类型不匹配则报错:

rust
fn foo() -> i32 {
    "hello" // !! 类型不匹配!
}

有时需要在函数体的中间提前返回内容, 可以调用 return 关键字:

rust
fn foo() -> i32 {
    // 干些啥
    if ...{
        return 1;
    }
    // 干些啥
    0
}

控制流

if关键字

rust
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 + 条件表达式 + 执行语句块, 而语句块必须包裹以一对大括号 {}. 换言之,

    rust
    if some_boolean_expr
        do something;

    这种 C/C++ 里的语法糖, Rust 不允许存在.

  • Rust 万物皆表达式, if 也不例外, 因此可以如此操作:

    rust
    let x = if some_boolean_expr { "hello" } else { "world" };
    三目运算符

    显然上例起到了一些语言中三目运算符的作用:

    c
    x = someBooleanExpr ? "hello" : "world";
    // 标准形式 =>      condition ? expr1 : expr 2

    该运算符很容易写出令人费解的屎山, 又不同于 goto 能够用更高级的抽象轻松规避.

    类 C 语言, 包括 C/C++/C\sharp, 以及 Java, JavaScript, 均拥有上述经典三目运算符. 其中 JavaScript 允许嵌套, 而 C\sharp 要求两个分支仅需满足至少可隐式转换.

    函数式语言及多范式语言的函数式范式的处理通常更现代些.

    其中, Python 的解决方案是一种奇怪的语法:

    Python
    x = "hello" if someBooleanExpr else "world"

    虽说号称更贴近自然语言, 但这其实是社区在千禧年前后几年左右脑互搏之后的成果, 未必最优, 因为并非绝大多数 Pythoner 赞成这一语法.

    Scala, Kotlin, Rust 这些习惯于万物皆表达式的语言均支持直接以 if-else 控制流表三目之意:

    Kotlin
    val x = if (a > b) a else b

    Ruby 的 if 表达式表达能力不弱, 但保留了传统的三目运算符:

    ruby
    x = if someBooleanExpr then "hello" else "world" end
    # 或者传统写法
    x = someBooleanExpr ? "hello" : "world"

    一些表达能力稍弱的语言会考虑变通处理, 例如没有三目时的 Python 和 Lua:

    Lua
    x = (someBooleanExpr) and "hello" or "world"

    这种写法需要保证第一个分支的内容不是布尔值 False, 否则会出错. Lua 往往采用更安全的写法:

    Lua
    function ternary(condition, T, F)
        if condition then return T else return F end
    end
    
    max = ternary(someCondition, "hello", "world")

    也有一些现代语言选择更激进地处理. 例如 Go 语言明确不支持三目运算符, 并鼓励完整使用 if 控制流:

    Go
    var 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 相近, 支持语句块返回值, 并要求不同分支类型一致:

    zig
    const x = if (someBooleanExpr) blk: {
        ...
        break :blk "hello";
    } else "world";

分支返回值

所有分支的返回值必须为同一类型, 因为 Rust 是静态类型语言, 设计中预期所有变量的类型在编译期确定, 不一致的类型将导致特定变量的类型确定推迟到运行期:

rust
let x = if true {
    "hello" // &str
} else {
    42      // i32
}

这样的写法会导致编译器报错.

进一步说明

在动态类型语言 Python 中, 上述逻辑完全可行:

python
x = "hello" if True else 42

那么 Rust 如果确实遇到此类需求该怎么办呢? 我们之后会介绍到, 考虑定义所有可能返回类型构成的枚举类型:

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 关键字

最基础的循环, 其他循环基于它实现.

rust
fn main() {
    let x = 1;
    loop {
        println!("{}", x);
        println!("{}", " ");
    }
}

预期不加停止地持续输出:

text
1
 

无限循环固然在某些时候有用, 但显然我们暂时不需要, 须通过一些手段控制循环结束, 也就是 break 关键字:

rust
fn main() {
    let mut x = 0;
    loop {
        println!("{}", x);
        x = x + 1;
        if x == 42 {
            break;
        }
    }
    println!("打印完毕!");
}

预期输出:

text
1
2
...
40
41
打印完毕!

而有时候我们不需要直接嘎掉整个循环体, 而仅仅是希望跃过某次循环的剩余部分直接进入下一循环, 那么就采用 continue 关键字:

rust
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
}
引理 1 (素数判定)

nn 为合数, 当且仅当存在 dnd \le \sqrt n 使得 dnd\mid n.

证明细节
  • (\Rightarrow). 若 nn 为合数, 则存在 dnd\le\sqrt n, 使得 dnd\mid n.

按题, 存在整数 a,ba, b, 使得

n=ab,1<ab<n.n=ab,\quad 1<a\le b<n.

而若 a>n a>\sqrt n , 则

b=na<nn=n,b = \dfrac na<\dfrac n{\sqrt n}=\sqrt n,

ab a\le b 矛盾, 故而必有:

a,2an, and an.\exists a, 2\le a\le\sqrt n, \text{ and } a\mid n.
  • ( \Leftarrow ) 若存在 dnd\le\sqrt n 使得 dnd\mid n, 则 nn 为合数. 显然.

上述代码预期依次输出所有 [2,1000)[2,1000) 中的素数.

此外作为万物皆表达式之一部分, loop 同样允许存在非空返回值, 通过 break <表达式>; 完成, 当然, 同样需要在所有分支返回同一类型:

rust
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
}

预期输出

text
总计素数: 168

while 关键字

rust
while 布尔表达式 {
    ...
}

很简明, 本质等价于

rust
loop {
    if 布尔表达式 {
        break;
    }
    ...
}

不允许返回值.

for 关键字

语法如下:

rust
for element in set.iterator {...}

上文代码可通过 for 简化, 并去掉无用逻辑, 如下:

rust
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 本质为语法糖, 创建了一个 i322210001000 的迭代器, 末尾的 10001000算在内. 实际上亦有不算在内的语法:

rust
for i in 2..<1001 { ... }

底层实现

for 循环语法本质仍为 loop 的语法糖:

rust
for element in set.iter() {
    println!("{element}");
}

本质展开为

rust
{
    // 迭代器 --> 迭代器实例
    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

有时需要嵌套使用循环语句, 而又需要从内层循环直接跳出外层循环. 如果采用普通语法, 将显得极其臃肿而不优雅, 此时可以为循环添加标签:

rust

// 查找二维数组中的特定值
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, 如果同时需要如此, 语法为

rust
break 'o out_val;