Rust-02

83 阅读12分钟

if/else

条件选择是一个表达式,并且所有分支都必须返回相同类型。

fn main() {
    let n = 5;

    if n < 0 {
        print!("{} is negative", n);
    } else if n > 0 {
        print!("{} is positive", n);
    } else {
        print!("{} is zero", n);
    }

    let big_n =
        if n < 10 && n > -10 {
            println!(", and is a small number, increase ten-fold");
            // 这个表达式返回一个 `i32` 类型。
            10 * n
        } else {
            println!(", and is a big number, half the number");
            // 这个表达式也必须返回一个 `i32` 类型。
            n / 2
            // 试一试 ^ 试着加上一个分号来结束这条表达式。
        };
    //   ^ 不要忘记在这里加上一个分号!所有的 `let` 绑定都需要它。
    println!("{} -> {}", n, big_n);
}

loop 循环

无限循环,通过continuebreak控制或退出

mach

switch类似 ,第一个匹配分支会被比对,并且所有可能的值都必须匹配

fn main() {
    let number = 13;
    // 试一试 ^ 将不同的值赋给 `number`

    println!("Tell me about {}", number);
    match number {
        // 匹配单个值
        1 => println!("One!"),
        // 匹配多个值
        2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
        // 试一试 ^ 将 13 添加到质数列表中
        // 匹配一个闭区间范围
        13..=19 => println!("A teen"),
        // 处理其他情况
        _ => println!("Ain't special"),
        // 试一试 ^ 注释掉这个总括性的分支
    }

    let boolean = true;
    // match 也是一个表达式
    let binary = match boolean {
        // match 分支必须覆盖所有可能的值
        false => 0,
        true => 1,
        // 试一试 ^ 将其中一条分支注释掉
    };

    println!("{} -> {}", boolean, binary);
}

解构

match 代码块可以对输入进行解构

  • 元组
  • 枚举
  • 指针和引用
  • 结构体

卫语句

match可以加上卫语句来过滤分支

fn main() {
    let pair = (2, -2);
    // 试一试 ^ 将不同的值赋给 `pair`

    println!("Tell me about {:?}", pair);
    match pair {
        (x, y) if x == y => println!("These are twins"),
        // ^ `if` 条件部分是一个卫语句
        (x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
        (x, _) if x % 2 == 1 => println!("The first one is odd"),
        _ => println!("No correlation..."),
    }
}

绑定

// `age` 函数,返回一个 `u32` 值。
fn age() -> u32 {
    15
}

fn main() {
    println!("Tell me what type of person you are");

    match age() {
        0             => println!("I haven't celebrated my first birthday yet"),
        // 可以直接匹配(`match`) 1 ..= 12,但那样的话孩子会是几岁?
        // 相反,在 1 ..= 12 分支中绑定匹配值到 `n` 。现在年龄就可以读取了。
        n @ 1  ..= 12 => println!("I'm a child of age {:?}", n),
        n @ 13 ..= 19 => println!("I'm a teen of age {:?}", n),
        // 不符合上面的范围。返回结果。
        n             => println!("I'm an old person of age {:?}", n),
    }
}
fn some_number() -> Option<u32> {
    Some(42)
}

fn main() {
    match some_number() {
        // 得到 `Some` 可变类型,如果它的值(绑定到 `n` 上)等于 42,则匹配。
        Some(n @ 42) => println!("The Answer: {}!", n),
        // 匹配任意其他数字。
        Some(n)      => println!("Not interesting... {}", n),
        // 匹配任意其他值(`None` 可变类型)。
        _            => (),
    }
}

if let

// 以这个 enum 类型为例
enum Foo {
    Bar,
    Baz,
    Qux(u32)
}

fn main() {
    // 创建变量
    let a = Foo::Bar;
    let b = Foo::Baz;
    let c = Foo::Qux(100);

    // 变量 a 匹配到了 Foo::Bar
    if let Foo::Bar = a {
        println!("a is foobar");
    }

    // 变量 b 没有匹配到 Foo::Bar,因此什么也不会打印。
    if let Foo::Bar = b {
        println!("b is foobar");
    }

    // 变量 c 匹配到了 Foo::Qux,它带有一个值,就和上面例子中的 Some() 类似。
    if let Foo::Qux(value) = c {
        println!("c is {}", value);
    }
}

while let

fn main() {
    // 将 `optional` 设为 `Option<i32>` 类型
    let mut optional = Some(0);

    // 这读作:当 `let` 将 `optional` 解构成 `Some(i)` 时,就
    // 执行语句块(`{}`)。否则就 `break`。
    while let Some(i) = optional {
        if i > 9 {
            println!("Greater than 9, quit!");
            optional = None;
        } else {
            println!("`i` is `{:?}`. Try again.", i);
            optional = Some(i + 1);
        }
        // ^ 使用的缩进更少,并且不用显式地处理失败情况。
    }
    // ^ `if let` 有可选的 `else`/`else if` 分句,
    // 而 `while let` 没有。
}

for循环和区间

for与区间

for...in 结构可以遍历一个 Iterator(迭代器),创建迭代器最简单方法是使用a..b,这会产生左闭右开,步长为1的值,

或者使用a..=b表示两端都在内。

for与迭代器

for..in 产生的迭代器分为三种,如果没有给出,默认使用into_iter

  • into_iter

通过移动方式,消耗元素

  • iter

通过不可变借用方式

  • iter_mut

通过可变借用方式

函数

方法

方式是预防于对象的函数,这些方法通过self 来访问对象中的数据和其它方法在代码块中定义。

Rust函数fn定义->指定返回类型

struct Point {
    x: f64,
    y: f64,
}

// 实现的代码块,`Point` 的所有方法都在这里给出
impl Point {
    // 这是一个静态方法(static method)
    // 静态方法不需要被实例调用
    // 这类方法一般用作构造器(constructor)
    fn origin() -> Point {
        Point { x: 0.0, y: 0.0 }
    }

    // 另外一个静态方法,需要两个参数:
    fn new(x: f64, y: f64) -> Point {
        Point { x: x, y: y }
    }
}

struct Rectangle {
    p1: Point,
    p2: Point,
}

impl Rectangle {
    // 这是一个实例方法(instance method)
    // `&self` 是 `self: &Self` 的语法糖(sugar),其中 `Self` 是方法调用者的
    // 类型。在这个例子中 `Self` = `Rectangle`
    fn area(&self) -> f64 {
        // `self` 通过点运算符来访问结构体字段
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;

        // `abs` 是一个 `f64` 类型的方法,返回调用者的绝对值
        ((x1 - x2) * (y1 - y2)).abs()
    }

    fn perimeter(&self) -> f64 {
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;

        2.0 * ((x1 - x2).abs() + (y1 - y2).abs())
    }

    // 这个方法要求调用者是可变的
    // `&mut self` 为 `self: &mut Self` 的语法糖
    fn translate(&mut self, x: f64, y: f64) {
        self.p1.x += x;
        self.p2.x += x;

        self.p1.y += y;
        self.p2.y += y;
    }
}

// `Pair` 拥有资源:两个堆分配的整型
struct Pair(Box<i32>, Box<i32>);

impl Pair {
    // 这个方法会 “消耗” 调用者的资源
    // `self` 为 `self: Self` 的语法糖
    fn destroy(self) {
        // 解构 `self`
        let Pair(first, second) = self;

        println!("Destroying Pair({}, {})", first, second);

        // `first` 和 `second` 离开作用域后释放
    }
}

fn main() {
    let rectangle = Rectangle {
        // 静态方法使用双冒号调用
        p1: Point::origin(),
        p2: Point::new(3.0, 4.0),
    };

    // 实例方法通过点运算符来调用
    // 注意第一个参数 `&self` 是隐式传递的,亦即:
    // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)`
    println!("Rectangle perimeter: {}", rectangle.perimeter());
    println!("Rectangle area: {}", rectangle.area());

    let mut square = Rectangle {
        p1: Point::origin(),
        p2: Point::new(1.0, 1.0),
    };

    // 报错! `rectangle` 是不可变的,但这方法需要一个可变对象
    //rectangle.translate(1.0, 0.0);
    // 试一试 ^ 去掉此行的注释

    // 正常运行!可变对象可以调用可变方法
    square.translate(1.0, 1.0);

    let pair = Pair(Box::new(1), Box::new(2));

    pair.destroy();

    // 报错!前面的 `destroy` 调用 “消耗了” `pair`
    //pair.destroy();
    // 试一试 ^ 将此行注释去掉
}

闭包

闭包也叫lambda,是一类能够捕获周围作用域中变量的函数。例如,一个可以捕获 x 变量的闭包如下:

|x| x+1

调用闭包和调用函数完全相同,不过闭包输入和返回类型都可以自动推导,而输入变量必须指明。

特点有:

  • 声明使用|| 代替()包括输入参数
  • 函数体定界符({})
  • 有能力捕获外部环境的变量

捕获

闭包本质上很灵活,能做功能要求的事情,使闭包在没有类型标注的情况下运行。这使得捕获(capture)能够灵活地适应用例,既可移动(move),又可借用(borrow)。闭包可以通过以下方式捕获变量:

通过引用:&T

通过可变引用:&mut T

通过值:T

闭包优先通过引用来捕获变量,并且仅在需要时使用其他方式。

fn main() {
    use std::mem;

    let color = String::from("green");

    // 这个闭包打印 `color`。它会立即借用(通过引用,`&`)`color` 并将该借用和
    // 闭包本身存储到 `print` 变量中。`color` 会一直保持被借用状态直到
    // `print` 离开作用域。
    //
    // `println!` 只需传引用就能使用,而这个闭包捕获的也是变量的引用,因此无需
    // 进一步处理就可以使用 `println!`。
    let print = || println!("`color`: {}", color);

    // 使用借用来调用闭包 `color`。
    print();

    // `color` 可再次被不可变借用,因为闭包只持有一个指向 `color` 的不可变引用。
    let _reborrow = &color;
    print();

    // 在最后使用 `print` 之后,移动或重新借用都是允许的。
    let _color_moved = color;

    let mut count = 0;
    // 这个闭包使 `count` 值增加。要做到这点,它需要得到 `&mut count` 或者
    // `count` 本身,但 `&mut count` 的要求没那么严格,所以我们采取这种方式。
    // 该闭包立即借用 `count`。
    //
    // `inc` 前面需要加上 `mut`,因为闭包里存储着一个 `&mut` 变量。调用闭包时,
    // 该变量的变化就意味着闭包内部发生了变化。因此闭包需要是可变的。
    let mut inc = || {
        count += 1;
        println!("`count`: {}", count);
    };

    // 使用可变借用调用闭包
    inc();

    // 因为之后调用闭包,所以仍然可变借用 `count`
    // 试图重新借用将导致错误
    // let _reborrow = &count;
    // ^ 试一试:将此行注释去掉。
    inc();

    // 闭包不再借用 `&mut count`,因此可以正确地重新借用
    let _count_reborrowed = &mut count;

    // 不可复制类型(non-copy type)。
    let movable = Box::new(3);

    // `mem::drop` 要求 `T` 类型本身,所以闭包将会捕获变量的值。这种情况下,
    // 可复制类型将会复制给闭包,从而原始值不受影响。不可复制类型必须移动
    // (move)到闭包中,因而 `movable` 变量在这里立即移动到了闭包中。
    let consume = || {
        println!("`movable`: {:?}", movable);
        mem::drop(movable);
    };

    // `consume` 消耗了该变量,所以该闭包只能调用一次。
    consume();
    //consume();
    // ^ 试一试:将此行注释去掉。
}

在竖线 | 之前使用 move 会强制闭包取得被捕获变量的所有权:

fn main() {
    // `Vec` 在语义上是不可复制的。
    let haystack = vec![1, 2, 3];

    let contains = move |needle| haystack.contains(needle);

    println!("{}", contains(&1));
    println!("{}", contains(&4));

    //println!("There're {} elements in vec", haystack.len());
    // ^ 取消上面一行的注释将导致编译时错误,因为借用检查不允许在变量被移动走
    // 之后继续使用它。

    // 在闭包的签名中删除 `move` 会导致闭包以不可变方式借用 `haystack`,因此之后
    // `haystack` 仍然可用,取消上面的注释也不会导致错误。
}

作为输入参数

虽然 Rust 无需类型说明就能在大多数时候完成变量捕获,但在编写函数时,这种模糊写法是不允许的。当以闭包作为输入参数时,必须指出闭包的完整类型,它是通过使用以下 trait 中的一种来指定的。其受限制程度按以下顺序递减:

  • Fn:表示捕获方式为通过引用(&T)的闭包
  • FnMut:表示捕获方式为通过可变引用(&mut T)的闭包
  • FnOnce:表示捕获方式为通过值(T)的闭包

译注:顺序之所以是这样,是因为 &T 只是获取了不可变的引用,&mut T 则可以改变变量,T 则是拿到了变量的所有权而非借用。

对闭包所要捕获的每个变量,编译器都将以限制最少的方式来捕获。

译注:这句可能说得不对,事实上是在满足使用需求的前提下尽量以限制最多的方式捕获

作为输出参数

闭包作为输入参数是可能的,所以返回闭包作为输出参数(output parameter)也应该是可能的。然而返回闭包类型会有问题,因为目前 Rust 只支持返回具体(非泛型)的类型。按照定义,匿名的闭包的类型是未知的,所以只有使用impl Trait才能返回一个闭包。

返回闭包的有效特征是:

  • Fn
  • FnMut
  • FnOnce

除此之外,还必须使用 move 关键字,它表明所有的捕获都是通过值进行的。这是必须的,因为在函数退出时,任何通过引用的捕获都被丢弃,在闭包中留下无效的引用。

fn create_fn() -> impl Fn() {
    let text = "Fn".to_owned();
    move || println!("This is a: {}", text)
}
fn create_fnmut() -> impl FnMut() {
    let text = "FnMut".to_owned();
    move || println!("This is a: {}", text)
}
fn create_fnonce() -> impl FnOnce() {
    let text = "FnOnce".to_owned();

    move || println!("This is a: {}", text)
}
fn main() {
    let fn_plain = create_fn();
    let mut fn_mut = create_fnmut();
    let fn_once = create_fnonce();
    fn_plain();
    fn_mut();
    fn_once();
}

发散函数

发散函数(diverging function)绝不会返回。 它们使用 ! 标记,这是一个空类型。

#![allow(unused)]
fn main() {
fn foo() -> ! {
    panic!("This call never returns.");
}
}

模块

模块是由多个函数,结构体,trait等多项组成,模块内部项对外不可见,只有通过pub关键字才可以。

结构体中的字段同样也只在模块内可见,对外需要pub

use ... as ...

可以减少模块调用路径

super & self

可以消除歧义,当模块和父模块有相同函数时候

fn function() {
    println!("called `function()`");
}

mod cool {
    pub fn function() {
        println!("called `cool::function()`");
    }
}

mod my {
    fn function() {
        println!("called `my::function()`");
    }
    
    mod cool {
        pub fn function() {
            println!("called `my::cool::function()`");
        }
    }
    
    pub fn indirect_call() {
        // 让我们从这个作用域中访问所有名为 `function` 的函数!
        print!("called `my::indirect_call()`, that\n> ");
        
        // `self` 关键字表示当前的模块作用域——在这个例子是 `my`。
        // 调用 `self::function()` 和直接调用 `function()` 都得到相同的结果,
        // 因为他们表示相同的函数。
        self::function();
        function();
        
        // 我们也可以使用 `self` 来访问 `my` 内部的另一个模块:
        self::cool::function();
        
        // `super` 关键字表示父作用域(在 `my` 模块外面)。
        super::function();
        
        // 这将在 *crate* 作用域内绑定 `cool::function` 。
        // 在这个例子中,crate 作用域是最外面的作用域。
        {
            use crate::cool::function as root_function;
            root_function();
        }
    }
}

fn main() {
    my::indirect_call();
}

文件分层

$ tree .
.
|-- my
|   |-- inaccessible.rs
|   |-- mod.rs
|   `-- nested.rs
`-- split.rs

当使用 mod my时,会依次my.rsmy/mod.rs文件

Crate

crate(中文有 “包,包装箱” 之意)是 Rust 的编译单元。当调用 rustc some_file.rs 时,some_file.rs 被当作 crate 文件。如果 some_file.rs 里面含有 mod 声明,那么模块文件的内容将在编译之前被插入 crate 文件的相应声明处。换句话说,模块不会单独被编译,只有 crate 才会被编译。

crate 可以编译成二进制可执行文件(binary)或库文件(library)。默认情况下,rustc 将从 crate 产生二进制可执行文件。这种行为可以通过 rustc 的选项 --crate-type 重载。

$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib

默认情况下,库会使用 crate 文件的名字,前面加上 “lib” 前缀,但这个默认名称可以使用 crate_name属性 覆盖。

Cargo

cargo 是官方的 Rust 包管理工具。 它有很多非常有用的功能来提高代码质量和开发人员的开发效率! 这些功能包括:

  • 依赖管理和与 crates.io(官方 Rust 包注册服务)集成
  • 方便的单元测试
  • 方便的基准测试

本章将介绍一些快速入门的基础知识,不过你可以在 cargo 官方手册中找到详细内容。

依赖

创建项目

# 二进制可执行文件
cargo new foo
# 或者库
cargo new --lib foo

Cargo.html内容

[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]

[dependencies]
clap = "2.27.1" # 来自 crates.io
rand = { git = "https://github.com/rust-lang-nursery/rand" } # 来自网上的仓库
bar = { path = "../bar" } # 来自本地文件系统的路径

约定规范

一般Cargo目录结构:

foo
├── Cargo.toml
└── src
    └── main.rs

cargo 支持两个二进制文件,默认二进制名称是 main,但可以通过将文件放在 bin/ 目录中来添加其他二进制可执行文件:

foo
├── Cargo.toml
└── src
    ├── main.rs
    └── bin
        └── my_other_bin.rs

cargo --bin my_other_bin 运行我们bin

其中 my_other_bin 是我们想要使用的二进制可执行文件的名称,