绅士地介绍 Rust

Rust

感谢 {大卫马里诺-David Marino}

为什么要学习一种新的编程语言?

本教程的目标是带您到一个能担当 Rust 读和写的地方,还在线提供优质学习资源,尤其是这本:Rust 编程语言。这是一个 先试后买 的机会,通过感受该语言的力量,会让你想要深入研究一番。

Rust 编程语言:中文

正如,爱因斯坦说过(可能),”As gentle as possible, but no gentler.”。这里有很多新东西要学习,而且不同的是,需要重新整理你的过去思维,因为我们要重新出发啦。 通过’gentle’,你会明白我的意思是,会用示例去友好描述其特性;

当我们遇到困难时,我希望展示, Rust 是如何解决这些问题的。 在了解解决方案的意义之前,理解问题非常重要。 用高档的话说,我们要去在一个山区徒游,期间在路上,我会为你介绍一些有趣的岩层,不用担心,只是几个’地质讲座’,:)。 会有一些上坡难度,但这会让更我们热血沸腾; 社区异常愉快和更乐意帮助人们,如rust 用户论坛和一个活跃的Sub reddit,非常适合。更有官方的常问问题页面,如果你有特定的问题,会是一个很好的资源。

首先,为什么要学习一门新的编程语言? 这是时间和精力的投资,需要一些理由。 即使你没有立即用这种语言找到一份很酷的工作,它也会让你的思维肌肉壮大,并使你成为更好的 programmer。 似乎看起来是一种糟糕的投资回报,但是如果你不一直学习一些 真正的 东西,那么你会停滞不前,即便有十年的经验,也只不过是一遍又一遍地做着同样的事情。

Rust 的闪光点

Rust 是一种静态和强类型的系统编程语言。 静态 意味着,所有类型在编译时都是已知的,强类型 意味着,这些类型的设计使得编写不正确的程序变得更加困难。 一个成功的汇编语言意味着你比牛仔语言(像 C 语言)更好地保证正确性。 系统 意味着通过完全控制内存,生成最佳机器码。 所以可以接受的硬件就很多啦: 操作系统,设备驱动程序和甚至可能没有操作系统的嵌入式系统。 然而,Rust 也能编写普通的应用程序代码,实际上来说,Rust 也是一种非常愉快的语言。

与 C 和 C ++的最大区别在于, Rust 默认是安全的; 所有内存访问都会被检查。不可能因意外,而损坏内存。

Rust 幕后统一原则是:

  • 严格执行数据的 安全借用
  • 在数据上,用函数,方法和闭包来操作
  • 元组,结构和枚举来聚合数据
  • 模式匹配来选择和解构数据
  • trait 来定义 数据的 行为

通过 Cargo ,我们可以有一个快速增长的可用库生态系统,我们将通过学习使用标准库来关注语言的核心原则。 我的建议是,编写 很多小例子,所以,学习直接使用rustc成为了是核心技能。 当我在这段旅程中展示示例时,我编了个叫做rrun的小脚本,它会编译并运行结果:

rustc $1.rs && ./$1

配置

本教程假设您已在本地安装 Rust。 幸运的是,这非常简单

$ curl https://sh.rustup.rs -sSf | sh
$ rustup component add rust-docs

我会建议获取默认的稳定(stable)版本; 稍后可以轻松下载不稳定版本并在两者之间切换。

这得到了编译器,包管理器 Cargo,API 文档和 Rust 书。 千里之行始于一步,而这第一步是无痛的。

rustup是您用来管理 Rust 安装的命令。 当一个新的稳定版本出现时,你只需说rustup update就可以升级。 rustup doc将在您的浏览器中打开离线文档。

你可能已经拥有了你喜欢的编辑器,可以看看主流编辑器的基本 Rust 支持。 我建议你先从基本的语法高亮开始,随着程序变大而工作。

我个人很喜欢Geany这是为数不多的具有 Rust 支持的编辑器之一; 它特别易于使用,因为它可以通过软件包管理器获得,但在其他平台上是可以正常工作。

译者: VsCode 也是可以的

最主要的是知道如何编辑,编译和运行 Rust 程序。 你要学会用 手指 (一字一字) 对你的程序进行编程; 自己键入代码,并学习使用编辑器有效地管理,编排你的流程。

Zed Shaw 的 Python 编程的建议很好,且不论是什么语言。 他说学会编程就像学习乐器 - 秘诀是练习和坚持。 瑜伽和柔道武术也有很好的建议,比如太极拳,感受紧,但不要过度紧张。不要壮大傻瓜肌肉.

我想感谢那些,给出我坏英语或坏 Rust 建议的许多贡献者,并且感谢 大卫马力诺-David Marino 对他的 Rust 表现图,他是一位友善但硬派的无瑕骑士,闪耀着盔甲。

Steve Donovan © 2017-2018 MIT license version 0.4.0

基础

目录

你好,世界!

自从第一个 C 语言版本诞生,”hello world”的最初目的是测试编译器并运行一个实际的程序。

// hello.rs
fn main() {
    println!("Hello, World!");
}
$ rustc hello.rs
$ ./hello
Hello, World!

Rust 是一种带分号的花括号语言, C ++ 风格注释和一个main函数 一 目前来说,非常熟悉吧。 感叹号{!}表明这是一个 调用。 对于 C ++ 程序员来说,这可能是一个退步,因为它们使用了非常愚蠢的 C 宏 - 但我可以确保这些宏能够更强大和更理智。

对于其他任何人来说,会是”现在好了,我不得不记得说,砰!”。 但是,编译器很强的,知道吧;如果你忽略了那个惊叹号,你会得到:

error[E0425]: unresolved name `println`
    --> hello2.rs:2:5
    |
2 |     println("Hello, World!");
    |     ^^^^^^^ did you mean the macro `println!`?

学习一门语言意味着要熟悉它的错误。 试着把编译器当做是一个严格但友好的帮手,而不是一台对你 大喊大叫{shouting} 的电脑,因为你在最开始时,就会看到很多红墨迹。对于编程人员来说,你的编译器提前指出你的错误比程序在用户面前炸毁要好得多。

下一步是介绍一个 变量{variable}:

// let1.rs
fn main() {
    let answer = 42;
    println!("Hello {}", answer);
}

拼写错误是 编译{compile} 错误,而不是类似 Python 或 JavaScript 等动态语言的运行时错误。 这将为您节省很多压力!如果我写了’answr’而不是’answer’,编译器实际上会有关于它的 不错提示 :

    4 |     println!("Hello {}", answr);
      |                         ^^^^^ did you mean `answer`?

println!宏需要一个格式字符串{format string}和一些 值 ;它与 Python 3 使用的格式非常相似。

另一个非常有用的宏是assert_eq!。 这是在 Rust 中进行测试的主力;您 断言{assert} 两件事必须相等,如果不是,就会 panic{恐慌},相当于程序崩溃。

// let2.rs
fn main() {
    let answer = 42;
    assert_eq!(answer,42);
}

本来是不会产生任何输出。但一旦改 42 为 40:

thread 'main' panicked at
'assertion failed: `(left == right)` (left: `42`, right: `40`)',
let2.rs:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.

这是我们在 rust 中的第一个 运行时错误

循环和条件语句

任何有趣的事情都可以会做不止一次:

// for1.rs
fn main() {
    for i in 0..5 {
        println!("Hello {}", i);
    }
}

范围{range} 并不包括 5,所以i的范围从0 到 4。这在将数组等内容从 0 开始进行 索引{indexes} 的语言中很方便。

有趣的事情也必须要 有条件地{conditionally} 做:

// for2.rs
fn main() {
    for i in 0..5 {
        if i % 2 == 0 {
            println!("even {}", i);
        } else {
            println!("odd {}", i);
        }
    }
}
even 0
odd 1
even 2
odd 3
even 4

i % 2为 0,如果i能被 2 整除; Rust 使用 C 风格操作符。 条件周围没有括号,这就像 Go 语言。但是在条件后面必须要跟使用花括号的代码块。

同样的事情,更有趣的写法方式:

// for3.rs
fn main() {
    for i in 0..5 {
        let even_odd = if i % 2 == 0 {"even"} else {"odd"};
        println!("{} {}", even_odd, i);
    }
}

传统上,编程语言有 声明{statements} (比如if) 和 表达式{expressions} (比如1 + i) 。 在 rust 里,几乎所有的东西都有一个值并且可以成为表达式。 不再需要超丑的 C ‘三元操作符’i % 2 == 0?"even": "odd".

⚠️ 请注意,这些代码块中没有任何分号(像{"even"} else {"odd"}这样的)。

开始堆积木吧

计算机非常擅长算术。 这里第一次尝试添加从 0 到 4 的所有数字:

// add1.rs
fn main() {
    let sum = 0;
    for i in 0..5 {
        sum += i;
    }
    println!("sum is {}", sum);
}

但它没有编译成功:

error[E0384]: re-assignment of immutable variable `sum`
    --> add1.rs:5:9
3 |     let sum = 0;
    |         --- first assignment to `sum`
4 |     for i in 0..5 {
5 |         sum += i;
    |         ^^^^^^^^ re-assignment of immutable variable

不可变{Immutable}? 一个变量不能 变{vary}? 默认的,let声明时,变量只能赋值。添加魔法mut ( 让变量可变) 完成表演:

// add2.rs
fn main() {
    let mut sum = 0;
    for i in 0..5 {
        sum += i;
    }
    println!("sum is {}", sum);
}

其他语言使用人员,可能会感到费解,因,在他们看来,默认情况下变量就可以被重写。 变量的产生是,在运行时被分配了一个计算值 - 这不是一个 常数 constant} 。 在数学中也有同样的说法,就像我们说’让 n 是 S 中最大的数’。

声明变量默认 只读 ,是有原因的。 在更大的程序中,很难跟踪正在写入的代码。 所以 Rust 是为了能够明确地表现出,像可变性 (’能写入’) 的东西。 Rust 语言中有很多聪明之处,但它不会隐藏任何东西。

Rust 既是静态类型又是强类型的,它们通常是混淆的,但请考虑 C (静态但弱类型) 和 Python (动态但强类型)。 在静态类型中,类型在编译时是已知的,而动态类型仅在运行时知道。

然而,此刻,感觉 Rust 把这些类型 藏{hiding} 了起来。究竟i是什么类型? 编译器可以从 0 开始, 类型推断 并提出i32 (四字节有符号整数)。

让我们做一个改变00.0. 然后我们得到错误:

error[E0277]: the trait bound `{float}: std::ops::AddAssign<{integer}>` is not satisfied
    --> add3.rs:5:9
    |
5 |         sum += i;
    |         ^^^^^^^^ the trait `std::ops::AddAssign<{integer}>` is not implemented for `{float}`
    |

好了,蜜月结束了: 这意味着什么? 每个操作符 (像 += ) 对应一个 特性{trait} ,而这是一个抽象的接口,必须为每种具体的类型实现。 稍后我们将详细地处理 trait,但是这里您需要知道的是,附加赋值{AddAssign}是实现+=运算符的 trait 名称,错误是说浮点数没有实现整数的+=运算符。 (运算符 trait 的完整列表在这里)

同样,Rust 喜欢张扬, 它不会默默地把那个整数转换成浮点数。

我们必须显式地将该值类型 转换 为浮点数.

// add3.rs
fn main() {
    let mut sum = 0.0;
    for i in 0..5 {
        sum += i as f64;
    }
    println!("sum is {}", sum);
}

函数类型是明确的

函数{Functions} 是一个,编译器不容有失的类型之处。

这实际上是一个深思熟虑的决定,因像 Haskell ,该语言拥有强大的类型推断,几乎没有显式的类型名称。这 Haskell 风格,确实是函数+显式类型签名的好方法。而这也是 rust 需要的。

这是一个简单的用户定义函数:

// fun1.rs

fn sqr(x: f64) -> f64 {
    return x * x;
}

fn main() {
    let res = sqr(2.0);
    println!("square is {}", res);
}

Rust 回到了一个传统的参数声明,其中类型跟在名称后面。如同在 Pascal 等 Algol 派生语言。

再次,若没有整数到浮点数的转换 - 如果你用’2’直接代替2.0,那么我们 会得到一个明确的错误:

8 |     let res = sqr(2);
    |                   ^ expected f64, found integral variable
    |

你很少会看到函数使用return声明。 更多时候,它会像这样:


# #![allow(unused_variables)]
#fn main() {
fn sqr(x: f64) -> f64 {
    x * x
}
#}

这是因为函数的主体({}内部)具有 最后值表达式 ,就像 if-as-an-expression.

由于分号是由人的手指半自动插入的,因此您可以添加它 在 最后值表达式 ,并得到以下错误:

    |
3 | fn sqr(x: f64) -> f64 {
    |                       ^ expected f64, found ()
    |
    = note: expected type `f64`
    = note:    found type `()`
help: consider removing this semicolon:
    --> fun2.rs:4:8
    |
4 |     x * x;
    |       ^

()类型是空的类型,没有什么结果,无效{void},0,空,什么都没有的意思。 Rust 的一切都有个值,但有时它就是为空。编译器察觉这是个常见的错误,并能实实在在地帮助到你,(每个在 C++编译器上花过时间的人都知道,这可是个 要死要死的情况 )。

也就是说, 如果你要返回, 就不能加 分号{;}

没 return 表达风格的几个例子:


# #![allow(unused_variables)]
#fn main() {
// 返回,一个浮点数的绝对值函数
fn abs(x: f64) -> f64 {
    if x > 0.0 {
        x
    } else {
        -x
    }
}

// 确保,该数字,定然在给予的范围内
fn clamp(x: f64, x1: f64, x2: f64) -> f64 {
    if x < x1 {
        x1
    } else if x > x2 {
        x2
    } else {
        x
    }
}
#}

使用return不是错误的,但没有它,代码就会更干净。 但是对于从一个函数 提前回来 , 你仍会用到return

一些操作可以被优雅地表达 递归:


# #![allow(unused_variables)]
#fn main() {
fn factorial(n: u64) -> u64 {
    if n == 0 {
        1
    } else {
        n * factorial(n-1)
    }
}
#}

起初这可能有些奇怪,然后最好用铅笔和纸制作一些例子。然而,通常这样做不是最 高效 的方式。

值也可以通过 引用 方式传递。 一个引用是由&创建,还有用* 解引用

fn by_ref(x: &i32) -> i32{
    *x + 1
}

fn main() {
    let i = 10;
    let res1 = by_ref(&i);
    let res2 = by_ref(&41);
    println!("{} {}", res1,res2);
}
// 11 42

如果你想要一个函数来修改它的一个参数呢? 那么请输入 可变引用:

// fun4.rs

fn modifies(x: &mut f64) {
    *x = 1.0;
}

fn main() {
    let mut res = 0.0;
    modifies(&mut res);
    println!("res is {}", res);
}

这比 C ++ 更像 C ++ 。 你必须明确地传递参数 (加上&) 和明确 用* 解引用 。 然后键入mut, 因为它不是默认可变的。 (我一直觉得与 C 相比, C++ 引用太容易错过。 )

基本上, Rust 是引入一些 摩擦{friction} 这里。并不是那么巧妙地推动函数直接返回值。 幸运的是, rust 有强力的方式表达”操作成功,结果在这里”。 所以mut不需要那么频繁。 当我们有一个大对象并且不想复制它时,传递引用就很重要了。

变量后加上类型的样式,同样适用于let,当你真的想改变变量的类型:


# #![allow(unused_variables)]
#fn main() {
let bigint: i64 = 0;
#}

学习在哪里找到绳子

现在是开始使用文档的时候了。 这已安装在您的机器上,您可以使用rustup doc --std在浏览器中打开它。

注意顶部的 搜索 ,因为这将是你的朋友;它完全离线运行。

假设我们想知道数学函数在哪里,所以搜索”cos”。 前两个,显示它为单精度和双精度浮点数字的定义。 它定义在 值本身{value itself} 之上,作为一种方法,像这样:


# #![allow(unused_variables)]
#fn main() {
let pi: f64 = 3.1416;
let x = pi/2.0;
let cosine = x.cos();
#}

结果近乎于零; 我们显然需要一个更权威的’pi’!

(为什么我们需要一个明确的f64类型? 因为没有它,该3.1416常数可以是f32f64类型,而这些都是非常不同的。)

让我引用一个cos例子,但写一个完整的程序(assert_eq!的表亲戚assert!;表达式必须正确)。

fn main() {
    let x = 2.0 * std::f64::consts::PI;

    let abs_difference = (x.cos() - 1.0).abs();

    assert!(abs_difference < 1e-10);
}

std::f64::consts::PI是一口饭! ::与在 c++中有同样的意思,(通常使用”.”在其他语言) - 这是一个完全合格的名字。 在文档搜索“PI”后,我们在第二个提示中得到这个全名。

到目前为止,我们的小 Rust 项目一直抛开importexclude这些,会使讨论”Hello World”程序慢下来的东西。让这个程序可读性更强的use声明:

use std::f64::consts;

fn main() {
    let x = 2.0 * consts::PI;

    let abs_difference = (x.cos() - 1.0).abs();

    assert!(abs_difference < 1e-10);
}

为什么我们现在不需要这样做? 这是因为 Rust 的prelude在起作用,使许多基本功能无需显式 use语句。

数组和切片

所有静态类型的语言都有 数组,这在内存装有鼻子到尾巴的值。数组 索引 从零开始:

// array1.rs
fn main() {
    let arr = [10, 20, 30, 40];
    let first = arr[0];
    println!("first {}", first);

    for i in 0..4 {
        println!("[{}] = {}", i,arr[i]);
    }
    println!("length {}", arr.len());
}

输出是:

first 10
[0] = 10
[1] = 20
[2] = 30
[3] = 40
length 4

在这种情况下,Rust 知道数组 究竟 有多大,如果你尝试访问arr[4],这将是一个 编译错误

学习一门新语言往往涉及到 忘却 来自旧语言的已知思维习惯; 如果你是一个 Pythonista,那么这些括号你想是list。快速产生思绪,这是 Rust 中的list等同物,但数组不是你正在想的那样; 他们是 固定大小。 他们也会是 可变的 (如果我们问得好),但你不能添加新的元素。

在 Rust 中不常使用数组,因为数组的类型包含他们大小。 示例中的数组的类型是[i32;4]; [10,20]类型将会[i32;2]等等: 他们有 不同类型。 所以他们作为函数参数是件麻烦事。

常用的 切片。 你可以把它们看作是一个基本值数组的 快照 。 它们的行为很像一个数组, 且 知道他们的尺寸 ,不像那些危险的 C 指针东东。

注意这里有两个重要的事情 - 如何写一个切片的类型,和你必须使用&将其传递给函数.

// array2.rs
// 读作 as: i32切片
fn sum(values: &[i32]) -> i32 {
    let mut res = 0;
    for i in 0..values.len() {
        res += values[i]
    }
    res
}

fn main() {
    let arr = [10,20,30,40];
    // 看着这里的 &
    let res = sum(&arr);
    println!("sum {}", res);
}

先忽略sum函数,看看&[i32]。 rust 数组和切片之间的关系类似于 C 数组和指针 之间的关系,除了两个重要的区别: rust 的切片会跟踪它们的大小 (如果你 尝试访问这个大小之外 会 panic),并且想把数组作为一个切片传递,你必须明确地使用&操作符。

C 程序员读&作为”取地址符”,rust 程序员则是 借用{borrow} 它。 这将是要学习的 rust 关键词。 借用是编程中常见模式的名称; 每当你通过引用传递 (几乎总是发生在动态语言中) 或 在 C 中传递指针时,原始所有者所拥有的任何东西被 借用 了。

切和割

不能以通常的方式{}打印出一个数组,但你可以用{:?}做一个 debug性质的打印。

// array3.rs
fn main() {
    let ints = [1, 2, 3];
    let floats = [1.1, 2.1, 3.1];
    let strings = ["hello", "world"];
    let ints_ints = [[1, 2], [10, 20]];
    println!("ints {:?}", ints);
    println!("floats {:?}", floats);
    println!("strings {:?}", strings);
    println!("ints_ints {:?}", ints_ints);
}

这使:

ints [1, 2, 3]
floats [1.1, 2.1, 3.1]
strings ["hello", "world"]
ints_ints [[1, 2], [10, 20]]

所以,数组套数组是没问题,但重要的是,数组包括内容 只能有一个类型。 数组中的值 在内存中排列在一起,以便他们非常高效地访问。

如果你对这些变量实际的类型感到好奇,这有些能用的方法。就是用一个你知道会是错误的显式类型,来声明一个变量:


# #![allow(unused_variables)]
#fn main() {
let var: () = [1.1, 1.2];
#}

这是信息错误:

3 |     let var: () = [1.1, 1.2];
  |                   ^^^^^^^^^^ expected (), found array of 2 elements
  |
  = note: expected type `()`
  = note:    found type `[{float}; 2]`

({float}意思是”一些不完全指定的浮点数类型)

切片会给你 相同 数组的不同 视角 :

// slice1.rs
fn main() {
    let ints = [1, 2, 3, 4, 5];
    let slice1 = &ints[0..2];
    let slice2 = &ints[1..];  // 开放式范围!

    println!("ints {:?}", ints);
    println!("slice1 {:?}", slice1);
    println!("slice2 {:?}", slice2);
}
ints [1, 2, 3, 4, 5]
slice1 [1, 2]
slice2 [2, 3, 4, 5]

这是一个简洁的符号,类似于 Python 切片但是有很大区别: 从未有过任何数据的副本。 这些 切片 都是借用{borrow} 他们自己的数组数据。 与数组存有一个非常亲密的关系,且 Rust 花很多精力来确保这种关系不会被破坏。

可选(Option)值

切片,就像数组一样,可以 索引。 Rust 在编译时知道数组的大小,但只有在运行时才知道分切片的大小。 所以s[i]在运行时会引起超出界限的错误和 恐慌{panic}。 这你不会想要,而一个安全启动中止 与 非常昂贵的切片 之间也有所不同。 无一例外

冷静下,大招来了。 你不能在某些 try-block 中包装可怕的问题代码,用来”捕获错误” - 至少不是你每天都想使用的方式。 那么 Rust 如何保证安全?

有一种切片方法get,这并不恐慌{panic}。但是它返回了什么?

// slice2.rs
fn main() {
    let ints = [1, 2, 3, 4, 5];
    let slice = &ints;
    let first = slice.get(0);
    let last = slice.get(5);

    println!("first {:?}", first);
    println!("last {:?}", last);
}
// first Some(1)
// last None

last失败 (我们忘记了基于零的索引),但返回了一个叫做None的东西。 first很好,但是作为一个 值包装在Some中。 欢迎Options类型!它可能是Some 或者 None

option w类型有一些有用的方法:


# #![allow(unused_variables)]
#fn main() {
    println!("first {} {}", first.is_some(), first.is_none());
    println!("last {} {}", last.is_some(), last.is_none());
    println!("first value {}", first.unwrap());

// first true false
// last false true
// first value 1
#}

如果你 打开{unwrap} last,你会得到一个恐慌{panic}。但至少你可以调用is_some - 如示例中,如果默认你有一个 没有值的变量:


# #![allow(unused_variables)]
#fn main() {
    let maybe_last = slice.get(5);
    let last = if maybe_last.is_some() {
        *maybe_last.unwrap()
    } else {
        -1
    };
#}

注意* - Some内部的精确类型是&i32,这是一个引用。 我们需要解引用回到一个i32的值.

这繁琐,一个快捷方式是unwrap_or, 如果返回的值是NoneOption类型。 - 类型要匹配,因get返回一个引用。所以你必须写成&i32&-1。最后再次使用*获得i32类型值。


# #![allow(unused_variables)]
#fn main() {
    let last = *slice.get(5).unwrap_or(&-1);
#}

很容易漏写&,但你有编译器的帮助。 如果它是-1rustc says ‘expected &{integer}, found integral variable’,然后告诉你’help: try&-1“。

你可以把Option想成一个可能包含一个值的 盒子,或者什么都没有 (None) (在 Haskell, 它被称为Maybe)。 可能包含 任何 值,就是它的 类型规范 。而在这种情况下,完整的类型是Option<&i32>,使用 C ++ 风格的表示 泛型{generics}。 打开这个 盒子可能会引起爆炸,但不像薛定谔的猫,我们可以事先知道它是否包含一个值。

在 Rust 函数/方法中, 返回这些可能的盒子(Option),是非常常见的,所以学习如何舒适地使用它们

向量

我们将再次回到切片方法,但首先看看:向量{Vec}。 这些是 灵活大小 的数组,其行为很像 Python 的List和 C++ 的std::vector。 事实上,rust 的Vec会有所不同,你可以将额外的值附加到一个向量上,当然注意,它必须声明为可变的。

// vec1.rs
fn main() {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v.push(30);

    let first = v[0];  // 同样,超出范围也会 panic
    let maybe_first = v.get(0);

    println!("v is {:?}", v);
    println!("first is {}", first);
    println!("maybe_first is {:?}", maybe_first);
}
// v is [10, 20, 30]
// first is 10
// maybe_first is Some(10)

一个常见的初学者错误是忘记mut,那你会得到一个有用的错误信息:

3 |     let v = Vec::new();
  |         - use `mut v` here to make mutable
4 |     v.push(10);
  |     ^ cannot borrow mutably

向量和切片之间有非常密切的关系:

// vec2.rs
fn dump(arr: &[i32]) {
    println!("arr is {:?}", arr);
}

fn main() {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v.push(30);

    dump(&v);

    let slice = &v[1..]; // <== 这个 &
    println!("slice is {:?}", slice);
}

那个小小的,很重要的借用符号&是为了 迫使 向量进入切片。且它是完全说得通的,因为向量也在观察着一个有值的数组,不同的是该数组为 动态地 分配。

如果你来自一种动态的语言,那么现在是时候开始讨论下了。 在系统语言中,程序存储器有两种: 栈和堆。 在栈上分配数据非常简单,但是栈是有限的; 通常是 MB 为单位。 堆可以是 GB,但是分配成本相对昂贵,并且这样的内存必须是之后 释放 。在所谓的’管理’语言 (如 java,Go 和所谓的脚本语言) 这些细节都隐藏在’便利的市政工程’称 垃圾收集器 中。 一旦系统确定数据不再引用的其他数据,它就会回到可用内存池。

一般来说,这是一个值得付出的代价。 玩栈非常不安全, 因为如果你犯了一个错误,在当前函数中覆盖返回地址,那么你跪了。

我写的第一个 C 程序是在 DOS PC 上, 抛开电脑本身。Unix 系统总是表现得更好,且只有 伴随一个 segfault 的进程才会挂掉 。 为什么这比 Rust(或 Go)程序恐慌{panic}更糟? 因为 Rust 会当原始问题出现了,就会发生恐慌{panic}, 而不是像以前困惑程序怎么崩溃的,并吃掉你所有的功课。

恐慌{panic}就是 内存安全 ,它们在任何非法访问内存之前发生。 这是一个 C 中常见的安全问题,因为所有内存访问都是不安全的,并且一个狡猾的攻击者 可以利用这个弱点。

恐慌{panic}本身听起来是绝望的,无计划性的,但 Rust 的恐慌{panic}是结构化的 - 栈的 释放 方式 与异常(抛出错误)情况发生时相同。 所有分配的对象都被删除,并且生成一个回溯。

垃圾收集的缺点? 首先是它是浪费内存, 看看那些占有重要地位,越来越统治我们世界的小型嵌入式微芯片, 其次是它会在最糟糕的时候决定进行 立即 清理 。 (有个妈妈的比喻是,她想在,你与新的情人快乐玩耍时,进行房间的打扫 )。 这些嵌入式系统需要当事物发生时,对其做出响应 (’实时’),并且不能容忍计划外的 清洗举动。 Roberto Ierusalimschy,Lua 的首席设计师(最优雅的动态语言设计师之一) 说,他不想飞机,是 依靠垃圾收集软件在飞。

回到 vectors :当一个 vectors 被修改或创建时,它由堆分配内存,并变成 该内存的 拥有者 。 切片从 vectors 的内存中借用。 当 vectors 死亡或 drops 时,切片也会跟随 vectors 的动作。

迭代器

我们到目前为止,都没有提及的关键部分,也正是 rust 的难题 - 迭代器.

一个范围的 for 循环,是在使用迭代器(0..n,其实是类似于 Python 3 的range功能)。

迭代器很容易定义。 下面是一个”对象”,它使用next方法返回一个Option。只要这个值不是None,我们就一直next下去:

// iter1.rs
fn main() {
    let mut iter = 0..3;
    assert_eq!(iter.next(), Some(0));
    assert_eq!(iter.next(), Some(1));
    assert_eq!(iter.next(), Some(2));
    assert_eq!(iter.next(), None);
}

而这正是for var in iter {}所做的。

这似乎是定义 for 循环的一种低效方式,但是rustc在发布模式中会进行变态的优化,相信它会和while循环一样快。

这是对数组进行迭代的第一次尝试:

// iter2.rs
fn main() {
    let arr = [10, 20, 30];
    for i in arr {
        println!("{}", i);
    }
}

失败,但有帮助哟:

4 |     for i in arr {
  |     ^ the trait `std::iter::Iterator` is not implemented for `[{integer}; 3]`
  |
  = note: `[{integer}; 3]` is not an iterator; maybe try calling
   `.iter()` or a similar method
  = note: required by `std::iter::IntoIterator::into_iter`

按照rustc的建议,下面的程序按预期工作.

// iter3.rs
fn main() {
    let arr = [10, 20, 30];
    for i in arr.iter() {
        println!("{}", i);
    }

    // 切片将隐式转换为迭代器...
    let slice = &arr;
    for i in slice {
        println!("{}", i);
    }
}

实际上,迭代数组或切片,用这种方式比for i in 0..slice.len() {}效率更高,因为 Rust 不必痴迷于检查每个索引操作。

我们之前有一个,一系列整数总和的例子。 它涉及一个mut变量和循环。以下是 惯用的, 总和方式:

// sum1.rs
fn main() {
    let sum: i32  = (0..5).sum();
    println!("sum was {}", sum);

    let sum: i64 = [10, 20, 30].iter().sum();
    println!("sum was {}", sum);
}

请注意,这是其中一个需要明确说明的情况,就是该变量的 类型 ,因为不这样做, Rust 就没有足够的信息。 这里我们用两个不同的整数做总和,没有问题。 (如果用尽所有的名字,那创建一个新的同名变量也是没有问题的。 )

为了扩展需要,这有更多的切片 方法。 (另一个文档提示;在每个文档页的右边有一个’[-],可单击该按钮以折叠方法列表。 然后你可以扩展任何看起来很有趣的细节。 那些看起来怪异的东西,现在就忽略它吧。

这个windows方法,提供了一个迭代器,层叠的值窗口。

// slice4.rs
fn main() {
    let ints = [1, 2, 3, 4, 5];
    let slice = &ints;

    for s in slice.windows(2) {
        println!("window {:?}", s);
    }
}
// window [1, 2]
// window [2, 3]
// window [3, 4]
// window [4, 5]

块{chunks}:


# #![allow(unused_variables)]
#fn main() {
    for s in slice.chunks(2) {
        println!("chunks {:?}", s);
    }
// chunks [1, 2]
// chunks [3, 4]
// chunks [5]
#}

更多关于向量

有一个有用的小宏vec!用于初始化向量。 注意你可以使用pop 去除{remove} 向量结尾值,和 扩展{extend} 一个兼容迭代器的向量。

// vec3.rs
fn main() {
    let mut v1 = vec![10, 20, 30, 40];
    v1.pop();

    let mut v2 = Vec::new();
    v2.push(10);
    v2.push(20);
    v2.push(30);

    assert_eq!(v1, v2);

    v2.extend(0..2);
    assert_eq!(v2, &[10, 20, 30, 0, 1]);
}

验证向量,它们之间每个对应值都相互比较,切片为值。

可以将值插入到向量中的任意位置。 插入{insert}或者使用去除{remove}移除。 这不像 push 和 pop 一样高效 ,这些值将不得不被移动以腾出空间,所以请小心这些操作 向量。

vec 具有大小和 capacity{容量}。 如果你清除了一个 vec ,它的大小就变成了零, 但它仍保留其旧容量。 所以用push等来填充,只会 当尺寸大于该容量时,才会重新分配容量。

vec 可以排序,然后可以删除重复的 - 这些操作就在 vec 上。 (如果你想先复制,可使用clone).

// vec4.rs
fn main() {
    let mut v1 = vec![1, 10, 5, 1, 2, 11, 2, 40];
    v1.sort();
    v1.dedup();
    assert_eq!(v1, &[1, 2, 5, 10, 11, 40]);
}

字符串

Rust 中的字符串比其他语言中的字符串更复杂一些; String类型, 像Vec,动态分配并可调整大小。 (所以它就像 C ++ 的std::string 但不像 Java 和 Python 的不可变字符串。)但是一个程序可能包含很多 string literals {字符串常量}(如”hello”)和系统语言应该能够在执行时静态存储这些 。 若放在微型嵌入式来说,这可能意味着存在 于 廉价的 ROM 而不是 昂贵的 RAM(对低功耗设备来说,RAM 是 在功耗方面也很昂贵。)所以 系统 语言必须具有 两种字符串,分配的与静态的。

所以”hello”不是String类型。 它是&str类型(发音为’string slice’)。 这就像 C ++ 中 const char*std::string 之间的区别,除了 &str更智能。 实际上,&strString有一个很好的的相似关系 就是&[T]Vec<T>

// string1.rs
fn dump(s: &str) {
    println!("str '{}'", s);
}

fn main() {
    let text = "hello dolly";  // string 切片
    let s = text.to_string();  // 现变成 已分配的 string

    dump(text);
    dump(&s);
}

再次, 借用符号 可以迫使String成为&str, 就像Vec<T>能被迫使进&[T]

在引擎盖下,String基本上是一个Vec<u8>,和&str是一个&[u8], 但是那些字节 必须 表示有效的 UTF-8 文本。

就像一个 Vec,你可以push一个字符,和popString结尾:

// string5.rs
fn main() {
    let mut s = String::new();
    // 初始化 空的!
    s.push('H');
    s.push_str("ello");
    s.push(' ');
    s += "World!"; //  `push_str`的简写
    // 移除最后的char
    s.pop();

    assert_eq!(s, "Hello World");
}

to_string可以将许多类型转换为字符串。 (如果可以用”{}”打印它们,那么它们就可以被转换) . format!是像println!使用相同的格式字符串,但构建更复杂的字符串的一种非常有用的方法。

// string6.rs
fn array_to_str(arr: &[i32]) -> String {
    let mut res = '['.to_string();
    for v in arr {
        res += &v.to_string();
        res.push(',');
    }
    res.pop();
    res.push(']');
    res
}

fn main() {
    let arr = array_to_str(&[10, 20, 30]);
    let res = format!("hello {}", arr);

    assert_eq!(res, "hello [10,20,30]");
}

注意&在前面的v.to_string()- &符号表示一个字符串切片,不是String自身(to_string返回),因此,它需要一点手法来匹配。

小课堂:v本身是&i32类型,通过to_string转为String,再通过&符号转为&str,让 res 的+=语法糖(也就是add_assign方法)操作可以成功。官方文档

用于切片的..也与字符串一起工作:

// string2.rs
fn main() {
    let text = "static";
    let string = "dynamic".to_string();

    let text_s = &text[1..];
    let string_s = &string[2..4];

    println!("slices {:?} {:?}", text_s, string_s);
}
// slices "tatic" "na"

但是,你不能索引字符串! 这是因为它们使用的是 唯(一)真(正)编码 UTF-8,其中的”character”可能是一个字节数。

// string3.rs
fn main() {
    let multilingual = "Hi! ¡Hola! привет!";
    for ch in multilingual.chars() {
        print!("'{}' ", ch);
    }
    println!("");
    println!("len {}", multilingual.len());
    println!("count {}", multilingual.chars().count());

    let maybe = multilingual.find('п');
    if maybe.is_some() {
        let hi = &multilingual[maybe.unwrap()..];
        println!("Russian hi {}", hi);
    }
}
// 'H' 'i' '!' ' ' '¡' 'H' 'o' 'l' 'a' '!' ' ' 'п' 'р' 'и' 'в' 'е' 'т' '!'
// len 25
// count 18
// Russian hi привет!

⚠️ 现在,让我们思考下 - 有 25 个字节,但是只有 18 个字符! 但是,如果你使用类似find的方法,你会得到一个有效的索引(如果有的话)和任意切片也会没事。

( Rust 的char类型是一个 4 字节的 Unicode 代码点。所以字符串不是字符 的数组!)

字符串切片可能会像 Vec 索引一样爆炸,因为它使用字节偏移量。在这种情况下, 该字符串由两个字节组成,所以试图拉出第一个字节,可是一个 Unicode 错误。 所以, 注意只使用来自字符串方法的有效偏移来切分字符串。


# #![allow(unused_variables)]
#fn main() {
    let s = "¡";
    println!("{}", &s[0..1]); // <-- 错, 这是多字节字符的第一个字节
#}

拆解字符串是一种常见和有用的方式。字符串的split_whitespace 方法返回会 迭代器,然后,我们就选择去如何处理它。一个主要做法是需要 创建拆分子串的 vec 。

collect非常普遍,因此需要一些关于,处于 collect 的线索,也就是看其 显式的类型。


# #![allow(unused_variables)]
#fn main() {
    let text = "the red fox and the lazy dog";
    let words: Vec<&str> = text.split_whitespace().collect();
    // ["the", "red", "fox", "and", "the", "lazy", "dog"]
#}

你也可以这样说,传递迭代器到扩展{extend}方法:


# #![allow(unused_variables)]
#fn main() {
    let mut words = Vec::new();
    words.extend(text.split_whitespace());
#}

在大多数语言中,我们将不得不制作这些 分离的,已分配 字符串, 而在这里, Vec 中的每个片段,都是从原始字符串中借用的。 我们所分配的是持有切片的位置。

看看这个可爱的双线| |; 我们从chars得到了一个迭代器, 并只要那些不是 空格 的字符。 再次,collect需要 一个线索(我们可能想要一个 字符串向量=String):


# #![allow(unused_variables)]
#fn main() {
    let stripped: String = text.chars()
        .filter(|ch| ! ch.is_whitespace()).collect();
    // theredfoxandthelazydog
#}

filter方法接受一个 闭包函数,这是 Rust 的 lambdas/匿名函数。这里的参数类型从上下文中是清楚的,所以显式规则是放松了的。

就是这样,你可以这样搞定 chars 的显式循环,将返回的字符切片推送到一个可变的向量中,但是这个更短,读取性很好 ( 你习惯了),同样也很快。使用一个循环的方式不是一种 ,然而,我会鼓励你,也写这个一串过的版本。

插曲: 获取命令行参数

到目前为止,我们的节目都生活在对外界的无知之中;现在是时候给他们提供数据。

std::env::args是你如何访问命令行参数法宝;它返回一个迭代器作为字符串的参数,包括程序名。

// args0.rs
fn main() {
    for arg in std::env::args() {
        println!("'{}'", arg);
    }
}
src$ rustc args0.rs
src$ ./args0 42 'hello dolly' frodo
'./args0'
'42'
'hello dolly'
'frodo'

返回一个Vec会更好吗? 这很容易,使用collect制作迭代器,使用该向量的skip方法跳过程序名。


# #![allow(unused_variables)]
#fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.len() > 0 { // we have args!
        ...
    }
#}

这还不错;几乎所有的语言都会这样做.

读取单个参数的 更 Rust-y 特色的方法(传递一个整数值):

// args1.rs
use std::env;

fn main() {
    let first = env::args().nth(1).expect("please supply an argument");
    let n: i32 = first.parse().expect("not an integer!");
    // do your magic
}

nth(1)为您提供迭代器的第二个值,以及expect方法就像一个unwrap但带有可读的信息。

将字符串转换为数字很简单,但您需要指定值的类型 - 还有什么是可以parse的,你知道吗?

这个程序可能会恐慌{panic},不过对笨拙的测试程序来说还能用。但不要太习惯于这种方便的想法。

匹配

我们提取俄罗斯问候语的string3.rs代码,并不是通常的写法。 进入 match 的世界吧:


# #![allow(unused_variables)]
#fn main() {
    match multilingual.find('п') {
        Some(idx) => {
            let hi = &multilingual[idx..];
            println!("Russian hi {}", hi);
        },
        None => println!("couldn't find the greeting, Товарищ")
    };
#}

match包括几个 模式{patterns} ,用一个匹配值和后跟 胖箭头,用逗号分隔。 它方便地,将Options中的值与idx束缚起来。 你 必须 指定所有的可能性,所以我们必须处理None

一旦你习惯了 (我的意思是,打多几遍),感觉比is_some检查更自然,因检查还需要一个额外的Option存储。

但是,如果你对这里的失败不感兴趣,那么if let会是你的朋友:


# #![allow(unused_variables)]
#fn main() {
    if let Some(idx) = multilingual.find('п') {
        println!("Russian hi {}", &multilingual[idx..]);
    }
#}

如果你想做一次匹配,且 对一个可能的结果感兴趣,那这无疑是个方便的写法。

匹配{match}也会像一个 C 的switch声明,就像其他 Rust 构造一样可以返回一个值:


# #![allow(unused_variables)]
#fn main() {
    let text = match n {
        0 => "zero",
        1 => "one",
        2 => "two",
        _ => "many",
    };
#}

这个_就像 C 的default,是一个备用情况。如果你不提供一个默认, rustc会认为这是一个错误。(在 C++中,最好的期望是一个警告,会说很多关于各自的语言)。

Rust 的匹配语句也可以匹配范围。 请注意,这些范围是有 three{三个} 点 ,并且是全包含性的范围,所以第一个条件将匹配 3。


# #![allow(unused_variables)]
#fn main() {
    let text = match n {
        0...3 => "small",
        4...6 => "medium",
        _ => "large",
     };
#}

读取文件

下一步是向世界展示的,是 读取文件

回想一下,expect就像unwrap,但可自定义一个错误消息。 在这里我们会扔掉一些错误:

// file1.rs
use std::env;
use std::fs::File;
use std::io::Read;

fn main() {
    let first = env::args().nth(1).expect("please supply a filename");

    let mut file = File::open(&first).expect("can't open the file");

    let mut text = String::new();
    file.read_to_string(&mut text).expect("can't read the file");

    println!("file had {} bytes", text.len());

}
src$ file1 file1.rs
file had 366 bytes
src$ ./file1 frodo.txt
thread 'main' panicked at 'can't open the file: Error { repr: Os { code: 2, message: "No such file or directory" } }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.
src$ file1 file1
thread 'main' panicked at 'can't read the file: Error { repr: Custom(Custom { kind: InvalidData, error: StringError("stream did not contain valid UTF-8") }) }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.

所以,open会失败,因为该文件不存在或者我们不允许读它,然后,read_to_string也会失败,因为该文件不包含有效的 UTF-8。 (虽然这么说,但你可以使用read_to_end并将 其内容 用 一个字节 vec 替代。) 对于不太大的文件,一口一口地读取它们是有用的,且直接。

如果你知道其他语言的文件处理,你可能会想要知道,文件什么时候 关闭{closed}。如果我们正在写入该文件,那么不关闭它,可能导致数据丢失。 但是这里啊,当函数结束时,文件就会被关闭,应file变量被 释放{dropped} 了。

要知道”抛出错误(throw-catch)”的做法习惯是很糟糕的。你不会想将这些代码放入函数中,因为它知道,它可以很容易地使整个程序崩溃。 所以现在我们必须谈论,File::open到底返回什么。如果Option是一个值,其可能包含或不包含任何内容,那么Result就是一个可能包含某些内容或一个错误的值。 他们都明白unwrap (和它的表弟expect) ,但它们完全不同。 Result是由 二种 类型参数定义的,分别是Ok值和Err值。 Result‘盒子’ 有两个隔间,一个标签是Ok而另一个是Err.

fn good_or_bad(good: bool) -> Result<i32,String> {
    if good {
        Ok(42)
    } else {
        Err("bad".to_string())
    }
}

fn main() {
    println!("{:?}",good_or_bad(true));
    //Ok(42)
    println!("{:?}",good_or_bad(false));
    //Err("bad")

    match good_or_bad(true) {
        Ok(n) => println!("Cool, I got {}",n),
        Err(e) => println!("Huh, I just got {}",e)
    }
    // Cool, I got 42

}

(实际的”错误”类型是随意的,很多人使用字符串,直到人们对 Rust 错误类型产生兴趣)。 这是返回一个值 另一个值的方便方法。

这些文件读取函数版本是不会崩溃。 因它返回一个Result,当然还要 呼叫者{caller} 决定如何处理这个错误。

// file2.rs
use std::env;
use std::fs::File;
use std::io::Read;
use std::io;

fn read_to_string(filename: &str) -> Result<String,io::Error> {
    let mut file = match File::open(&filename) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    let mut text = String::new();
    match file.read_to_string(&mut text) {
        Ok(_) => Ok(text),
        Err(e) => Err(e),
    }
}

fn main() {
    let file = env::args().nth(1).expect("please supply a filename");

    let text = read_to_string(&file).expect("bad file man!");

    println!("file had {} bytes", text.len());
}

第一次匹配 从Ok安全地提取值,这就成了该 match 的值。 如果它是Err,就返回错误,并重新包装为一个Err

第二个匹配返回字符串,包装为Ok,否则返回 (再一次)错误。Ok中的实际值不重要,所以我们用_忽略 它。

当一个函数的大部分在处理错误时,会不太好看; 那么 ‘快乐’就会迷失了。往往这个问题,伴有很多 明确的提前返回,或者是 ignoring errors{忽视了错误} 。(顺便说一下, 这可是在 Rust 世界中,最接近邪恶的东西。)

幸运的是,有一个捷径。

std::io模块定义了一个别名,名为io::Result<T>类型,这与Result<T,io::Error>相同,但更容易的类型。


# #![allow(unused_variables)]
#fn main() {
fn read_to_string(filename: &str) -> io::Result<String> {
    let mut file = File::open(&filename)?; // <== ?
    let mut text = String::new();
    file.read_to_string(&mut text)?;
    Ok(text)
}
#}

这里的?, 也几乎完全匹配了File::open所做的;如果其结果是一个错误,那么它将立即返回错误。 否则,它将返回Ok结果。 最后,我们仍然需要把该字符串包成一个 Result 类型。

2017 年是一个 Rust 的好年,还有酷酷的?也变得稳定。你也能看到用于旧代码的try!宏:


# #![allow(unused_variables)]
#fn main() {
fn read_to_string(filename: &str) -> io::Result<String> {
    let mut file = try!(File::open(&filename));
    let mut text = String::new();
    try!(file.read_to_string(&mut text));
    Ok(text)
}
#}

总而言之,你可以编写完全安全,且不丑的 Rust 代码,不需要什么异常捕获。

结构{structs},枚举{enums}和匹配{match}

目录

Rust 喜欢 move 它, move 它

我想稍微回退一下,给你看一些惊奇的东西:

// move1.rs
fn main() {
    let s1 = "hello dolly".to_string();
    let s2 = s1;
    println!("s1 {}", s1);
}

我们得到以下错误:

error[E0382]: use of moved value: `s1`
 --> move1.rs:5:22
  |
4 |     let s2 = s1;
  |         -- value moved here
5 |     println!("s1 {}", s1);
  |                      ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`,
  which does not implement the `Copy` trait

Rust 与其他语言有不同的行为。 其他语言的变量总是会引用{references} (如 Java 或 Python),s2成为对引用到s1的字符串对象的又一个引用。 在 C ++ 中,s1是一种值{value},它会 复制s2。 但 Rust 会移动该值。 它没有看到字符串{strings}是具有 可复制性的 (”没有实现 Copy trait”,也就是相应的复制方法,它并没有)。

我们不会看到像数字这样的”原始”类型不能复制,因为它们只是数值; 他们被允许复制,因为他们复制成本堪称便宜。 但,String是已经分配了包含”Hello dolly”的内存,而要复制这内容,将涉及分配更多内存还要复制字符{char}。Rust 才不会静悄悄地做这样的事情。

考虑一个String,它包含”Moby-Dick”的全文。 它不是一个很大的结构,只有文本的内存地址,以及它的大小以及分配块的大小。要复制这String会是昂贵的,因为该内存分配在堆上,和复制品也需要自己的内容分配区。

    String
    | addr | ---------> Call me Ishmael.....
    | size |                    |
    | cap  |                    |
                                |
    &str                        |
    | addr | -------------------|
    | size |

    f64
    | 8 bytes |

第二个值是一个字符串切片 (&str),它与第一个字符串指向相同的内存,再加个大小 - 它仅仅只是(地址)名字。便宜复制!

第三个值是一个f64- 只有 8 个字节。 它不涉及任何其他内存,所以它的复制和移动一样便宜。

复制{Copy}值只能通过它们在内存中的表示来定义,而当 Rust 拷贝时,它只是在其他地方复制这些字节。类似地, 一个没有复制{Copy}值{value}也是 只是移动了{moved}。 与 C ++ 不同的是, Rust 在复制和移动方面没有自作聪明。

译者: 对 具有引用的变量 隐形移动{move}该变量, 在 Rust 是错误的。

用函数调用重写,将显示完全相同的错误:

// move2.rs

fn dump(s: String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(s1);
    println!("s1 {}", s1); // <---error: 'value used here after move'
}

在这里,你有一个选择。 您可以传递对该字符串的引用{&},或者使用它的clone方法来明确拷贝。一般来说,第一种是更好的方法。

fn dump(s: &String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(&s1);
    println!("s1 {}", s1);
}

错误消失。 但你很少看到一个简朴String像这样的引用,因为传递一个字符串文字是非常丑陋的, 还要 涉及创建一个临时字符串。


# #![allow(unused_variables)]
#fn main() {
    dump(&"hello world".to_string());
#}

因此,声明该函数的最佳方式是:


# #![allow(unused_variables)]
#fn main() {
fn dump(s: &str) {
    println!("{}", s);
}
#}

那么, dump(&s1)dump("hello world")这两种情况都会 好好工作。 (这里就是Deref起的作用, Rust 会为你转换&String&str。 )

总而言之,非复制{non-Copy}的分配工作,会将 值 从一个位置移动{move}到另一个位置。不然的话,Rust 将被迫 隐式 做一个副本{copy},并打破 Rust 本身 明确分配 的承诺。

变量的范围

所以,经验法则是更愿意保留对原始数据的引用{&} - 以此来"借用{borrow}"它。

但,一个引用必须 不能 长命过拥有人{owner}!

首先, Rust 是一个 块范围的{block-scoped} 语言。 变量仅在其代码块持续时间内存在:


# #![allow(unused_variables)]
#fn main() {
{
    let a = 10;
    let b = "hello";
    {
        let c = "hello".to_string();
        // a,b 和 c 有
    }
    //  c 没有了
    // a,b  有
    for i in 0..a {
        let b = &b[1..];
        // 原来的 b 不再可见 - 它被罩住了。
    }
    //  b 没有了
    // i 没有了
}
#}

循环变量 (如i) 有点不同,它们只在循环代码块中可见。 创建一个使用相同名称的新变量并不是一个错误 ('覆盖') ,但它可能会造成混淆。

当一个变量’超出范围’,那么它会 扔掉了{dropped}。 任何使用的内存都会被回收,而该变量的其他 资源{resources} 将返回给系统 - 例如,扔掉一个文件{File},就等于关闭它。 这是一件好事。不用的资源在不需要时立即回收。

(另一个 Rust 的特色问题是,变量看起来可能在范围内,但其值已经是移动{move}了的。 )

这里有一个rs1,其引用到tmp值, 而引用只在其区块{}内存在:

01 // ref1.rs
02 fn main() {
03    let s1 = "hello dolly".to_string();
04    let mut rs1 = &s1;
05    {
06        let tmp = "hello world".to_string();
07        rs1 = &tmp; // <==
08    }
09    println!("ref {}", rs1);
10 }

我们先借用{borrow}s1值,然后再借用tmp值。但tmp在(05~08)区块之外就被扔掉了!

error: `tmp` does not live long enough
  --> ref1.rs:8:5
   |
7  |         rs1 = &tmp;
   |                --- borrow occurs here
8  |     }
   |     ^ `tmp` dropped here while still borrowed
9  |     println!("ref {}", rs1);
10 | }
   | - borrowed value needs to live until here

tmp哪里去了? 走了,死了,回到了天空中的堆中,故名: 扔掉了{dropeed}。 Rust 把你从 C 的可怕的’悬挂指针’问题中拯救出来,问题具体就是:一个指向陈旧数据的引用。

在 区块中, rs1-指向-> &tmp, 但在区块结束后, tmp 整个都被 扔掉了{dropped} , 这个时候 rs1 就变成一个指向陈旧(已扔掉)数据的引用。

元组

有时,从函数返回多个值,会非常有用。元组就是一个方便的解决方案:

// tuple1.rs

fn add_mul(x: f64, y: f64) -> (f64,f64) {
    (x + y, x * y)
}

fn main() {
    let t = add_mul(2.0,10.0);

    // 可以 调试打印
    println!("t {:?}", t);

    // 可以 给出值'索引'
    println!("add {} mul {}", t.0,t.1);

    // 可以 _提取_ 值
    let (add,mul) = t;
    println!("add {} mul {}", add,mul);
}
// t (12, 20)
// add 12 mul 20
// add 12 mul 20

元组能包含 不同 类型,这也是它与数组的主要区别。


# #![allow(unused_variables)]
#fn main() {
let tuple = ("hello", 5, 'c');

assert_eq!(tuple.0, "hello");
assert_eq!(tuple.1, 5);
assert_eq!(tuple.2, 'c');
#}

下面出现在一些迭代器{Iterator}方法。 enumerate就像同名的 Python 生成器(generator) 一样:


# #![allow(unused_variables)]
#fn main() {
    for t in ["zero","one","two"].iter().enumerate() {
        print!(" {} {};",t.0,t.1);
    }
    //  0 zero; 1 one; 2 two;
#}

zip会将两个迭代器,组合成一个 包含来自两者的值的元组 的迭代器:


# #![allow(unused_variables)]
#fn main() {
    let names = ["ten","hundred","thousand"];
    let nums = [10,100,1000];
    for p in names.iter().zip(nums.iter()) {
        print!(" {} {};", p.0,p.1);
    }
    //  ten 10; hundred 100; thousand 1000;
#}

结构{Structs}

元组很方便,但是要追踪每个部分的含义,t.1的这种写法不够直接与明了。

Rust 结构 就不同,它包含命名 字段{fields} :

// struct1.rs

struct Person {
    first_name: String,
    last_name: String
}

fn main() {
    let p = Person {
        first_name: "John".to_string(),
        last_name: "Smith".to_string()
    };
    println!("person {} {}", p.first_name,p.last_name);
}

虽然,不应该假定任何特定的内存布局,但是结构体的值将在内存中相邻放置,因为编译器是要高效,而不是节省大小的手段,来组织内存,哦,还有存在填充的可能。

初始化这个结构有点笨拙,所以我们想要把构造一个Person,融入其自身的函数。通过把它放进impl块, 这初始函数可以做成Person的一个 关联函数 :

// struct2.rs

struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

}

fn main() {
    let p = Person::new("John","Smith");
    println!("person {} {}", p.first_name,p.last_name);
}

这个new名字,没有什么魔力或其他东西,随你喜欢。要注意的是,它使用类似 C ++ 进行访问 - 使用双冒号的符号::

下面是个Person 方法,需要一个 自我引用{reference self} 参数:


# #![allow(unused_variables)]
#fn main() {
impl Person {
    ...

    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }

}
...
    println!("fullname {}", p.full_name());
// fullname John Smith
#}

明确使用该self,并作为引用传递。 (你可以把&self想成self: &Person简写。 )

还有,关键字Self(自身:注意首大写)指的是结构类型 - 你可以在脑海中用Person替换掉Self:


# #![allow(unused_variables)]
#fn main() {
    fn copy(&self) -> Self {
        Self::new(&self.first_name,&self.last_name)
    }
#}

方法可以允许修改数据, 用到 可变的自我{mutable self} 参数:


# #![allow(unused_variables)]
#fn main() {
    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }
#}

当使用简单的self参数时,数据会 移动{move}


# #![allow(unused_variables)]
#fn main() {
    fn to_tuple(self) -> (String,String) {
        (self.first_name, self.last_name)
    }
#}

(试试使用&self- 结构不会在没有过争斗的情况下,放开数据!)

注意,v.to_tuple()被调用之后,v已经移动并且不再可用。

总结:

  • 没有self相关参数: 您可以将函数与结构关联,如new“构造函数”。
  • &self参数: 可以使用结构体的值,但不能改变它们。
  • &mut self参数: 可以修改这些值。
  • self参数: 将消耗值,因它移动了。

如果您尝试对Person执行一个调试打印,你会得到一个信息错误:

error[E0277]: the trait bound `Person: std::fmt::Debug` is not satisfied
  --> struct2.rs:23:21
   |
23 |     println!("{:?}", p);
   |                     ^ the trait `std::fmt::Debug` is not implemented for `Person`
   |
   = note: `Person` cannot be formatted using `:?`; if it is defined in your crate,
    add `#[derive(Debug)]` or manually implement it
   = note: required by `std::fmt::Debug::fmt`

编译器提供建议,所以我们放了#[derive(Debug)]Person前面,现在有实用的输出:

Person { first_name: "John", last_name: "Smith" }

指示{directive} 注释会让编译器对Person生成一个 Debug 实现, 简单且有效。对于你的结构来说,这是一个很好的事情,简单加上一句注释,它们就可以打印出来。

译者:该指令注释,是有关 Rust 宏方面的知识,若想了解更多

这是最后的小程序:

// struct4.rs
use std::fmt;

#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

    fn full_name(&self) -> String {
        format!("{} {}",self.first_name, self.last_name)
    }

    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }

    fn to_tuple(self) -> (String,String) {
        (self.first_name, self.last_name)
    }
}

fn main() {
    let mut p = Person::new("John","Smith");

    println!("{:?}", p);

    p.set_first_name("Jane");

    println!("{:?}", p);

    println!("{:?}", p.to_tuple());
    // p has now moved.

}
// Person { first_name: "John", last_name: "Smith" }
// Person { first_name: "Jane", last_name: "Smith" }
// ("Jane", "Smith")

生命周期{Lifetimes}开始咬人啦

通常,结构体包含值,但通常它们还需要包含引用{&}。 假设我们想在一个结构中放置一个字符串切片{&str},而不是一个字符串值。

// life1.rs

#[derive(Debug)]
struct A {
    s: &str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}
error[E0106]: missing lifetime specifier
 --> life1.rs:5:8
  |
5 |     s: &str
  |        ^ expected lifetime parameter

为了理解编译器的投诉,你必须从 Rust 的角度看问题。

如果不知道一个‘引用’的生命周期,是不允许你存储它。 所有引用{&}都是从某个值那里借用{borrowed}的,而且所有的都是有生命周期{lifetimes}的。引用的生命周期不能长于该值的生命周期。Rust 不能允许这种 引用可能突然失效 的情况。

译者: 这时,你可以停一停了,好好想想上面这段话的含义,且自行概略如下问题的答案。 问:值 与 引用 的关系?

现在,字符串切片是从 字符串常量 借用的,像”hello”或是String值。 字符串常量在整个程序期间都存在,也称为”静态{static}”生命周期。

所以,下面写法是有效的 - 我们向 Rust 保证字符串切片,总是指向这静态{static}字符串:

// life2.rs

#[derive(Debug)]
struct A {
    s: &'static str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}
// A { s: "hello dammit" }

确实,这不是最 漂亮 符号,但有时丑,是精确的必要代价。

这也可以用来指明,从函数返回的字符串切片:


# #![allow(unused_variables)]
#fn main() {
fn how(i: u32) -> &'static str {
    match i {
    0 => "none",
    1 => "one",
    _ => "many"
    }
}
#}

这是静态字符串的特殊情况,但应该严格对待。

不过嘛,我们也可以指定引用{&}的生命周期,与结构本身 至少一样长

// life3.rs

#[derive(Debug)]
struct A <'a> { // 注意写法
    s: &'a str
}

fn main() {
    let s = "I'm a little string".to_string(); // string
    let a = A { s: &s }; // <== 结构

    println!("{:?}", a);
}

生命周期{Lifetimes}通常被称为’a’,’b’等,不过您也可以写’我{me}’,随你喜欢,自己知道且简洁就好。

之后看看main函数的内容,我们的a结构和s字符串受到严格的合同约束: a借用了s,并且不能长命过s

接下来,用这个 A 结构体定义,我们想写一个函数,它返回一个A值:


# #![allow(unused_variables)]
#fn main() {
fn makes_a() -> A {
    let string = "I'm a little string".to_string();
    A { s: &string }
}
#}

A 需要一个生命周期 - “要预期的生命周期参数{expected lifetime parameter}”:

  = help: this function's return type contains a borrowed value,
   but there is no value for it to be borrowed from
  = help: consider giving it a 'static lifetime

rustc提供建议,所以我们遵循它:


# #![allow(unused_variables)]
#fn main() {
fn makes_a() -> A<'static> {
    let string = "I'm a little string".to_string();
    A { s: &string }
}
#}

而现在的错误是

8 |      A { s: &string }
  |              ^^^^^^ does not live long enough
9 | }
  | - borrowed value only lives until here

这是无法安全工作的,因为string将在函数结束时被删除,并且引用不可以长命过string

您可以将生命周期参数,视为一个值类型的一部分,会有所帮助。

有时候,结构中包含一个值 从该值借用的引用,看,似乎是个好主意。 但,这基本上是不可能的,因为结构必须是 可移动的,而任何移动都将使引用无效。其实也没有必要这样做 - 例如,如果你的结构有一个字符串字段-string,并且还想要提供切片,那么,它完全可以保留索引,再加个方法,来生成实际的切片。

特点{Traits}

译者: Traits 的 中文意思名字有好几个,但,本质是: 定义结构的一系列行为/方法。

请注意 Rust 不会拼写struct 。 关键字在其他语言中是如此超载,意味着,它有效地击毙了原真的想法。

让我们这样说吧: Rust 结构不能 继承 来自其他结构; 他们都是独特的类型。 没有 sub-typing{子类型} 。他们都是愚蠢的数据.

所以,一个类型之间的关系又应该怎样 呢? 这正是 Traits 的作用。

rustc经常谈到实现{implementing} X 的特点{trait},所以现在恰是讨论 Traits 的时候了。

这里有一个定义 Traits 的例子, 帮特定类型去 实现 它。

// trait1.rs

trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("eight-byte float {}", self)
    }
}

fn main() {
    let answer = 42;
    let maybe_pi = 3.14;
    let s1 = answer.show();
    let s2 = maybe_pi.show();
    println!("show {}", s1);
    println!("show {}", s2);
}
// show four-byte signed 42
// show eight-byte float 3.14

它太酷了; 我们增加了i32f64两者泛型的 一种新方法 !

熟悉 Rust ,就要学习标准库的基本 trait (他们倾向于成群结队)。

非常普遍的有Debug。 我们给Person一个方便的默认实现,#[derive(Debug)],但,假如我们想要一个完整的Person-Debug 实现:


# #![allow(unused_variables)]
#fn main() {
use std::fmt;

impl fmt::Debug for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.full_name())
    }
}
...
    println!("{:?}", p);
    // John Smith
#}

write!是一个非常有用的宏 - 内部的f是实现了Write的东西。 (这也适用于File- 甚至是一个String. )

而,显示{Display}控制如何使用”{}”打印值,当然也要有对应的实现,就像Debug一样。 作为一个有用的副作用,任何实现了Display的,其ToString也自动可用。 所以,如果我们实现了PersonDisplay, p.to_string()也可用了。

Clone定义了clone方法,可简单用”#[deriv(Clone)]”进行定义,如果要所有的字段都实现Clone的话。

示例: 遍历浮点范围的迭代器

之前,我们已经遇到范围表达 (0..n) ,但它们不适用于浮点值。 ( 强行 去做,最终你会得到一个无趣的 1.0。 )

回想一下,迭代器的非正式定义; 它是一个带有结构体,具有一个可能会返回SomeNonenext方法。 在这个过程中,迭代器本身被修改,它保持迭代的状态 (如 next 索引等等)。 迭代的数据通常不会改变, (但,可以参阅Vec::drain,对于修改其数据的有趣迭代器)。

这里是正式的定义: 迭代器(Iterator) trait.


# #![allow(unused_variables)]
#fn main() {
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    ...
}
#}

我们在这里,看到了Iterator trait 的关联类型{associated type}。这个 trait 必须与任意类型合作,所以你必须以某种方式指定返回类型。 方法next可以在不使用特定类型的情况下编写 - 而是通过Self引用该类型参数的Item

f64的迭代器 trait ,是写入Iterator<Item=f64>,它可以理解为:”迭代器的关联类型 Item 设置为 f64”。

至于,...表达语句指的是Iterator提供的方法 。 你只需要定义Itemnext,那该表达语句就可为你所用。

// trait3.rs

struct FRange {
    val: f64,
    end: f64,
    incr: f64
}

fn range(x1: f64, x2: f64, skip: f64) -> FRange {
    FRange {val: x1, end: x2, incr: skip}
}

impl Iterator for FRange {
    type Item = f64;

    fn next(&mut self) -> Option<Self::Item> {
        let res = self.val;
        if res >= self.end {
            None
        } else {
            self.val += self.incr;
            Some(res)
        }
    }
}


fn main() {
    for x in range(0.0, 1.0, 0.1) {
        println!("{} ", x);
    }
}

而相当凌乱的结果是

0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999

这是因为 0.1 不能精确表示为一个浮点数,所以需要一些格式化帮助。 更换成println!


# #![allow(unused_variables)]
#fn main() {
println!("{:.1} ", x);
#}

我们得到更干净的输出 (这个格式的意思是’小数点后一位小数’。 ) 所有默认的迭代器方法都是可用,所以,我们可以将这些值收集到一个向量{Vec}中,通过 map方法来使用它们。等等。


# #![allow(unused_variables)]
#fn main() {
    let v: Vec<f64> = range(0.0, 1.0, 0.1).map(|x| x.sin()).collect();
#}

泛型函数

我们需要一个函数,来抛出实现了Debug的任何值。 以下是对泛型函数的第一次尝试,我们可以在其中传递一个 任何 值类型的引用。T是一个类型参数,需要在函数名称后面声明:


# #![allow(unused_variables)]
#fn main() {
fn dump<T> (value: &T) {
    println!("value is {:?}",value);
}

let n = 42;
dump(&n);
#}

但是, Rust 显然对这种泛型类型T一无所知:

error[E0277]: the trait bound `T: std::fmt::Debug` is not satisfied
...
   = help: the trait `std::fmt::Debug` is not implemented for `T`
   = help: consider adding a `where T: std::fmt::Debug` bound

为了这个工作, 需要告知 Rust ,这个T要实现Debug了的!


# #![allow(unused_variables)]
#fn main() {
fn dump<T> (value: &T)
where T: std::fmt::Debug {
    println!("value is {:?}",value);
}

let n = 42;
dump(&n);
// value is 42
#}

Rust 泛型函数需要 Traits bounds 类型 - 我们在这里说,”T 是实现了 Debug 的任意类型”。 rustc是非常有用的,并且确切地说明需要提供什么界限(bound)。

译者: Traits bounds (特征界限),本质上说: 参数的类型,约束 在,要是实现了对应的 Trait。

现在,Rust 知道这个T的 特征界限,它可以给你敏锐的编译器消息:


# #![allow(unused_variables)]
#fn main() {
struct Foo {
    name: String
}

let foo = Foo{name: "hello".to_string()};

dump(&foo)
#}

错误是:”Foo 没有实现 std::fmt::Debug trait”。

函数在动态语言中已经是泛型的,因为值会带有它们的实际类型,并且类型检查会在运行时发生 - 或者惨败。 对于较大的程序,我们确实想在编译时想知道问题! 这些语言的程序员不应平静地坐在编译器的错误之中,而必须处理程序运行时,才会出现的问题。 墨菲定律,告诉我们这些问题往往会发生在 最不方便/灾难性 的时刻。

平方数的操作函数是泛型的: x * x要适用整数,浮点数和任意知道关于乘法运算符*的类型。 但是,其类型界限又是什么?

// gen1.rs

fn sqr<T> (x: T) -> T {
    x * x
}

fn main() {
    let res = sqr(10.0);
    println!("res {}",res);
}

第一个问题是 Rust 不知道T可以做乘法:

error[E0369]: binary operation `*` cannot be applied to type `T`
 --> gen1.rs:4:5
  |
4 |     x * x
  |     ^
  |
note: an implementation of `std::ops::Mul` might be missing for `T`
 --> gen1.rs:4:5
  |
4 |     x * x
  |     ^

遵循编译器的建议,让我们使用这个 Traits限制该类型参数,这个 Traits 用来实现乘法运算符*:


# #![allow(unused_variables)]
#fn main() {
fn sqr<T> (x: T) -> T
where T: std::ops::Mul {
    x * x
}
#}

仍,不起作用:

rror[E0308]: mismatched types
 --> gen2.rs:6:5
  |
6 |     x * x
  |     ^^^ expected type parameter, found associated type
  |
  = note: expected type `T`
  = note:    found type `<T as std::ops::Mul>::Output`

rustc是说有关x * x的类型,是T::Output关联类型,而不是T。 实际上,x * xx类型没有道理是相同的,例如,两个向量的积是一个标量。


# #![allow(unused_variables)]
#fn main() {
fn sqr<T> (x: T) -> T::Output
where T: std::ops::Mul {
    x * x
}
#}

现在的错误是:

error[E0382]: use of moved value: `x`
 --> gen2.rs:6:7
  |
6 |     x * x
  |     - ^ value used here after move
  |     |
  |     value moved here
  |
  = note: move occurs because `x` has type `T`, which does not implement the `Copy` trait

所以,我们需要进一步限制类型!


# #![allow(unused_variables)]
#fn main() {
fn sqr<T> (x: T) -> T::Output
where T: std::ops::Mul + Copy {
    x * x
}
#}

(终于) 起作用了。要冷静地倾听编译器,每次都会让你更接近原力点,... 终会流畅编译。

确实, 在 C ++ 中, 更简单一点:

template <typename T>
T sqr(x: T) {
    return x * x;
}

但, (说实话) C ++ 在这里采用了牛仔策略。C ++ 的模板{template}错误很不好,因为,编译器都知道的所有, (最终) 是某些操作符或方法没有被定义。 C ++ 委员会知道这是一个问题,所以他们正在努力让concepts工作起来,这与 Rust 中的trait约束类型参数非常相似。

Rust 泛型函数,一开始可能看起来有点难接受,但是,显式,就是明确定义,就能确切地知道可以安全地提供哪种值。

这些函数是 单态{monomorphic} 调用的,与 多态{polymorphic} 合作。 函数的主体都会为每个 唯一类型 分别编译的。通过多态函数,相同的机器代码可以与每种匹配类型一起工作, 动态地 调度{dispatching} 正确的方法。

Monomorphic生成更快的代码,专用于特定类型,并且,常是 内联{inlined} 起来。所以,当sqr(x)被看到,它会被有效地用x * x取代。 缺点是,大的泛型函数为每一种可能导致的类型,产生大量的代码,引起 代码膨胀。但与往常一样,总是有折衷的方式; 有经验的人学会为工作,做出正确的选择。

简单的枚举

枚举{enums}类型具有一些确定的值。 例如,一个方向只有四个可能的值。(上下左右)


# #![allow(unused_variables)]
#fn main() {
enum Direction {
    Up,
    Down,
    Left,
    Right
}
...
    // `start` is type `Direction`
    let start = Direction::Left;
#}

可以在枚举上定义方法,就像结构一样。 该match表达语句是处理enum值的基本方式。


# #![allow(unused_variables)]
#fn main() {
impl Direction {
    fn as_str(&self) -> &'static str {
        match *self { // *self 有 Direction 类型
            Direction::Up => "Up",
            Direction::Down => "Down",
            Direction::Left => "Left",
            Direction::Right => "Right"
        }
    }
}
#}

标点符号很重要。 注意match后面的self之前的*。 很容易忘记,因为 Rust 经常会推断它 (我们说self.first_name,而不是(*self).first_name)。 但是,匹配{matching}是更精确的工作。若将它排除在外,会产生一大堆消息,这些消息可归结为这种类型的不匹配:

   = note: expected type `&Direction`
   = note:    found type `Direction`

这是因为self&Direction类型,所以我们必须投入* 遵循 该值。

像结构一样,枚举可以实现 traits,我们的朋友#[derive(Debug)],可以添加到Direction:


# #![allow(unused_variables)]
#fn main() {
        println!("start {:?}",start);
        // start Left
#}

所以,as_str方法并不是真的必要,因为我们总是可以从Debug得到名字。 (但as_str不分配{not allocate} ,这可能很重要。)

你不应该在这里,假设任何特定的顺序 - 这里没有默许的”起始”整数值。

这里有一个方法,来定义每个方向值的’后继者’。 非常方便的通配符用法,将枚举名称暂时放入方法上下文中:


# #![allow(unused_variables)]
#fn main() {
    fn next(&self) -> Direction {
        use Direction::*; // <===
        match *self {
            Up => Right,
            Right => Down,
            Down => Left,
            Left => Up
        }
    }
    ...

    let mut d = start;
    for _ in 0..8 {
        println!("d {:?}", d);
        d = d.next();
    }
    // d Left
    // d Up
    // d Right
    // d Down
    // d Left
    // d Up
    // d Right
    // d Down
#}

结果就是,这个特定的,任意的顺序中,各个方向一直循环。 它 (事实上)是非常简单的状态机器。

这些枚举值,无法比较:

assert_eq!(start, Direction::Left);

error[E0369]: binary operation `==` cannot be applied to type `Direction`
  --> enum1.rs:42:5
   |
42 |     assert_eq!(start, Direction::Left);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: an implementation of `std::cmp::PartialEq` might be missing for `Direction`
  --> enum1.rs:42:5

解决办法就是,在enum Direction前面加上#[derive(Debug,PartialEq)]

这是一个重点 - Rust 用户定义的类型一开始就是这么新鲜和朴素。

你通过实现共同的 traits ,给予他们合理的默认行为。这也适用于结构 - 如果你要求 Rust 为一个结构体 derive PartialEq,它会做出同样合理的事情,但要,假设所有的字段都实现它,并构建了一个对照结果。 如果不是这样,或者你想重新定义相等性质,那么你可以明确地自定义PartialEq

Rust 也有’C 风格的枚举’:

// enum2.rs

enum Speed {
    Slow = 10,
    Medium = 20,
    Fast = 50
}

fn main() {
    let s = Speed::Slow;
    let speed = s as u32;
    println!("speed {}", speed);
}

它们用一个整数值进行初始化,并可以通过类型转换(as),将其转换为整数。

你只需要给名字一个值,然,每次自动增加一个值:


# #![allow(unused_variables)]
#fn main() {
enum Difficulty {
    Easy = 1,
    Medium,  // is 2
    Hard   // is 3
}
#}

顺便说一下,枚举内字段的’名字’一词太模糊了,就像一直在说’物质’。 这里的合适名词,是 变种{variant} - Speed枚举有SlowMediumFast的变种。

这些枚举 有一个自然的顺序,但你必须问得好。在enum Speed前面放置#[derive(PartialEq,PartialOrd)]之后,Speed::Fast > Speed::SlowSpeed::Medium != Speed::Slow才是对的。

枚举的全部荣耀

完全形式的 rust 类似于类固醇上的 C 联盟,like a Ferrari compared to a Fiat Uno。考虑以 类型-安全的方式 存储不同值的问题。

// enum3.rs

#[derive(Debug)]
enum Value {
    Number(f64),
    Str(String),
    Bool(bool)
}

fn main() {
    use Value::*;
    let n = Number(2.3);
    let s = Str("hello".to_string());
    let b = Bool(true);

    println!("n {:?} s {:?} b {:?}", n,s,b);
}
// n Number(2.3) s Str("hello") b Bool(true)

同样,这个枚举只能包含这些值的 一个 ;其大小将是 最大变体 的大小。

到目前为止,并不是真正的超级跑车,虽然枚举知道如何打印出来是很酷的。 但,他们也知道它们包含的 哪一种 值,和 还有 match的超级力量:


# #![allow(unused_variables)]
#fn main() {
fn eat_and_dump(v: Value) {
    use Value::*;
    match v {
        Number(n) => println!("number is {}", n),
        Str(s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}
....
eat_and_dump(n);
eat_and_dump(s);
eat_and_dump(b);
//number is 2.3
//string is 'hello'
//boolean is true
#}

(而这就是OptionResult的本质 - 都是枚举。)

我们喜欢这个eat_and_dump函数,但我们希望将该值作为引用传递,因为当前移动{move}了,并且该值被’吃掉’了:


# #![allow(unused_variables)]
#fn main() {
fn dump(v: &Value) {
    use Value::*;
    match *v {  // type of *v is Value
        Number(n) => println!("number is {}", n),
        Str(s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}

error[E0507]: cannot move out of borrowed content
  --> enum3.rs:12:11
   |
12 |     match *v {
   |           ^^ cannot move out of borrowed content
13 |     Number(n) => println!("number is {}",n),
14 |     Str(s) => println!("string is '{}'",s),
   |         - hint: to prevent move, use `ref s` or `ref mut s`
#}

这次, 你无法处理借用引用。 Rust 不会让你 提取 包含在原始值中的字符串。 它没有抱怨Number,因为它很高兴复制f64,但是String是没有实现Copy的。

我之前提到过,match对精确 类型 是挑剔的,在这里,我们按照提示进行操作(加 ref); 现在,我们只是借用对包含字符串的引用。

译者: 据我了解,现 rustc 编译器已不再提示这个示例的错误,因它自行修正了此错误。2019.2.24


# #![allow(unused_variables)]
#fn main() {
fn dump(v: &Value) {
    use Value::*;
    match *v {
        Number(n) => println!("number is {}", n),
        Str(ref s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}
    ....

    dump(&s);
    // string is 'hello'
#}

在我们继续前进之前,感受下 Rust 编译成功的欣快感,也让我们暂停一下。rustc在生成足够上下文,以供人类使用非常优秀的 修正 错误,却不一定要 理解 错误。现在我们来理解下。

这个问题是 match 的正确性,以及 借用检查者阻止任何违反规则的企图的结合。 其中一条规则是你不能抽出所属某种拥有类型的值。 C ++ 的一些知识在这里是一个障碍,因为 C ++ 会用复制它的方式绕过这个问题,甚至还 说得通

如果你尝试从一个 Vec 中抽出一个字符串,你会得到完全相同的错误,也就是*v.get(0).unwrap() (因为索引返回的是引用,所以使用*。 ),而它不会让你这样做。 (有时在这种情况下,clone并不是一个坏的解决方案。)

(顺便一提,正是出于这个原因,v[0]不适用于像字符串这样的非可复制值。 你必须借用&v[0]或使用 v[0].clone() 复制来达到目的)

至于match,你可以看到Str(s)=>,其作为Str(s: String)=>的简称。 局部变量(通常称为一个 绑定 值 ) 被创建。 当你吃掉一个值,并提取其内容时,通常推断的类型是可行,但我们真正需要的是s: &String,而ref暗示,可以确保这一点: 我们只是想借用该字符串。

在这里,我们确实想提取该字符串,并且不关心之后的枚举值。 _像往常一样会匹配任何东西。


# #![allow(unused_variables)]
#fn main() {
impl Value {
    fn to_str(self) -> Option<String> {
        match self {
        Value::Str(s) => Some(s),
        _ => None
        }
    }
}
    ...
    println!("s? {:?}", s.to_str());
    // s? Some("hello")
    // println!("{:?}", s) // error! s has moved...
#}

函数命名很重要 - 这叫做to_str,而不是as_str。 你可以编写一个方法,借用该字符串,作为(as)一个Option<&String> (这个引用需要与 枚举变量 具有相同的生命周期。 ) ,这样,你就不能命名为to_str

你也可以写to_str - 它完全等价的:


# #![allow(unused_variables)]
#fn main() {
    fn to_str(self) -> Option<String> {
        if let Value::Str(s) = self {
            Some(s)
        } else {
            None
        }
    }
#}

关于匹配的 更多

回想一下,元组的值可以用’()’来提取:


# #![allow(unused_variables)]
#fn main() {
    let t = (10,"hello".to_string());
    ...
    let (n,s) = t;
    // t 已 移动了. 不再存在
    // n 是 i32, s 是 String
#}

这是一个 解构{destructuring} 特例; 我们有一些数据,希望将其分开来 (像这里) ,或只是借用它的值。无论哪种方式,我们都可以得到结构的各个部分。

语法与在match中使用的相似。 在这里,我们明确地借用了这些值。


# #![allow(unused_variables)]
#fn main() {
    let (ref n,ref s) = t;
    // n 和 s 从 t 那里借用. t 还存在!
    // n 是 &i32, s 是 &String
#}

解构与结构一起工作:


# #![allow(unused_variables)]
#fn main() {
    struct Point {
        x: f32,
        y: f32
    }

    let p = Point{x:1.0,y:2.0};
    ...
    let Point{x,y} = p;
    // p 还在, 直到 x 和 y 已复制
    // x 和 y 都是 f32
#}

下面时间,看看match的新模式。前两种模式与let解构相同 - 它只匹配第一个元素为零的元组,和一个 任何 字符串; 第二个模式增加了一个if,所以它只匹配(1, "hello")。 最后,只是一个匹配 任何 的 变量。但,如果match要应用一个表达式,而你不希望将变量绑定到该表达式,那会被忽略的_就会很有用,这是一个match结尾的常用方法。


# #![allow(unused_variables)]
#fn main() {
fn match_tuple(t: (i32,String)) {
    let text = match t {
        (0, s) => format!("zero {}", s),
        (1, ref s) if s == "hello" => format!("hello one!"),
        tt => format!("no match {:?}", tt),
        // 或 使用  _ => format!("no match")
        // 若你对变量不感兴趣。
     };
    println!("{}", text);
}
#}

为什么该函数不匹配match_tuple((1,"hello"))? 匹配是一个精确的工作,而编译器会抱怨:

  = note: expected type `std::string::String`
  = note:    found type `&'static str`

我们为什么需要ref s? 如果你有一个需要借用的if-守卫,这时存在个稍微隐晦的问题 (查找 E0008 错误),因为如果 if-守卫 是在不同的上下文中发生,就会发生移动。这是隐晦漏洞的示例情况。

译者 TODO: 添加 E0008 错误的中文翻译

如果类型 &str,那么我们直接匹配它:


# #![allow(unused_variables)]
#fn main() {
    match (42,"answer") {
        (42,"answer") => println!("yes"),
        _ => println!("no")
    };
#}

match用到if let的情况。这有个很酷的例子,因为如果我们得到一个Some,我们可以匹配里面的,只从元组中提取字符串。 所以在这里没有必要嵌套if let表达式。我们用_,因为我们对元组的第一部分不感兴趣。


# #![allow(unused_variables)]
#fn main() {
    let ot = Some((2, "hello".to_string());

    if let Some((_,ref s)) = ot {
        assert_eq!(s, "hello");
    }
    // 我们只是借用该字符串, 而不是 '不可挽回地破坏结构'
#}

使用parse时会出现一个有趣的问题 (或任何需要从上下文中,计算出其返回类型 的函数)


# #![allow(unused_variables)]
#fn main() {
    if let Ok(n) = "42".parse() {
        ...
    }
#}

那么,这n是什么类型的? 不管怎样,你必须提供一个提示 - 什么样的整数?它是否是一个整数?


# #![allow(unused_variables)]
#fn main() {
    if let Ok(n) = "42".parse::<i32>() {
        ...
    }
#}

这种不太优雅的语法被称为”涡轮运算符{turbofish operator}”.

如果你有正在返回Result的一个函数,那么问号运算符提供了一个更加优雅的解决方案:


# #![allow(unused_variables)]
#fn main() {
    let n: i32 = "42".parse()?;
#}

但是,解析错误需要转换为Result的错误变种,这是我们稍后讨论时要讨论的话题-6.错误处理.

闭包{Closures}

Rust 的很多力量来源于 闭包。 它们最简单的形式就像快捷函数一样:


# #![allow(unused_variables)]
#fn main() {
    let f = |x| x * x;

    let res = f(10);

    println!("res {}", res);
    // res 100
#}

在这个例子中没有明确的类型 - 一切都是从整数常量 10 ,开始推导出来的。

如果我们运行,会收到f具有不同类型的错误 - Rust 已经决定f必须在整数类型上调用:

    let res = f(10);

    let resf = f(1.2);
  |
8 |     let resf = f(1.2);
  |                  ^^^ expected integral variable, found floating-point variable
  |
  = note: expected type `{integer}`
  = note:    found type `{float}`

所以,第一次调用修复了参数的类型x。这相当于这个函数:


# #![allow(unused_variables)]
#fn main() {
    fn f (x: i32) -> i32 {
        x * x
    }
#}

但,函数和闭包之间存在很大差异,具体 体现 在明确类型的需要。 这里,我们先执行一个线性函数:


# #![allow(unused_variables)]
#fn main() {
    let m = 2.0;
    let c = 1.0;

    let lin = |x| m*x + c;

    println!("res {} {}", lin(1.0), lin(2.0));
    // res 3 5
#}

你不能用明确的fn形式 - 因它不知道闭包范围内的变量。闭包函数是从其上下文 借用了 mc

现在,这lin是什么类型? 只有rustc知道。 在引擎盖下,闭包是一个 结构 ,且是可调用的 (’实现调用操作符’) 。它的行为就好像这样写出来的:


# #![allow(unused_variables)]
#fn main() {
struct MyAnonymousClosure1<'a> {
    m: &'a f64,
    c: &'a f64
}

impl <'a>MyAnonymousClosure1<'a> {
    fn call(&self, x: f64) -> f64 {
        self.m * x  + self.c
    }
}
#}

当然,编译器就出来做事了,把简单的闭包语法变成完整的代码! 你需要知道的是,闭包为一个 结构 和它 借用 来自其环境的值。因此它有一个 lifetime

所有闭包都是独特的类型,但它们有共同的 traits。 所以即使我们不知道确切的类型,我们知道泛型约束:


# #![allow(unused_variables)]
#fn main() {
fn apply<F>(x: f64, f: F) -> f64
where F: Fn(f64)->f64  {
    f(x)
}
...
    let res1 = apply(3.0,lin);
    let res2 = apply(3.14, |x| x.sin());
#}

子曰: applyT这样的 任何 且具备Fn(f64) -> f64的类型工作 - 也就是说,这是一个需要f64并返回f64的函数。

运行apply(3.0,lin)后,试图访问lin会给出一个有趣的错误:

    let l = lin;
error[E0382]: use of moved value: `lin`
  --> closure2.rs:22:9
   |
16 |     let res = apply(3.0,lin);
   |                         --- value moved here
...
22 |     let l = lin;
   |         ^ value used here after move
   |
   = note: move occurs because `lin` has type
    `[closure@closure2.rs:12:15: 12:26 m:&f64, c:&f64]`,
     which does not implement the `Copy` trait

就是这样,apply吃了我们的闭包函数。 还有,这个结构的实际类型,rustc会弥补实现它。 始终,将闭包视为结构是有帮助的。

调用一个闭包就是一个 方法调用: 三种函数 trait 对应于三种方法:

  • Fn 结构传递为&self
  • FnMut 结构传递为&mut self
  • FnOnce 结构传递为self

所以,闭包可能会改变它的 来自上层 引用:


# #![allow(unused_variables)]
#fn main() {
    fn mutate<F>(mut f: F)
    where F: FnMut() {
        f()
    }
    let mut s = "world";
    mutate(|| s = "hello");
    assert_eq!(s, "hello");
#}

注意mut-f需要可变来工作.

但是,你无法逃避借用规则。考虑一下:


# #![allow(unused_variables)]
#fn main() {
let mut s = "world";

// 闭包搞了个 s 的 可变借用
let mut changer = || s = "world";

changer();
// 再搞个 s 不可变借用
assert_eq!(s, "world");
#}

无法完成! 错误是:在 assert 声明中,我们不能借用s,因为它之前作为可变借用,已经被闭包changer搞走了。 只要闭包存在,其他代码就不能访问s,所以解决方案是通过将闭包放在一个 有限的范围 内,来控制这个生命周期:


# #![allow(unused_variables)]
#fn main() {
let mut s = "world";
{
    let mut changer = || s = "world";
    changer();
}
assert_eq!(s, "world");
#}

在这一点上,如果你习惯了 JavaScript 或 Lua 等语言,你可能会感到 Rust 闭包的复杂性,而不是在这些语言中的直截了当。 这正是 Rust 承诺不作出任何分配的必要成本。 在 JavaScript 中,等效的mutate(function() {s = "hello";}),将始终,导致动态分配闭包。

有时,你不希望闭包借用这些变量,而是 移动 他们。


# #![allow(unused_variables)]
#fn main() {
    let name = "dolly".to_string();
    let age = 42;

    let c = move || {
        println!("name {} age {}", name,age);
    };

    c();

    println!("name {}",name);
#}

最后的错误println是: “使用了移动值: name“,所以这里有一个解决方案 - 如果我们 想保持 name活着 - 就将 复制的副本 移入闭包{move}中:


# #![allow(unused_variables)]
#fn main() {
    let cname = name.to_string();
    let c = move || {
        println!("name {} age {}",cname,age);
    };
#}

为什么需要移动的闭包? 因为我们可能需要在 原始上下文不再存在 的地方调用它们。 经典案例是创建一个 thread{线程}。 移动的闭包不借用,就没有生命周期。

移动后, 线程中, 所使用的变量, 就会与 原上下文 没有关系了。

迭代器方法中,主要使用闭包。 回想一下,我们定义的遍历一系列浮点数的range迭代器。使用闭包对此 (或任何其他迭代器) 进行操作都很简单:


# #![allow(unused_variables)]
#fn main() {
    let sine: Vec<f64> = range(0.0,1.0,0.1).map(|x| x.sin()).collect();
#}

map没有在 Vec 上定义 (尽管,很容易创建一个这样的 trait),因为那样的话, 每次 map 都将创建一个新的 Vec。就这样,选择很明显了。

这个sum,不存在创建临时对象:


# #![allow(unused_variables)]
#fn main() {
 let sum: f64 = range(0.0,1.0,0.1).map(|x| x.sin()).sum();
#}

它 (事实上) 会像明确的循环一样快! 如果 Rust 闭包与 Javascript 闭包一样”没有摩擦火花”,那么这种性能保证就不可能。

filter是另一种有用的迭代器方法 - 它只允许,通过匹配条件的值:


# #![allow(unused_variables)]
#fn main() {
    let tuples = [(10,"ten"),(20,"twenty"),(30,"thirty"),(40,"forty")];
    let iter = tuples.iter().filter(|t| t.0 > 20).map(|t| t.1);

    for name in iter {
        println!("{} ", name);
    }
    // thirty
    // forty
#}

三种迭代器

三种类型 (再次) 对应于三种基本参数类型。

假设我们有一个String值的 Vec 。以下是明确的迭代器类型,和 隐式{implicitly},以及迭代器返回的实际类型。


# #![allow(unused_variables)]
#fn main() {
for s in vec.iter() {...} // &String
for s in vec.iter_mut() {...} // &mut String
for s in vec.into_iter() {...} // String

// 隐式!
for s in &vec {...} // &String
for s in &mut vec {...} // &mut String
for s in vec {...} // String
#}

就我个人而言,我更喜欢明确,但,了解这两种形式及其含义是非常重要的。

into_iter 消耗 Vec ,并提取它的字符串,所以之后 Vec 不再可用 - 它已被移动。 这是 Pythonistas 过去常说的一个确定问题for s in vec!

所以,隐含的形式for s in &vec通常才是你想要的,就像&T在向函数传递参数时,是一个很好的默认值。

理解这三种类型是如何工作是很重要的,因为 Rust 严重依赖于类型推导 - 在闭包参数中,你不会经常看到明确的类型。 这是一件好事, 因为如果所有这些类型都明确的话, 它的 写法 会很嘈杂。 当然,这个紧凑的代码的代价,是你需要知道隐式类型究竟是什么!

map取得迭代器返回的任何值,并将其转换为其他值,但是filter需要的是一个该值的 引用。 在这种正在使用iter的情况下,迭代器 item 的类型是&String。 注意filter接收的是这种类型的引用.


# #![allow(unused_variables)]
#fn main() {
for n in vec.iter().map(|x: &String| x.len()) {...} // n 是 usize
....
}

for s in vec.iter().filter(|x: &&String| x.len() > 2) { // s 是 &String
...
}
#}

在调用方法(如:x.len())时, Rust 会自动 解引用,所以问题不明显。 但|x:&& String|x ==”one”|将 不会 工作, 因为操作符号对 类型匹配 更加严格。 rustc会抱怨&&String&str没有这样进行比较的。 所以你需要明确的 解引用 ,让&&String变成能 完成 比较 的&String


# #![allow(unused_variables)]
#fn main() {
for s in vec.iter().filter(|x: &&String| *x == "one") {...}
// 等价的隐式写法:
for s in vec.iter().filter(|x| *x == "one") {...}
#}

如果省略显式类型,则可以修改参数,使s的类型就是现在的&String:


# #![allow(unused_variables)]
#fn main() {
for s in vec.iter().filter(|&x| x == "one")
#}

看你如何看待它。

具有动态数据的结构

一个最强大的技术是 一个包含对自身引用的结构

这里是一个 二叉树 的基本构建块,用 C 语言 表示 (每个人最喜欢的老亲戚都喜欢使用没有保护的电动工具。 )


# #![allow(unused_variables)]
#fn main() {
    struct Node {
        const char *payload;
        struct Node *left;
        struct Node *right;
    };
#}

你不能 直接{directly} 在 Rust 这样做 - 包含Node字段,因为Node的大小取决于Node的大小... 它无法计算。 所以我们使用指针指向Node结构,因为指针的大小总是已知的。

如果left不是NULL,那Node将有一个left字段,其指向另一个节点,一直无限下去。

Rust 不会NULL (至少不 安全) , 所以这显然是一份Option的工作。 但你,不能只是把一个Node放在Option里面,因为我们不知道Node的大小 (等等)。 这又是Box的工作,因为它分配了包含一个指向数据的指针,并且一直具有固定大小。

所以这里是 Rust 的等价物,使用type创建一个别名:


# #![allow(unused_variables)]
#fn main() {
type NodeBox = Option<Box<Node>>;

#[derive(Debug)]
struct Node {
    payload: String,
    left: NodeBox,
    right: NodeBox
}
#}

( Rust 以这种方式解决问题 - 不需要前瞻性声明。 )

下面,第一个测试程序:

impl Node {
    fn new(s: &str) -> Node {
        Node{payload: s.to_string(), left: None, right: None}
    }

    fn boxer(node: Node) -> NodeBox {
        Some(Box::new(node))
    }

    fn set_left(&mut self, node: Node) {
        self.left = Self::boxer(node);
    }

    fn set_right(&mut self, node: Node) {
        self.right = Self::boxer(node);
    }

}


fn main() {
    let mut root = Node::new("root");
    root.set_left(Node::new("left"));
    root.set_right(Node::new("right"));

    println!("arr {:#?}", root);
}

由于”{:#?}” (’#’表示’扩开’) ,输出结果非常漂亮.

root Node {
    payload: "root",
    left: Some(
        Node {
            payload: "left",
            left: None,
            right: None
        }
    ),
    right: Some(
        Node {
            payload: "right",
            left: None,
            right: None
        }
    )
}

现在, root变量若被丢弃会发生什么 ? 所有字段都被删除; 如果树的”分支”被丢弃,就会扔掉 它们 的字段等等。 Box::new可能是最接近new关键字的呢,但我们没有必要delete要么free

我们现在必须为这棵树制定一个用法。请注意,可以指定字符串 顺序: ‘bar’<‘foo’,’abba’>’aardvark’; 所谓的”字母顺序”。 (严格来说,这是词汇顺序,因为人类语言非常多样化,并且有着奇怪的规则。)

这是一个按字符串的顺序,插入节点的方法。我们将新数据与当前节点进行比较 - 如果较少,则尝试插入左侧,否则尝试插入右侧。 左边可能没有节点,那么就set_left等等。

    fn insert(&mut self, data: &str) {
        if data < &self.payload {
            match self.left {
                Some(ref mut n) => n.insert(data),
                None => self.set_left(Self::new(data)),
            }
        } else {
            match self.right {
                Some(ref mut n) => n.insert(data),
                None => self.set_right(Self::new(data)),
            }
        }
    }

    ...
    fn main() {
        let mut root = Node::new("root");
        root.insert("one");
        root.insert("two");
        root.insert("four");

        println!("root {:#?}", root);
    }

注意match- 我们会提供一个可变的引用给到 box,如果OptionSome的话,并应用insert方法。 否则,我们需要为左侧创建一个新的Node等等。 Box是一个 聪明 指针; 请注意,不需要”拆箱{unboxing}”来呼叫Node方法!

这里是输出树:

root Node {
    payload: "root",
    left: Some(
        Node {
            payload: "one",
            left: Some(
                Node {
                    payload: "four",
                    left: None,
                    right: None
                }
            ),
            right: None
        }
    ),
    right: Some(
        Node {
            payload: "two",
            left: None,
            right: None
        }
    )
}

比,其他字符串’小于’的字符串放在左侧,,则放在右侧。

参观时间。 这是 按顺序遍历 - 我们访问左边,在节点上做点什么,然后访问右边。


# #![allow(unused_variables)]
#fn main() {
    fn visit(&self) {
        if let Some(ref left) = self.left {
            left.visit();
        }
        println!("'{}'", self.payload);
        if let Some(ref right) = self.right {
            right.visit();
        }
    }
    ...
    ...
    root.visit();
    // 'four'
    // 'one'
    // 'root'
    // 'two'
#}

所以,我们按顺序访问这些字符串! 请注意重复出现的ref - if let使用与match完全相同的规则。

泛型结构

考虑前面的二叉树的例子。 这将是 严重刺激 ,不得不重写它, 当为了所有可能的 payload 类型。 所以,我们的泛型Node与它的类型参数T.


# #![allow(unused_variables)]
#fn main() {
type NodeBox<T> = Option<Box<Node<T>>>;

#[derive(Debug)]
struct Node<T> {
    payload: T,
    left: NodeBox<T>,
    right: NodeBox<T>
}
#}

该实现显示了语言之间的差异。 payload 的基本操作是比较,所以 T 必须与之相当< ,等等, 实现 PartialOrd。 必须在impl其中声明类型参数:

impl <T: PartialOrd> Node<T> {
    fn new(s: T) -> Node<T> {
        Node{payload: s, left: None, right: None}
    }

    fn boxer(node: Node<T>) -> NodeBox<T> {
        Some(Box::new(node))
    }

    fn set_left(&mut self, node: Node<T>) {
        self.left = Self::boxer(node);
    }

    fn set_right(&mut self, node: Node<T>) {
        self.right = Self::boxer(node);
    }

    fn insert(&mut self, data: T) {
        if data < self.payload {
            match self.left {
                Some(ref mut n) => n.insert(data),
                None => self.set_left(Self::new(data)),
            }
        } else {
            match self.right {
                Some(ref mut n) => n.insert(data),
                None => self.set_right(Self::new(data)),
            }
        }
    }
}


fn main() {
    let mut root = Node::new("root".to_string());
    root.insert("one".to_string());
    root.insert("two".to_string());
    root.insert("four".to_string());

    println!("root {:#?}", root);
}

所以,泛型结构要像 C ++ 一样,需要在 <> 中指定泛型类型参数(们)。 Rust 通常很聪明,可以从上下文中得出这个类型参数 - 它知道它有一个Node<T>,还知道它的insert方法需要T参数。 insert 的第一次运行,会把T钉成为String。如果有任何进一步的运行不一致,它会投诉。

但是,你确实需要适当地限制这种类型!

文件系统和进程

目录

再看看读取文件

第 1 部分的末尾,我展示了如何读取整个文件到一个字符串。 自然,这并不总是一个好法子,所以,现在介绍下如何逐行读取文件。

fs::File实现了io::Read,这是一个具备可读性的 trait 。 这个 trait 定义了一个能填充u8切片字节的read方法 - 唯一 要求 的方法,还免费提供一些方法,很像 Iterator。 您可以使用read_to_end填充可读的内容 到 字节 Vec, 还有read_to_string可以填充到 一个 string - 必须是 utf-8 编码。

这是一个’原始’读取,没有缓冲区。 对于缓冲性读取, 我们有io::BufRead trait,给了我们 read_line 和 一个lines迭代器。io::BufReader将给 任何 具备可读性的类型 提供io::BufRead实现。

fs::File 实现了io::Write

确保所有这些 traits 可用的最简单方法是,use std::io::prelude::*


# #![allow(unused_variables)]
#fn main() {
use std::fs::File;
use std::io;
use std::io::prelude::*;

fn read_all_lines(filename: &str) -> io::Result<()> {
    let file = File::open(&filename)?;

    let reader = io::BufReader::new(file);// 实现`io::BufRead`

    for line in reader.lines() {
        let line = line?;
        println!("{}", line);
    }
    Ok(())
}
#}

这里的let line = line?看起来可能有点奇怪。迭代器返回的line实际上是一个io::Result<String>,我们用?解开它。因为在迭代过程中可能 出现错误,如: I/O 错误,不是 utf-8 的字节块,等等。

lines作为一个迭代器,可以直接使用collect从一个文件读取为一个字符串向量,或者用enumerate迭代器打印带行号的 line。

然而,这并不是读取 所有行 的最有效方式,因为每行都要分配一个新字符串,有成本。使用read_line效率更高,虽然更难看些。请注意,返回的行是包含换行符,可以使用trim_right进行移除。


# #![allow(unused_variables)]
#fn main() {
    let mut reader = io::BufReader::new(file);
    let mut buf = String::new();
    while reader.read_line(&mut buf)? > 0 {
        {
            let line = buf.trim_right();
            println!("{}", line);
        }
        buf.clear();
    }
#}

分配内存的举动少得很多,因为字符串不会释放其分配的内存, clearing{清除} 也只是缓存区; 一旦字符串有足够的容量,不会再有分配。

这是我们使用一个块{}来控制单一借用的情况。linebuf的借用,而这个借用必须在我们修改buf之前完结。Rust 再一次试图阻止我们做一些愚蠢的事情,那就是 我们已经清除了缓冲区 ,访问line。(借用检查者有时会有所限制,Rust 由于”非词汇生命周期{non-lexical lifetimes}”,它会分析代码并看到,在buf.clear()之后line是不使用的。)

完成的不是很漂亮。 虽然我不能给你一个,能返回缓冲区引用的完全迭代器,但我可以给你一些 看起来像 一个迭代器的东西。

首先定义一个泛型结构; 类型参数R是’任意实现 Read 的类型’。结构包含读者{reader} 和我们要借用的缓冲区{buf}。


# #![allow(unused_variables)]
#fn main() {
// file5.rs
use std::fs::File;
use std::io;
use std::io::prelude::*;

struct Lines<R> {
    reader: io::BufReader<R>,
    buf: String
}

impl <R: Read> Lines<R> {
    fn new(r: R) -> Lines<R> {
        Lines{reader: io::BufReader::new(r), buf: String::new()}
    }
    ...
}
#}

然后是next方法。 它返回一个Option- 就像一个迭代器,当它返回None时,迭代器结束。这返回的类型嵌套个Result是因为read_line可能会失败,我们永远不要错失错误。 所以如果失败了,我们把它的错误包进Some<Result>。 否则,文件的自然结束时,它可能读取到零字节 - 而零字节不是错误,只是一个None

此时,缓冲区包含附有换行符 (\n) 的行,修剪掉它,然后打包成字符串切片。


# #![allow(unused_variables)]
#fn main() {
    fn next<'a>(&'a mut self) -> Option<io::Result<&'a str>>{
        self.buf.clear();
        match self.reader.read_line(&mut self.buf) {
            Ok(nbytes) => if nbytes == 0 {
                None // 没有更多行啦!
            } else {
                let line = self.buf.trim_right(); // trim_right的函数签名:`pub fn trim_right(&self) -> &str`
                Some(Ok(line))
            },
            Err(e) => Some(Err(e))
        }
    }
#}

现在,请注意 生命周期 如何工作。 我们需要明确的 生命周期 ,因为 Rust 永远不会让我们,在不知道他们的 生命周期 的情况下,搞到借用的字符串切片。在这里,我们说这个借用的字符串的 生命周期 在self 的生命周期里面。

而且,生命周期的这个签名不兼容Iterator的接口(/trait), 但是如果兼容就很容易出现问题; 考虑到collect试图制作这些字符串切片的 Vec,可这是不能工作的,因为它们都是从 同一个 可变字符串self.buf中借用的! (如果您已 读取了文件的 所有 ,并转换为字符串,而这个字符串的lines迭代器是可以返回字符串切片,因为它们都是借用原始字符串的 不同 部分)。

最终,得到的循环结果更清晰,文件缓冲区对用户是不可见的。


# #![allow(unused_variables)]
#fn main() {
fn read_all_lines(filename: &str) -> io::Result<()> {
    let file = File::open(&filename)?;

    let mut lines = Lines::new(file);
    while let Some(line) = lines.next() {
        let line = line?;
        println!("{}", line);
    }

    Ok(())
}
#}

你甚至可以这样写循环,显式匹配可以从字符串切片拉出来 :


# #![allow(unused_variables)]
#fn main() {
    while let Some(Ok(line)) = lines.next() {
        println!("{}", line)?;
    }
#}

这很诱人,但你在这里抛出一个可能的错误;每当发生错误时,此循环都会静静地停止。 特别是,它将停止在,无法将 line 转换为 utf-8 的第一处位置。适合休闲代码,不适合生产代码!

写进文件

Debug实现处,我们遇到了write!宏,- 它也适用于任何实现了Write的东西。那另一种,是print!:


# #![allow(unused_variables)]
#fn main() {
    let mut stdout = io::stdout();
    ...
    write!(stdout,"answer is {}\n", 42).expect("write failed");
#}

如果 可能 有错误,你必须处理它,不 容易 但可能发生。通常还好,因为如果是文件 I/O,(一般情况下)应该能加个?

但有一个区别: print!为每个写锁定 stdout。 通常是您想要输出的内容,因为若没有锁定,多线程程序会混淆输出,搞笑的方式。 但是,如果是要甩出大量文字,那么write!会更快。

对任意文件,我们用到write!。 在 write_out的结尾, out变量会释放,文件自然关闭。

// file6.rs
use std::fs::File;
use std::io;
use std::io::prelude::*;

fn write_out(f: &str) -> io::Result<()> {
    let mut out = File::create(f)?;
    write!(out,"answer is {}\n", 42)?;
    Ok(())
}

fn main() {
  write_out("test.txt").expect("write failed");
}

如果你关心性能,你需要知道 Rust 文件默认是无缓冲的。 所以每个小的写入请求都会直接进入操作系统,而这会明显变慢。我提到了这一点,因为这种默认设置与其他编程语言不同,并且可能导致令人震惊的发现: Rust 可能有脚本语言遗留的残渣!

io::BufWriterReadio::BufReader,但带有缓冲的Write

文件,路径和目录

这是一个用于在机器上打印 Cargo 目录的小程序。最简单的情况是’~/.cargo’。在一个 Unix shell 环境,所以我们使用env::home_dir,因为它是跨平台的。 (它可能会失败,但没有主目录的计算机, 无论如何都不会打算托管 Rust 工具的。 )

这里,我们创建一个PathBuf,并使用它的push方法,构建完整的文件路径就像 组件。 (这比 用/,\或其他任何东西来要容易得多,老要顾虑系统。)

// file7.rs
use std::env;
use std::path::PathBuf;

fn main() {
    let home = env::home_dir().expect("no home!");
    let mut path = PathBuf::new();
    path.push(home);
    path.push(".cargo");

    if path.is_dir() {
        println!("{}", path.display());
    }
}

一个PathBuf就好像String- 它拥有一组可扩展的方法,但具有专门用于构建路径的方法。 但其大部分功能都来自借用版本的Path,这就像&str。 所以,举个例子,is_dir就是一个Path方法。

这可能听起来像一种继承形式,但魔法Deref trait 的工作方式不同。就像String/&str能一起工作 - PathBuf引用可 包裹{Coerce}Path引用。 (’Coerce’是一个很强的词,但这确实是 Rust ,为你提供转换的几个地方之一。 )


# #![allow(unused_variables)]
#fn main() {
fn foo(p: &Path) {...}
...
let path = PathBuf::from(home);
foo(&path);
#}

PathBufOsString有亲密的关系,它代表我们直接从系统获得的字符串。(相应的,OsString/&OsStr关系. )

这样的字符串不 保证 可以表示为 utf-8! 现实生活是一个复杂的事情,特别是看到’他们为什么这么辛苦’的答案。总而言之,首先有几年的 ASCII 传统编码,以及其他语言的多种特殊编码。 其次,人类语言很复杂。 例如’noël’是 五个 Unicode 代码点!

确实,现代操作系统文件名的大部分都是 Unicode 格式 (Unix 方面的 utf-8 ,Windows 方面的 UTF-16) ,又或者不是。 Rust 必须严格处理这种可能性。 例如,Path有一个as_os_str方法。它返回一个&OsStr,但是to_str方法却是返回一个Option<&str>。随缘!

人们在这一点上遇到了麻烦,因为他们已经过分依赖’string’和’character’作为唯一的必要抽象。 正如爱因斯坦所说, 编程语言必须尽可能简单,但并不简单。 系统语言 需要 区分一个String/&str(拥有与借用: 这也非常方便) ,如果它希望站在标准化的 Unicode 字符串上,那么它需要另一种类型来处理无效 Unicode 的文本 - 因此有了OsString/&OsStr。 请注意,这些类型没有任何有趣的,类似字符串的方法,因为我们本来就不知道无效 Unicode 编码。

但是,人们习惯像处理字符串一样处理文件名,这就是 Rust 使用PathBuf方法操作文件路径,会更容易的原因。

您可以pop连续去除路径组件。 这里我们从程序的当前目录开始:

// file8.rs
use std::env;

fn main() {
    let mut path = env::current_dir().expect("can't access current dir");
    loop {
        println!("{}", path.display());
        if ! path.pop() {
            break;
        }
    }
}
// /home/steve/rust/gentle-intro/code
// /home/steve/rust/gentle-intro
// /home/steve/rust
// /home/steve
// /home
// /

这是一个有用的变化。 我有一个搜索 配置{config} 文件 的程序,其规则是,它可能出现在当前目录的任何子目录中。 所以我创建/home/steve/rust/config.txt,并在/home/steve/rust/gentle-intro/code启动此程序:

// file9.rs
use std::env;

fn main() {
    let mut path = env::current_dir().expect("can't access current dir");
    loop {
        path.push("config.txt");
        if path.is_file() {
            println!("gotcha {}", path.display());
            break;
        } else {
            path.pop();
        }
        if ! path.pop() {
            break;
        }
    }
}
// gotcha /home/steve/rust/config.txt

如此像 git 的工作方式,当它想知道当前的存储库是是什么的时候。

有关文件的详细信息 (其大小,类型等) 被称为它的 元数据。 与往常一样,可能存在错误 - 不仅仅是”找不到”,或是我们没有权限读取此文件。

// file10.rs
use std::env;
use std::path::Path;

fn main() {
    let file = env::args().skip(1).next().unwrap_or("file10.rs".to_string());
    let path = Path::new(&file);
    match path.metadata() {
        Ok(data) => {
            println!("type {:?}", data.file_type());
            println!("len {}", data.len());
            println!("perm {:?}", data.permissions());
            println!("modified {:?}", data.modified());
        },
        Err(e) => println!("error {:?}", e)
    }
}
// type FileType(FileType { mode: 33204 })
// len 488
// perm Permissions(FilePermissions { mode: 436 })
// modified Ok(SystemTime { tv_sec: 1483866529, tv_nsec: 600495644 })

文件的长度 (以字节为单位) 和修改时间很容易解释。 (注意我们可能无法获得这个时间!) 文件类型有方法is_dir,is_fileis_symlink

权限{perissions}是一个有趣的点。 Rust 努力成为跨平台的,所以这是’木桶的最短木条’的例子。 一般来说,你可以查询的仅是,文件是否只读 - ‘权限’概念在 Unix 中被扩展,并为 用户/群组/其他 提供 读/写/可执行的权限。

但是,如果您对 Windows 不感兴趣,那么引入特定于平台的 traits 将至少为我们提供,权限模式位数。 (像往常一样,一个 trait 只有在它可见时才会触发。 ) 然后,应用到程序:


# #![allow(unused_variables)]
#fn main() {
use std::os::unix::fs::PermissionsExt;
...
println!("perm {:o}",data.permissions().mode());
// perm 755
#}

(注意”{:o}”用于打印 八进制)

(Windows 上的文件是否可执行取决于其扩展名。可执行文件的扩展名可以在PATHEXT环境变量找到 - ‘.exe’,’. bat’等等) .

std::fs包含许多用于处理文件的有用功能,例如复制或移动文件,制作符号链接和创建目录。

要查找目录的内容,std::fs::read_dir提供了一个迭代器。 以下是扩展名为”.rs”且大小大于 1024 字节 的所有文件:


# #![allow(unused_variables)]
#fn main() {
fn dump_dir(dir: &str) -> io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let data = entry.metadata()?;
        let path = entry.path();
        if data.is_file() {
            if let Some(ex) = path.extension() {
                if ex == "rs" && data.len() > 1024 {
                    println!("{} length {}", path.display(),data.len());
                }
            }
        }
    }
    Ok(())
}
// ./enum4.rs length 2401
// ./struct7.rs length 1151
// ./sexpr.rs length 7483
// ./struct6.rs length 1359
// ./new-sexpr.rs length 7719
#}

显然,read_dir可能会失败 (通常是’找不到’或’没有权限’),但是获取每个新条目时,也可能会失败 (这就像是line迭代器 遍历 缓冲的 reader 的内容)。 另外,我们可能无法获取与条目对应的元数据。 一个文件可能没有扩展名,所以我们也必须检查。

为什么不仅搞出,一个遍历路径的迭代器? 在 Unix 上,是opendir系统调用在起作用,但在 Windows 上,您无法在不获取元数据的情况下,迭代目录的内容。所以,这已是一个相当优雅的妥协方案,它允许跨平台的代码尽可能的高效。

关于感觉到’错误疲劳{error fatigue}’,你可以被原谅。但请注意 错误总是存在 - 这不是 Rust 的新发明。 它只是在努力让你无法忽视它们。 任何操作系统调用都可能失败。

Java 和 Python 等语言会引发异常; 像 Go 和 Lua 这样的语言返回两个值,其中第一个是 结果,第二个是 错误: 像 Rust 也一样,它要考虑到是库函数引发错误的不良。所以才有,这么多错误检查和函数的提前返回。

Rust 使用Result,因为它有两面性(either-or): 你不能同时得到 一个结果 和 错误。?问号运算符使处理错误更加清晰。

进程

一个基本的需求是程序去运行程序,或者 启动进程 。 你的程序可以 启动{launch} 尽可能多的子进程,顾名思义此类进程间有特殊的关系。

使用Command结构,运行子程序很简单,拿到传递给子程序的构建参数:

use std::process::Command;

fn main() {
    let status = Command::new("rustc")
        .arg("-V")
        .status()
        .expect("no rustc?");

    println!("cool {} code {}", status.success(), status.code().unwrap());
}
// rustc 1.15.0-nightly (8f02c429a 2016-12-15)
// cool true code 0

看啊,new收到该程序的名称 (它将查找PATH,如果不是绝对文件名的话),arg增加了一个新的 尾随参数,并且status导致它运行。 这返回一个Result,若为Ok,说明程序运行了,且包含一个退出状态{ExitStatus}。在这种情况下,程序成功,并返回 退出码 0 (使用unwrap是因为,如果程序被信号杀死了,我们不总是得到退出代码)。

如果我们改变了-V-v (一个易犯的错误),导致rustc失败:

error: no input filename given

cool false code 101

所以有三种可能性:

  • 程序不存在,很糟糕,或者我们不允许运行它
  • 程序运行,但没有成功 - 非零退出代码
  • 程序运行,零退出代码。成功!

默认情况下,程序的 stdout 和标准错误流将发送到终端。

我们经常对这种输出非常感兴趣,也就是output方法.

// process2.rs
use std::process::Command;

fn main() {
    let output = Command::new("rustc")
        .arg("-V")
        .output()
        .expect("no rustc?");

    if output.status.success() {
        println!("ok!");
    }
    println!("len stdout {} stderr {}", output.stdout.len(), output.stderr.len());
}
//Ok!
// len stdout 44 stderr 0

status一样,我们的程序会阻塞,直到子进程结束,我们返回三个值 - status (如上),stdout 的内容和标准错误的内容。

捕获到的内容输出是简单的Vec<u8>- 只是字节。 回想一下,我们不能保证,我们从操作系统收到的数据是正确的 utf-8 编码 字符串。 事实上,我们 甚至 不能保证它是一个字符串 - 程序可能会返回任意二进制数据。

如果我们确信输出是 utf-8,那么String::from_utf8将转换这些 Vec 或字节 - 它返回的是一个Result,因为这种转换可能不会成功。 一个更迷糊的函数是String::from_utf8_lossy,能很好地转换,并在转换失败时插入无效的 Unicode标记。

下面是一个使用 shell 来运行程序的有用函数。这使用通常的 shell 机制,将标准错误连接到 stdout。 在 Windows 上 shell 的名字是不同的,但是除此之外的东西可以按预期工作。


# #![allow(unused_variables)]
#fn main() {
fn shell(cmd: &str) -> (String,bool) {
    let cmd = format!("{} 2>&1",cmd);
    let shell = if cfg!(windows) {"cmd.exe"} else {"/bin/sh"};
    let flag = if cfg!(windows) {"/c"} else {"-c"};
    let output = Command::new(shell)
        .arg(flag)
        .arg(&cmd)
        .output()
        .expect("no shell?");
    (
        String::from_utf8_lossy(&output.stdout).trim_right().to_string(),
        output.status.success()
    )
}


fn shell_success(cmd: &str) -> Option<String> {
    let(output,success) = shell(cmd);
    if success {Some(output)} else {None}
}
#}

我修整了右边的任何空格, 所以,如果你说shell("which rustc")的话,您将获得没有任何额外换行的路径。

您可以通过Process控制启动程序的执行, 使用current_dir方法指定它将运行的目录,和它所使用的环境变量env

到目前为止,我们的程序只是等待子进程完成. 如果你使用spawn方法,我们立即返回,可以明确地等待它完成 - 或者在此期间去做其他事情! 这个例子还显示了如何同时抑制 标准错误和标准错误:

// process5.rs
use std::process::{Command,Stdio};

fn main() {
    let mut child = Command::new("rustc")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("no rustc?");

    let res = child.wait();
    println!("res {:?}", res);
}

默认情况下,子进程”继承”父进程的标准输入和输出。 在这里,我们将 孩子的输出控制重定向到”没有”。 这相当于在 Unix shell 中说>/dev/null 2>/dev/null

现在, 你在 Rust 可以使用 shell (sh要么cmd) 来完成这些事情 。 但通过这种方式,您可以完全程序化地控制进程的创建。

例如,如果我们编写的是.stdout(Stdio::piped()),那么孩子的 stdout 就被重定向到管道。那child.stdout就是你可以用来直接读取输出的东西 (例如: 要实现了Read)。 同样,你可以使用.stdin(Stdio::piped())方法,以便您可以写进child.stdin

但,如果我们使用wait_with_output代替wait, 那么它会返回一个Result<Output>,并将孩子的输出,会以一个Vec<u8>,记录到Outputsudout字段,就像之前的一样。

Child结构,也给你一个明确的kill方法。

模块和 Cargo

目录

模块

随着程序变得越来越大,有必要将它们分散到多个文件中,和将函数和类型放在不同的 命名空间。 这些问题的 Rust 解决方案就是 模块

C 语言 吃了第一个螃蟹,而不是第二个,所以你最终会遇到类似primitive_display_set_width的可怕名字等等。实际上,文件名可以任意命名。

Rust 使用的全名看起来像primitive::display::set_width,之后可使用use primitive::display,这样就能用display::set_width代替。 你甚至可以说use primitive::display::set_width,然后只能用set_width,但这并不是一个好方式。 rustc虽然不会混淆,但是 稍后可能会感到困惑。为了这个工作,文件名必须遵循一些简单的规则。

一个新的关键字mod,用于将模块定义为,可以写入 Rust 类型或函数的块:

mod foo {
    #[derive(Debug)]
    struct Foo {
        s: &'static str
    }
}

fn main(){
    let f = foo::Foo{s: "hello"};
    println!("{:?}", f);
}

但它仍不正确 - 我们得到’struct Foo is 私人{private}’。 为了解决这个问题,我们需要允许Foo导出的pub关键字。然后错误又变为’结构的 foo::Foo 字段是私人的’,再放了pub后, 能导出Foo::s。事情办好了。


# #![allow(unused_variables)]
#fn main() {
    pub struct Foo {
        pub s: &'static str
    }
#}

一个明确的pub,意味着你必须 选择 哪些内容要通过模块公开。从模块导出的一组函数和类型称为它的 接口{interface}

隐藏结构内部,通常会更好,并且只允许通过方法访问:

mod foo {
    #[derive(Debug)]
    pub struct Foo {
        s: &'static str
    }

    impl Foo {
        pub fn new(s: &'static str)-> Foo {
            Foo{s: s}
        }
    }
}

fn main(){
    let f = foo::Foo::new("hello");
    println!("{:?}", f);
}

为什么隐藏 实现(impl) 是一件好事? 因为这意味着您可以在不中断接口,没有模块使用者太注意其细节的情况下稍后进行更改。 大规模编程的大敌是细节代码纠结的倾向,因此去理解一段一段代码,实际做了什么是不可能的。

在一个完美的世界里,一个模块做一件事,做好,并保持自己的秘密。

何时不要隐藏? 正如 Stroustrup 所说,当接口 实现,就像struct Point {x: f32,y: f32}结构要导出。

一个模块 ,所有的项对所有的其他项都可见。 这是一个舒适的地方,每个人都可以成为朋友,知道彼此的私密细节。

每个人都可以根据自己的喜好,将程序分成不同的文件。我开始对 500 感到不舒服,那就 超过 2000 好了,随你喜欢(或有规定)。

那么如何将这个程序分解成单独的文件呢?

我们把这个foo代码到foo.rs:


# #![allow(unused_variables)]
#fn main() {
// foo.rs
#[derive(Debug)]
pub struct Foo {
    s: &'static str
}

impl Foo {
    pub fn new(s: &'static str)-> Foo {
        Foo{s: s}
    }
}
#}

并在主main程序中, 在一个区块{}内,使用一个mod foo声明,:

// mod3.rs
mod foo;

fn main(){
    let f = foo::Foo::new("hello");
    println!("{:?}", f);
}

现在rustc mod3.rs也会引发foo.rs编译。 没有必要用 makefiles 来搞笑!

编译器也会看MODNAME/mod.rs,所以,如果我创建一个目录boo,其包含一个文件mod.rs,这也会工作:


# #![allow(unused_variables)]
#fn main() {
// boo/mod.rs
pub fn answer()->u32 {
    42
}
#}

现在主程序可以将两个模块作为单独的文件使用:

// mod3.rs
mod foo;
mod boo;

fn main() {
    let f = foo::Foo::new("hello");
    let res = boo::answer();
    println!("{:?} {}", f,res);
}

到目前为止,mod3.rs含有main,一个模块foo.rs和一个含mod.rs的目录boo。 通常的惯例是包含main的文件,就叫main.rs

为什么有两种可做同样事情的方法? 因为boo/mod.rs,可让boo引用定义的其他模块,更新boo/mod.rs,并添加一个新模块 - 注意导出明确性。(若没有pubbar只能看看在boo模块里面).


# #![allow(unused_variables)]
#fn main() {
// boo/mod.rs
pub fn answer()->u32 {
    42
}

pub mod bar {
    pub fn question()-> &'static str {
        "the meaning of everything"
    }
}
#}

然后,我们有了问题相对应的答案(bar模块在boo里面):


# #![allow(unused_variables)]
#fn main() {
let q = boo::bar::question();
#}

该模块部分可以被拉到boo/bar.rs:


# #![allow(unused_variables)]
#fn main() {
// boo/bar.rs
pub fn question()-> &'static str {
    "the meaning of everything"
}
#}

boo/mod.rs变为:


# #![allow(unused_variables)]
#fn main() {
// boo/mod.rs
pub fn answer()->u32 {
    42
}

pub mod bar;
#}

总之,模块是关于组织和可见性的,这可能涉及或不涉及单独的文件。

请注意use与导入无关,只是指定模块名称的可见性。 例如:


# #![allow(unused_variables)]
#fn main() {
{
    use boo::bar;
    let q = bar::question();
    ...
}
{
    use boo::bar::question();
    let q = question();
    ...
}
#}

重要的一点是,这里没有 单独编译 说法。 主程序及其模块文件每次都要重新编译。也就是这样,较大的程序需要花费相当长(非常)的时间, 当然rustc的渐进式编译会越来越好。

Crates

Rust 的”编译单位”是 箱子{crate} ,它是一个可执行文件或一个库。

要分别编译上一节中的文件,请先构建foo.rs作为 rust 静态库 箱:

src$ rustc foo.rs --crate-type=lib
src$ ls -l libfoo.rlib
-rw-rw-r-- 1 steve steve 7888 Jan  5 13:35 libfoo.rlib

我们现在可以 链接 这到我们的主要程序中:

src$ rustc mod4.rs --extern foo=libfoo.rlib

但,主要程序现在必须像这样,这个extern名称与链接时使用的名称相同。有一个隐式的顶级模块foo与 库 crate 相关联:

// mod4.rs
extern crate foo;

fn main(){
    let f = foo::Foo::new("hello");
    println!("{:?}", f);
}

在人们开始欢呼’Cargo!Cargo!’之前,让我过一遍这个 Rust 构建的底层环境。我是’Know Thy Toolchain’的忠实信徒, 若我们从一开始就使用 Cargo 管理项目,会减少你需要学习的新魔法数量。模块是基本的语言功能,可用于 Cargo 项目之外。

现在该理解下,为什么 Rust 的二进制文件如此之大:

src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 3,4M Jan  5 13:39 mod4

这很胖! 因 在该可执行文件中有 许多 调试信息.

这不是一件坏事,如果你想调试,并当你的程序发生混乱时,实际上需要有意义的回溯。那么让我们去除这些调试信息,并查看:

src$ strip mod4
src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 300K Jan  5 13:49 mod4

对如此简单的事情,尺寸仍感觉有点大,但是这个程序 静态 链接 Rust 标准库。这是一件好事,因为您可以将此可执行文件交给任何具有正确操作系统的人 - 他们不需要”Rust 运行时”,就可以启用该文件。(还有,rustup甚至可以让你根据其他操作系统和平台 进行跨平台编译。 )

我们可以 动态 链接到 Rust 运行时,并获得真正的小:

src$ rustc -C prefer-dynamic mod4.rs --extern foo=libfoo.rlib
src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 14K Jan  5 13:53 mod4
src$ ldd mod4
    linux-vdso.so.1 => (0x00007fffa8746000)
    libstd-b4054fae3db32020.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6(0x00007f3cd47aa000)
    /lib64/ld-linux-x86-64.so.2(0x00007f3cd4d72000)

这’找不到 no found’是因为rustup不会全局安装动态库。 至少在 Unix 上 我们可以用我们的快乐方式破解(是的,我知道最好的解决方案是符号链接)。

src$ export LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib
src$ ./mod4
Foo { s: "hello" }

Rust 没有动态链接的 玄学 问题,与 Go 一样。 只是当每 6 周发布一个稳定版本时,不得不重新编译所有内容。 如果你有一个适合你的稳定版本,那么很酷。 随着 Rust 的稳定版本更新换代,越来越多地移交给 OS 包管理器控制, 动态链接将变得更加流行。

Cargo

与 Java 或 Python 相比,Rust 标准库不是很大。虽然功能 比 C 或 C ++ 更强大,但主要依赖于操作系统提供的库。

但用 Cargo 访问crates.io社区提供的库很简单。 Cargo 查找正确的版本,并为您下载源代码,并确保下载其他所需的 crate。

我们来创建一个需要 读取 JSON 的简单程序。 这种数据格式的使用非常广泛,但是对于包含在标准库中的数据格式太偏科了。下面我们展示下,我们初始化一个 Cargo 项目,可以不使用’--bin’,因为默认就是创建一个二进制项目。

test$ cargo init --bin test-json
        Created binary(application)project
test$ cd test-json
test$ cat Cargo.toml
[package]
name = "test-json"
version = "0.1.0"
authors = ["Your Name <you@example.org>"]

[dependencies]

让项目依赖JSON crate,编辑’Cargo.toml’文件,如下所示:

[dependencies]
json="0.11.4"

然后用 Cargo 进行第一次构建:

test-json$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading json v0.11.4
Compiling json v0.11.4
Compiling test-json v0.1.0(file:///home/steve/c/rust/test/test-json)
Finished debug [unoptimized + debuginfo] target(s)in 1.75 secs

在用 Cargo 初始化这个项目的时候,主文件已经被 创建 , 它是’src’目录中的’main.rs’。 开始时,只是一个’你好世界’的应用程序,现在让它变成一个适当的测试程序。

请注意,非常方便的’原始{raw}’字符串字面量的使用 - 否则我们需要转义那些双引号,一段丑陋的格式:

// test-json/src/main.rs
extern crate json;

fn main(){
    let doc = json::parse(r#"
    {
        "code": 200,
        "success": true,
        "payload": {
            "features": [
                "awesome",
                "easyAPI",
                "lowLearningCurve"
            ]
        }
    }
    "#).expect("parse failed");

    println!("debug {:?}", doc);
    println!("display {}", doc);
}

main.rs改好后,您现在编译和运行此项目.

test-json$ cargo run
    Compiling test-json v0.1.0(file:///home/steve/c/rust/test/test-json)
    Finished debug [unoptimized + debuginfo] target(s)in 0.21 secs
        Running `target/debug/test-json`
debug Object(Object { store: [("code", Number(Number { category: 1, exponent: 0, mantissa: 200 }),
    0, 1),("success", Boolean(true), 0, 2),("payload", Object(Object { store: [("features",
    Array([Short("awesome"), Short("easyAPI"), Short("lowLearningCurve")]), 0, 0)] }), 0, 0)] })
display {"code":200,"success":true,"payload":{"features":["awesome","easyAPI","lowLearningCurve"]}}

调试(debug)输出了 JSON 文档的一些内部细节,而用,一个普通的”{}”,使用了Display trait,从解析的文档重生成 JSON。

我们来探索一下 JSON API。 如果我们无法提取数值,这将毫无用处。 该as_TYPE方法会返回Option<TYPE>, 因为我们无法确定该字段是否存在或是否属于正确类型。 (见 JsonValue 的文档)


# #![allow(unused_variables)]
#fn main() {
    let code = doc["code"].as_u32().unwrap_or(0);
    let success = doc["success"].as_bool().unwrap_or(false);

    assert_eq!(code, 200);
    assert_eq!(success, true);

    let features = &doc["payload"]["features"];
    for v in features.members(){
        println!("{}", v.as_str().unwrap()); // MIGHT explode
    }
    // awesome
    // easyAPI
    // lowLearningCurve
#}

features这里是一个JsonValue引用 - 它必须是一个引用,否则我们会试图移动一个 ,这会脱离 JSON。这里我们知道它是一个数组,所以members()将返回一个非空的&JsonValue迭代器。

如果”payload”对象没有”features”键,该怎么办? 那么features将被设置为Null。 不会有爆炸。 这种便利表达了 JSON 的自由表达任何东西的本质。 如果结构不匹配,您应该检查收到的任何文档结构,并创建自己的错误。

如果我们有let mut doc,您可以修改这些结构。记得加上 expect:


# #![allow(unused_variables)]
#fn main() {
    let features = &mut doc["payload"]["features"];
    features.push("cargo!").expect("couldn't push");
#}

如果feature不是一个数组,该push将失败,因此它 panic。

使用一个宏,来生成 JSON 字面量,漂亮:


# #![allow(unused_variables)]
#fn main() {
    let data = object!{
        "name"    => "John Doe",
        "age"     => 30,
        "numbers" => array![10,53,553]
    };
    assert_eq!(
        data.dump(),
        r#"{"name":"John Doe","age":30,"numbers":[10,53,553]}"#
    );
#}

为了这个宏工作,你需要显式地从 JSON 箱导入宏 :


# #![allow(unused_variables)]
#fn main() {
#[macro_use]
extern crate json;
#}

由于 JSON 的无定形,动态性质 和 Rust 的结构化,静态性质之间的不匹配,使用这个 crate 有一个缺点。 (readme 明确提到’有摩擦{friction}’),所以如果你 要将 JSON 映射到 Rust 数据结构,您最终会做很多检查,因为您不能认为接收到的结构与您的结构相匹配! 为此,更好的解决方案是serde_json, 它可以将 Rust 数据结构 序列化 为 JSON ,和 JSON 反序列化 到 Rust。

为此,请创建另一个 Cargo 二进制项目Cargo new --bin test-serde-json,进入test-serde-json目录和编辑Cargo.toml。 像这样编辑它:

[dependencies]
serde="0.9"
serde_derive="0.9"
serde_json="0.9"

并编辑src/main.rs:

#[macro_use]
extern crate serde_derive;
extern crate serde_json;

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u8,
    address: Address,
    phones: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Address {
    street: String,
    city: String,
}

fn main(){
    let data = r#" {
     "name": "John Doe", "age": 43,
     "address": {"street": "main", "city":"Downtown"},
     "phones":["27726550023"]
    } "#;
    let p: Person = serde_json::from_str(data).expect("deserialize error");
    println!("Please call {} at the number {}", p.name, p.phones[0]);

    println!("{:#?}",p);
}

你之前已经看到了derive属性,但是serde_derive crate 为特有的SerializeDeserialize trait ,定义了 自定义派生{custom derives}。生成的 Rust 结构体结果:

Please call John Doe at the number 27726550023
Person {
    name: "John Doe",
    age: 43,
    address: Address {
        street: "main",
        city: "Downtown"
    },
    phones: [
        "27726550023"
    ]
}

现在,如果你使用了json,那么你需要几百行的自定义转换代码,主要是错误处理。 单调乏味,容易搞砸,这些都不是你想要付出努力的地方。

如果,你想从外部来源处理结构良好的 JSON (如果需要,可以重新映射字段名称),serde显然是最好的解决方案,并为 Rust 程序通过网络与其他程序共享数据提供了一个强大的方法(因为如今一切都能理解 JSON)。 关于serde很酷的事情(名字来源于,SERialization:序列化 DEserialization: 反序列化 的 大写字母)是支持其他文件格式,例如toml,这是 cargo 中常用的配置友好格式。 因此,您的程序可以将 .toml 文件读入结构中,并将这些结构编写为.json

序列化是一项重要的技术,Java 和 Go 存在类似的解决方案 ,但有很大的不同。 在这些语言中,数据的结构可以在 运行时 运用 反射 找到,但现这情况,序列化代码是在 编译时- 更高效!

Cargo 被认为是 Rust 生态系统的一大优势,因为它为我们做了很多工作。 否则,我们不得不从 Github 下载这些库,构建为 静态库-crate ,并将它们与程序链接。 这对于 C ++ 项目来说是很痛苦的,如果 Cargo 不存在的话,Rust 项目相当于痛苦 C++ 本身。 C ++ 的痛苦中带点独特,所以我们应该将它与其他语言的包管理器进行比较。 npm(用于 JavaScript) 和 pip(用于 Python) 为您管理依赖关系和下载, 但分发流程更难,因为程序的用户需要安装 NodeJS 或 Python。 但 Rust 程序与它们的 依赖关系 是静态链接的,所以它们可以在没有外部依赖的情况下,再次发给你的好友。

更多的宝藏

处理除简单文本以外的任何内容时,正则表达式使您的生活变得更加轻松。 这通常适用于大多数语言,在这里假定你对正则表示法有基本的了解。 使用正则表达式, 把”regex =”0.2.1”’放在”[dependencies]”在您的 Cargo.toml。

我们将再次使用”raw 字符串”,以便反斜杠不必转义。 在中文,这个正则表达式意思是 “完全匹配两个数字,后接字符’:’,再是任意数字。共捕获两组数字”:


# #![allow(unused_variables)]
#fn main() {
extern crate regex;
use regex::Regex;

let re = Regex::new(r"(\d{2}):(\d+)").unwrap();
println!("{:?}", re.captures("  10:230"));
println!("{:?}", re.captures("[22:2]"));
println!("{:?}", re.captures("10:x23"));
// Some(Captures({0: Some("10:230"), 1: Some("10"), 2: Some("230")}))
// Some(Captures({0: Some("22:2"), 1: Some("22"), 2: Some("2")}))
// None
#}

成功的产出实际上有三个 捕获 项 - 全匹配,和两组数字。 默认情况下这些正则表达式不是 确定的 , 所以 正则表达式 将捕第一个出现的匹配,跳过任何不匹配的东西。 (如果你遗漏了’()’,它只会给我们全匹配。 )

可以 命名 那些捕捉项,并且将正则表达式分散在多行,甚至包括注释! 编译正则表达式可能会失败(第一个 expect)或者匹配可能失败(第二个 expect)。 在这里,我们可以使用结果作为关联数组,并按名称查找。


# #![allow(unused_variables)]
#fn main() {
let re = Regex::new(r"(?x)
(?P<year>\d{4}) # the year
-
(?P<month>\d{2})# the month
-
(?P<day>\d{2})  # the day
").expect("bad regex");
let caps = re.captures("2010-03-14").expect("match failed");

assert_eq!("2010", &caps["year"]);
assert_eq!("03", &caps["month"]);
assert_eq!("14", &caps["day"]);
#}

正则表达式可以分解符合模式的字符串,但不会检查它们是否有意义。 也就是说,你可以指定和匹配的 ISO 语法 风格的日期,但 语义 可能是无稽之谈,比如”2014-24-52”。

为此,您需要专门的日期时间处理,由计时 chrono提供。 你或需要做日期时,决定一个时区:

extern crate chrono;
use chrono::*;

fn main(){
    let date = Local.ymd(2010,3,14);
    println!("date was {}", date);
}
// date was 2010-03-14+02:00

但是,这不推荐,因为喂它不好的日期会导致恐慌!(尝试一个假日期)你需要的方法是 ymd_opt,其返回LocalResult<Date>


# #![allow(unused_variables)]
#fn main() {
    let date = Local.ymd_opt(2010,3,14);
    println!("date was {:?}", date);
    // date was Single(2010-03-14+02:00)

    let date = Local.ymd_opt(2014,24,52);
    println!("date was {:?}", date);
    // date was None
#}

您还可以直接解析日期时间,无论是以 标准 UTC 格式 还是 使用自定义格式{formats} 这些完全相同的的格式允许您, 按照想要的格式打印日期。 我特别强调了这两个有用的 crate ,因为它们将成为大多数其他语言的标准库的一部分。

事实上,这些 crate 的胚胎形态曾经是 Rust stdlib 的一部分,但被切开了。这是个有意的决定: Rust 团队非常重视 stdlib 的稳定性,所以只有在不稳定的夜间版本诞生,而后活过 beta 和 stable 的功能才能保持稳定。 对于需要实验和改进的 库 来说,他们保持独立,并且 Cargo 能够跟踪会更好。 出于所有实际原因,这两个 crate 会是 标准 ,它们不会消失,并且可能会在某个时候折回到 stdlib 中。

标准库范畴

目录

阅读文档

在本节中,我将简要介绍 Rust 标准库的一些常见部分。文档非常好,但有一点讲解和一些例子总是有好处的。

最初,阅读 Rust 文档可能很具挑战性,所以我会举个Vec例子。 一个有用的提示是勾选’[-]’框来折叠文档。(如果使用下载标准库源代码rustup component add rust-src ,一个 ‘[src]’链接将出现在此旁边。)这些可以让您全面了解所有可用的方法。

首先要注意的是, 并非所有可能的方法 都定义Vec本身。 它们是(大部分) 可改变 vec 的方法,例如push。有些方法仅适用于类型匹配某些约束的 Vec。例如,你只能调用dedup(删除重复项) ,如果这个类型确实是可以相互比较的东西。 Vec有多个impl定义块,针对不同类型的约束。

然后是Vec<T>&[T]间非常特殊的关系。 任何在切片上工作的方法也可以直接在 vec 上工作,而不必明确地使用as_slice方法。 这种关系表达为Deref<Target=[T]>。当你为需要 切片 的函数传递一个 Vec 时,这也会起作用 - 这是类型之间自动转换的几个之一。 所以像切片方法first,它可能 - 返回对第一个元素的引用,或者last,同时也为 Vec 工作。 许多切片方法与相应的字符串方法类似,所以就有了,为了在索引处获得一对切片的split_at方法,和starts_with能检查 vec 是否以某值序列开始,还有contains能检查 vec 是否包含特定值。

要知道,是没有查找特定值的索引的search方法的,但这里有一条经验法则: 如果在方法集上找不到想要的方法,请在 迭代器 上查找方法:

    let v = vec![10,20,30,40,50];
    assert_eq!(v.iter().position(|&i| i == 30).unwrap(), 2);

(该&是因为这是一个建于 引用 上 的迭代器 - 或者你可以用*i == 30)。

同样, vec 上没有map方法,因为iter().map(...).collect()会做这项工作。 Rust 不喜欢不必要地分配(内存) - 通常你不需要map这样的过程结果,因这会是实际分配的 Vec,浪费。

所以我建议你熟悉所有的 迭代器 方法,因为它们对编写好的 Rust 代码至关重要,不必一直写出循环。 与往常一样,编写小程序来探索 迭代器 方法,而不是在更复杂的程序中与它们搏斗。

Vec<T>&[T]方法拥抱共同的 trait : Vec 知道如何进行自己的调试显示(但,只有其元素也实现Debug,才如此。) 同样,如果它们的元素是可克隆的,那就是可克隆的。 他们实现了Drop,那当 vec 最终死亡时就会发生对应情况; 内存被释放,并且所有元素也被释放。

Extend trait 是说,不需要一个循环,就可以让 迭代器 的值添加到一个 Vec 中:

v.extend([60,70,80].iter());
let mut strings = vec!["hello".to_string(), "dolly".to_string()];
strings.extend(["you","are","fine"].iter().map(|s| s.to_string()));

还有FromIterator,它可以让 vec 由迭代器 构成{constructed} 。(迭代器collect方法依赖这个)。

任何容器(vec...)都需要可迭代。 回想一下有三种迭代器

for x in v {...} // 返回 T, 消耗 v
for x in &v {...} // 返回 &T
for x in &mut v {...} // 返回 &mut T

for声明依赖于IntoIteratortrait,实际上有三种实现。

然后是索引,由Index控制(从 vec 中读取) 和IndexMut(修改一个 Vec)。存在很多可能性,因为还有切片索引,像v[0..2]会返回切片,以及v[0]会返回对第一个元素的引用。

这里有一些From trait 的实现。例如Vec::from("hello".to_string())会给你一个字符串底层字节Vec<u8>的 Vec 。 现在,已经有一种into_bytes方法在String上,为什么要重复? 有多种方式来做同样的事情似乎很困惑,但是这是必要的,因为显式 trait 使泛型方法成为可能。

有时候, Rust 类型系统的局限性会让事情变得笨拙。 这里的一个例子是PartialEq 要为尺寸 32 的数组 单独 定义!(以后会变得更好。) 虽然可以将 Vec 与数组进行方便地比较,但要注意大小限制。

还有隐藏的珠宝深埋在文档中。 正如 Karol Kuczmarski 所说: “因为说实话: 没有人会滚动那么远”。 如何处理迭代器中的错误? 假设你映射了一些可能失败的操作,就返回Result好了,然后收集结果:

fn main() {
    let nums =["5","52","65"];
    let iter = nums.iter().map(|s| s.parse::<i32>());
    let converted: Vec<_> = iter.collect();
    println!("{:?}",converted);
}
//[Ok(5), Ok(52), Ok(65)]

还行,但现在你必须小心地解开这些错误! 但是,如果你要求 vec 包裹 在一个Result的话, 那 Rust 会知道如何做正确的事情 - 也就是说,无论是一个 vec 还是一个错误,都能处理了:

    let converted: Result<Vec<_>,_> = iter.collect();
//Ok([5, 52, 65])

如果这有个错误? 然后你会在遇到第一个错误时得到Err。 这是一个灵活collect的好例子。(这里的符号可能会吓人 -Vec<_>意味着”这是一个 Vec ,忽略实际的类型,和Result<Vec<_>,_>还要求 Rust 忽略错误类型。)

文档中有 许多 的详细信息。 但它肯定比 C ++文档所说的std::vec更清晰。

对元素施加的要求取决于对容器执行的实际操作。 一般来说,要求元素类型是完整类型并且符合 Erasable 的要求,但是许多成员函数会施加更严格的要求。

用 C ++,你是独立思考的。 Rust 的清晰度一开始就让人尊敬,但当你学习阅读约束条件时,你将确切地知道Vec要的任何特定方法.

我建议你使用rustup component add rust-src获得源代码,因为标准库源代码非常易读,并且方法实现通常不如方法声明那么可怕。

Maps

Maps(有时叫 关联数组 要么 dicts{字典} ); 可以让你存放 键值对的数据结构。这不是一个光想的概念,可以用元组+数组来完成:

    let entries =[("one","eins"),("two","zwei"),("three","drei")];

    if let Some(val) = entries.iter().find(|t| t.0 == "two") {
        assert_eq!(val.1,"zwei");
    }

对小 map 来说,还能用,且只需要与 定义的 key 相等就好啦,但搜索需要线性时间 - 与 map 大小成比例。

要想搜索 许多 键/值 对,有个更好的HashMap:

use std::collections::HashMap;

let mut map = HashMap::new();
map.insert("one","eins");
map.insert("two","zwei");
map.insert("three","drei");

assert_eq!(map.contains_key("two"), true);
assert_eq!(map.get("two"), Some(&"zwei"));

为什么是&"zwei"? 这是因为get返回一个 引用 ,而不是 值本身 。而 这个值 的类型是&str,所以我们得到一个&&str。 一般来说,它 必须 作为一个引用,因为我们不能只 移动 值,而不管其拥有的类型。

get_mut就好像get,但返回一个可能的可变引用。 这里我们有一个字符串/整数的映射,并希望更新’two’键的值:

let mut map = HashMap::new();
map.insert("one",1);
map.insert("two",2);
map.insert("three",3);

println!("before {}", map.get("two").unwrap());

{
    let mut mref = map.get_mut("two").unwrap();
    *mref = 20;
}

println!("after {}", map.get("two").unwrap());
// before 2
// after 20

请注意,获取的可写引用发生在它自己的块中 - 否则,我们将有一个可变的借用持续到结束,那样的话, Rust 不会允许map.get("two")map再次借用; 同一作用域内已经有一个可变引用,不允许再出现(同一个 map 的)任何引用。(因为,它不能保证那些只读的引用保持有效。)所以解决方案是确保可变借用,不会持续很长时间。

这不是最优雅的 API,但我们不能抛弃任何可能的错误。 Python 会抛出一个异常,而 C ++只会创建一个默认值。(这很方便,但偷偷摸摸;容易忘记a_map["two"]的成本,也总是返回一个整数,让我们不能区分 0 和 ‘未找到’ 之间的区别, 还加上 一个额外项被创建!)

其实,没有人只调用unwrap, 除了例子外。 但是,您看到的大多数 Rust 代码都由一些独立的示例组成! 匹配发生的可能性更大:

if let Some(v) = map.get("two") {
    let res = v + 1;
    assert_eq!(res, 3);
}
...
match map.get_mut("two") {
    Some(mref) => *mref = 20,
    None => panic!("_now_ we can panic!")
}

我们可以遍历 key/值对,但(实际)不以任何特定的顺序。

for(k,v) in map.iter() {
    println!("key {} value {}", k,v);
}
// key one value eins
// key three value drei
// key two value zwei

也有分别按键和值,返回迭代器的key/values方法,这使得创建值的 Vec 变得容易。

示例: 计算词数

与文本有关的一个有趣的事情是,计数字的频率。

split_whitespace将文本分解为单词很简单,但是我们要真的遵循标点符号。总之,这些词应该被定义为只包含字母字符,也需要换成小写字母进行比较。

直接在 一个 map 上做一个可变的查找,虽然处理查找失败的情况有点尴尬。但幸运的是,有一种更新 map 值的方式:

let mut map = HashMap::new();

for s in text.split(|c: char| ! c.is_alphabetic()) {
    let word = s.to_lowercase();
    let mut count = map.entry(word).or_insert(0);
    *count += 1;
}

如果没有对应于某个单词的现有计数,那么让我们为该单词创建一个包含零的项,并 插{insert} 进 map。它正是 C ++映射所做的,除了它是明确的,不是偷偷摸摸的。

这段代码中只有一个显式类型char,因为split的使用关系到字符串Patterntrait 的怪癖。但我们可以推断出 key 类型是String和 value 类型是i32

根据 Gutenberg 项目 的福尔摩斯历险记,我们可以更彻底地进行测试。 唯一字词的总数(map.len()) 是 8071。

如何找到最常见的二十个单词? 首先,将 map 转换为 (key, value) 元组的 Vec。(若我们使用了into_iter,会消耗了 map。)

let mut entries: Vec<_> = map.into_iter().collect();

接下来,我们可以按降序排列。 sort_by接收来自于Ord trait 的cmp方法的结果,它是由整型值类型实现的:

    entries.sort_by(|a,b| b.1.cmp(&a.1));

最后打印出,前 20 个项:

    for e in entries.iter().take(20) {
        println!("{} {}", e.0, e.1);
    }

(好吧,你也 可以 只是用0..20循环,并索引 Vec 就好了 - 这虽然没有错,但有点随便 - 而且对于大型迭代来说,可能更昂贵。)

  38765
the 5810
and 3088
i 3038
to 2823
of 2778
a 2701
in 1823
that 1767
it 1749
you 1572
he 1486
was 1411
his 1159
is 1150
my 1007
have 929
with 877
as 863
had 830

有点惊喜 - 首个空文本是什么? 这是因为split,适用于单字符分隔符,因此任何标点符号或额外空格都会导致新的分割。

Sets

Sets(集合) 是 只关心 key 的 map,而不关联任何值。 所以inserts只需要一个值,可使用contains用于测试一个值是否在一个集合中。

像所有容器一样,您可以用迭代器创建一个HashSet。这正是 collect所做的,一旦你给了它必要的类型提示。

// set1.rs
use std::collections::HashSet;

fn make_set(words: &str) -> HashSet<&str> {
    words.split_whitespace().collect()
}

fn main() {
    let fruit = make_set("apple orange pear orange");

    println!("{:?}", fruit);
}
// {"orange", "pear", "apple"}

注意(如预期的那样) 重复插入同一个 key 是不起作用的,且,集合中值的顺序并不重要。

没有常用操作,它们不会成为集合:

let fruit = make_set("apple orange pear");
let colours = make_set("brown purple orange yellow");

for c in fruit.intersection(&colours) {
    println!("{:?}",c);
}
// "orange"

他们都创建 迭代器,并且可以使用 collect使这些成为集合。

这是一个快捷方式,就像我们为 vec 定义的那样:

use std::hash::Hash;

trait ToSet<T> {
    fn to_set(self) -> HashSet<T>;
}

impl<T,I> ToSet<T> for I
where T: Eq + Hash, I: Iterator<Item=T> {

    fn to_set(self) -> HashSet<T> {
       self.collect()
    }
}

...

let intersect = fruit.intersection(&colours).to_set();

就像所有的 Rust 泛型一样,你需要限制类型 - 这只能为理解平等(Eq) 的类型和存在一个”散列函数”(Hash)的T实现。请记住,没有叫Iterator类型 ,所以I代表任何 实现 Iterator的类型。

这种在标准库类型上,实现我们自己的方法的技术似乎有点过于强大,但是同样存在规则。 我们只能为自己的 trait 做到这一点。 如果结构和 trait 来自同一个箱子(特别是 stdlib ) ,那么这种实现将不被允许。这种方式,您可以避免造成混乱。

在祝贺有如此聪明,方便的捷径之前,您应该意识到后果。 如果make_set是这样写的,若这些是所有权字符串的集合,那intersect的实际类型可能会是个’惊喜’:

fn make_set(words: &str) -> HashSet<String> {
    words.split_whitespace().map(|s| s.to_string()).collect()
}
...
// intersect 是 HashSet<&String>!
let intersect = fruit.intersection(&colours).to_set();

一般情况下, Rust 不会突然开始复制所有权字符串。 intersect包含从fruit借来的单个&String。我可以向你保证,当你开始修补生命周期时,这会给你带来麻烦! 更好的解决方案是使用迭代器的cloned方法来创建 intersection 的所有权字符串副本。

// intersect 是 HashSet<String> - 更好了
let intersect = fruit.intersection(&colours).cloned().to_set();

to_set更强大,可能会是self.cloned().collect(),我邀请您试试。

示例: 交互式命令处理

与程序进行交互式会话通常很有用。 每行都被读入并分割成单词; 该命令在第一个单词上查找,其余单词作为参数传递给该命令。

一个自然的实现是 命令名称/闭包 的 map。 但是,我们如何存储闭包,因为它们都会有不同的大小? 将他们放入盒子(Box),那么他们会复制到堆上:

这是第一次尝试:

    let mut v = Vec::new();
    v.push(Box::new(|x| x * x));
    v.push(Box::new(|x| x / 2.0));

    for f in v.iter() {
        let res = f(1.0);
        println!("res {}", res);
    }

我们在第二次 push 时,遇到了非常明显的错误:

  = note: expected type `[closure@closure4.rs:4:21: 4:28]`
  = note:    found type `[closure@closure4.rs:5:21: 5:28]`
note: no two closures, even if identical, have the same type

rustc导出了一个过于省略的类型,所以在事情刚刚开始之前,有必要强制该 Vec 具有 Box trait 类型 :

    let mut v: Vec<Box<Fn(f64)->f64>> = Vec::new();

我们现在可以使用相同的技巧,并将这些盒化的闭包保存在一个HashMap。 我们仍然需要警惕生命周期,因为闭包可以从他们的环境中借用。

直接选择FnMut作为闭包签名具有诱导性- 换句话说,他们可以修改任何捕获的变量。 但,我们会有不止一个命令,每个命令都有自己的闭包,所以你不能随意可变借用相同的变量。

最后,闭包被传递一个可变引用作为一个参数,加上一段字符串切片(&[&str]) 代表命令参数,会返回一些Result- 若为错误我们会用String

D是数据类型,可以是任何带有一个固有大小的数据。

type CliResult = Result<String,String>;

struct Cli<'a,D> {
    data: D,
    callbacks: HashMap<String, Box<Fn(&mut D,&[&str])->CliResult + 'a>>
}

impl<'a,D: Sized> Cli<'a,D> {
    fn new(data: D) -> Cli<'a,D> {
        Cli{data: data, callbacks: HashMap::new()}
    }

    fn cmd<F>(&mut self, name: &str, callback: F)
    where F: Fn(&mut D, &[&str])->CliResult + 'a {
        self.callbacks.insert(name.to_string(),Box::new(callback)); // 装箱
    }

cmd被传递一个名称和任何与我们的签名相匹配的闭包,这个闭包被装箱并输入到 map 。 Fn意味着我们的闭包借用他们的环境,但不能修改。 它是一个声明比实际实现更可怕的泛型方法! 忘记明确生命周期是一个常见的错误 - Rust 不会让我们忘记这些闭包限于他们环境的生命周期!

现在读取和运行命令:

    fn process(&mut self,line: &str) -> CliResult {
        let parts: Vec<_> = line.split_whitespace().collect();
        if parts.len() == 0 {
            return Ok("".to_string());
        }
        match self.callbacks.get(parts[0]) {
            Some(callback) => callback(&mut self.data,&parts[1..]),
            None => Err("no such command".to_string())
        }
    }

    fn go(&mut self) {
        let mut buff = String::new();
        while io::stdin().read_line(&mut buff).expect("error") > 0 {
            {
                let line = buff.trim_left();
                let res = self.process(line);
                println!("{:?}", res);

            }
            buff.clear();
        }
    }

非常简单明了 - 将行分成单词,做成 vec ,查找 map 中的第一个单词,并用存储的可变数据和其余单词调用闭包。 空行会被忽略,不会被视为错误。

接下来,让我们定义一些帮助函数,使我们的闭包更容易返回正确和不正确的结果。 这有点 聪明;它们是泛型函数,适用于任何可以转换为String的类型。

fn ok<T: ToString>(s: T) -> CliResult {
    Ok(s.to_string())
}

fn err<T: ToString>(s: T) -> CliResult {
    Err(s.to_string())
}

最后,主程序。看看ok(answer)如何工作 - 试因为整数知道如何将自己转换为字符串!

use std::error::Error;

fn main() {
    println!("Welcome to the Interactive Prompt! ");

    struct Data {
        answer: i32
    }

    let mut cli = Cli::new(Data{answer: 42});

    cli.cmd("go",|data,args| {
        if args.len() == 0 { return err("need 1 argument"); }
        data.answer = match args[0].parse::<i32>() {
            Ok(n) => n,
            Err(e) => return err(e.description())
        };
        println!("got {:?}", args);
        ok(data.answer)
    });

    cli.cmd("show",|data,_| {
        ok(data.answer)
    });

    cli.go();
}

这里的错误处理有点笨拙,我们稍后会看到如何在这种情况下,使用问号运算符。 基本上来说,特定的std::num::ParseIntError错误实现std::error::Error trait,为了使用description方法,要导入该 trait - Rust 在 trait 是可见的情况下,才让 它们 运作。

一次行动:

Welcome to the Interactive Prompt!
go 32
got["32"]
Ok("32")
show
Ok("32")
goop one two three
Err("no such command")
go 42 one two three
got["42", "one", "two", "three"]
Ok("42")
go boo!
Err("invalid digit found in string")

以下是一些供您尝试的明显改进。 首先,如果我们给到cmd第二参数是帮助行,那么我们可以存储这些帮助行,并自动执行一个”help”命令。 其次,有一些命令编辑和历史是 非常 方便的,所以从 Cargo 的库中使用rustylinecrate。

错误处理

目录

基本的错误处理

如果你不能使用问号操作符,那么在 Rust 中的错误处理会很笨拙。 为了这种实现的快乐,我们需要返回一个可以接受任何错误的Result。 所有错误都会实现std::error::Errortrait,这样 任何 错误都可以转换成一个Box<Error>

说我们需要处理 I/O 错误和从 String 转换到数字的 两种 错误:


# #![allow(unused_variables)]
#fn main() {
// box-error.rs
use std::fs::File;
use std::io::prelude::*;
use std::error::Error;

fn run(file: &str) -> Result<i32,Box<Error>> {
    let mut file = File::open(file)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?; // Result<usize>
    Ok(contents.trim().parse()?)
}
#}

所以,这给出了的两个问号,一个给 I/O 错误 (无法打开文件,或无法读取为 String) 以及转换错误一个。 最后,我们将结果包装在Ok内。Rust 可以根据返回类型签名,从parse得出应转换为i32

很容易为Result类型创建一个简写:


# #![allow(unused_variables)]
#fn main() {
type BoxResult<T> = Result<T,Box<Error>>;
#}

但是,我们的程序将具有特定于应用程序的错误条件,还需要创建自己的错误类型。错误类型的基本要求也很简单:

  • 可以 impl Debug
  • 必须 impl Display
  • 必须 impl Error

还有啊,你的错误可以做它喜欢做的事情。


# #![allow(unused_variables)]
#fn main() {
// error1.rs
use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError {
    details: String
}

impl MyError {
    fn new(msg: &str) -> MyError {
        MyError{details: msg.to_string()}
    }
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,"{}",self.details)
    }
}

impl Error for MyError {
    fn description(&self) -> &str {
        &self.details
    }
}

// 一个返回我们错误结果的测试函数
fn raises_my_error(yes: bool) -> Result<(),MyError> {
    if yes {
        Err(MyError::new("borked"))
    } else {
        Ok(())
    }
}
#}

老输入Result<T,MyError>会乏味的,许多 Rust 模块会定义它们自己的Result- 例如io::Result<T>Result<T,io::Error>的简写。

在下一个例子中,当一个 String 不能被解析为一个浮点数时,我们需要处理特定的错误。

现在?工作的方式,是从 表达 的错误到必 返回 的错误的一种转换。 并且这个转换由From trait 表示。Box<Error>一样是这样工作的,因为它为所有实现了Error的类型实现From

此时您可以继续使用便捷的别名BoxResult,像以前一样 catch 所有事情; 会有一个我们的错误到Box<Error>的转换,这对小型应用程序来说是一个很好的选择。 但我想显示其他错误,明确与我们的错误类型的合作。

ParseFloatError实现了 Error, 所以description()方法可用。


# #![allow(unused_variables)]
#fn main() {
use std::num::ParseFloatError;

impl From<ParseFloatError> for MyError {
    fn from(err: ParseFloatError) -> Self {
        MyError::new(err.description())
    }
}

// and test!
fn parse_f64(s: &str, yes: bool) -> Result<f64,MyError> {
    raises_my_error(yes)?;
    let x: f64 = s.parse()?;
    Ok(x)
}
#}

第一个?还行 (一种类型总用From转换自己) 和第二个?将转换ParseFloatErrorMyError

结果如下:

fn main() {
    println!(" {:?}", parse_f64("42",false));
    println!(" {:?}", parse_f64("42",true));
    println!(" {:?}", parse_f64("?42",false));
}
//  Ok(42)
//  Err(MyError { details: "borked" })
//  Err(MyError { details: "invalid float literal" })

不会太复杂,就有点啰嗦。 该繁琐处是不得不为所有其他需要与MyError玩耍的错误类型,编写From - 或者简单点,依靠Box<Error>。 新手会因为多种方式在 Rust 中做同样的事情而感到困惑; 总是有另一种方法帮鳄梨削皮。代价有很多灵活选择。 200 行的错误处理程序可比大型应用程序简单得多。若您想将您的’宝贝’打包为一个 Cargo crate,那么错误处理就变得至关重要。

目前,问号运算符仅适用于Result,不是Option,这是一个功能,而不是一个限制。 Option有一个ok_or_else,该方法将自己转换成一个Result。例如说,我们有一个HashMap,若没有定义键的话,则必须失败:


# #![allow(unused_variables)]
#fn main() {
    let val = map.get("my_key").ok_or_else(|| MyError::new("my_key not defined"))?;
#}

现在这里返回的错误是很清楚的! (该形式 使用闭包,因此只有在查找失败时才会创建错误值。)

提供简单错误的 simeple-error

simple-errorcrate 为你提供基于一个字符串 的基本错误类型,正如我们在这里定义的那样,以及一些方便的宏。如同其他任何错误一样,Box<Error>也可以正常工作:

#[macro_use]
extern crate simple_error;

use std::error::Error;

type BoxResult<T> = Result<T,Box<Error>>;

fn run(s: &str) -> BoxResult<i32> {
    if s.len() == 0 {
        bail!("empty string");
    }
    Ok(s.trim().parse()?)
}

fn main() {
    println!("{:?}", run("23"));
    println!("{:?}", run("2x"));
    println!("{:?}", run(""));
}
// Ok(23)
// Err(ParseIntError { kind: InvalidDigit })
// Err(StringError("empty string"))

bail!(s)宏扩展为return SimpleError::new(s).into();- 提前返回转换 接收的类型签名。

你需要使用BoxResult,混合SimpleError类型与其他错误,因为我们无法为它实现From, 因为它的 trait 和类型都来自其他箱子(安全问题)。

提供严重错误的 error-chain

非凡的应用程序,看过来error_chaincrate。Rust 的一个小宏魔法的漫漫长路。

创建一个二进制包cargo new --bin test-error-chain,并进到这个目录。 编辑Cargo.toml,添加error-chain="0.8.1"到最后。

error-chain 为你做的是什么, 创建我们所需的所有定义的手动执行错误类型; 创建一个结构体,并实现必要的 trait : DisplayDebugError,也默认实现 From , 所以字符串 可以转换成错误。

我们的src/main.rs文件看起来像这样。所有的主要程序都是给run调用,打印出错误,并用非零退出代码结束程序。 error_chain宏,会在定义error的模块里面生成所有所需的 - 在一个更大的程序中,你会把error的模块放在它自己的文件中。 我们需要把放进error的所有东西,带回到全局作用域,因为我们的代码需要生成的 traits。 默认情况下,随带有一个Error结构和一个Result的定义。

我们也要求From的实现,这样使用foreign_linksstd::io::Error才会转换为我的错误类型:

#[macro_use]
extern crate error_chain;

mod errors {
    error_chain!{
        foreign_links {
            Io(::std::io::Error);
        }
    }
}
use errors::*;

fn run() -> Result<()> {
    use std::fs::File;

    File::open("file")?;

    Ok(())
}


fn main() {
    if let Err(e) = run() {
        println!("error: {}", e);

        std::process::exit(1);
    }
}
// error: No such file or directory (os error 2)

‘foreign_links’让我们的生活更轻松,因为问号符号现在知道如何转换std::io::Error进入我们的error::Error。 (在引擎盖下,宏正在创建一个From<std::io::Error>转换实现,正如前面所述。 )

所有的行动都发生在run;让我们打印出作为第一个程序参数给出的文件的前 10 行。 有可能或不会有这样的参数,这不一定是错误的。 这里我们要转换一个Option<String>到一个Result<String>Option有两个做这种转换的方法,我选择了最简单的一种。 我们的Error类型为&str实现From,所以用一个简单的文本就可以很容易制作一个错误。


# #![allow(unused_variables)]
#fn main() {
fn run() -> Result<()> {
    use std::env::args;
    use std::fs::File;
    use std::io::BufReader;
    use std::io::prelude::*;

    let file = args().skip(1).next()
        .ok_or(Error::from("provide a file"))?;

    let f = File::open(&file)?;
    let mut l = 0;
    for line in BufReader::new(f).lines() {
        let line = line?;
        println!("{}", line);
        l += 1;
        if l == 10 {
            break;
        }
    }

    Ok(())
}
#}

(再次) 有一个有用的小宏bail!,用于’抛出’错误。ok_or方法的一个替代方案:


# #![allow(unused_variables)]
#fn main() {
    let file = match args().skip(1).next() {
        Some(s) => s,
        None => bail!("provide a file")
    };
#}

会像?一样,它 提前返回

返回的错误包含一个ErrorKind枚举,这使我们能够区分各种各样的错误。 总有一个Msg变体 (当你用Error::from(str)) 和foreign_links申明包装 I/O 错误的Io:

fn main() {
    if let Err(e) = run() {
        match e.kind() {
            &ErrorKind::Msg(ref s) => println!("msg {}",s),
            &ErrorKind::Io(ref s) => println!("io {}",s),
        }
        std::process::exit(1);
    }
}
// $ cargo run
// msg provide a file
// $ cargo run foo
// io No such file or directory (os error 2)

添加新的错误很简单。 添加一个Error部分给error_chain!宏:


# #![allow(unused_variables)]
#fn main() {
    error_chain!{
        foreign_links {
            Io(::std::io::Error);
        }

        errors {
            NoArgument(t: String) {
                display("no argument provided: '{}'", t)
            }
        }

    }
#}

这定义了Display如何应用在这种新的错误。 现在我们可以更具体地处理’no argument’的错误,喂给ErrorKind::NoArgument一个String值:


# #![allow(unused_variables)]
#fn main() {
    let file = args().skip(1).next()
        .ok_or(ErrorKind::NoArgument("filename needed".to_string()))?;
#}

现在有一个额外的,您必须匹配的ErrorKind变体:

fn main() {
    if let Err(e) = run() {
        println!("error {}",e);
        match e.kind() {
            &ErrorKind::Msg(ref s) => println!("msg {}", s),
            &ErrorKind::Io(ref s) => println!("io {}", s),
            &ErrorKind::NoArgument(ref s) => println!("no argument {:?}", s),
        }
        std::process::exit(1);
    }
}
// cargo run
// error no argument provided: 'filename needed'
// no argument "filename needed"

一般来说,尽可能使错误尽可能具有特定的意义,尤其 如果是一个库函数! 这种 match-on-kind 技术几乎相当于传统的异常处理,您可以在catch要么except块种匹配异常类型。

综上所述,error-chain为你创建一个类型Errorstd::result::Result<T,Error>定义为Result<T>Error包含一个枚举ErrorKind,并且默认情况下有一个变体Msg用于从 String 创建的错误。 你用foreign_links来定义外部错误,这有两件事。首先,它创建一个新的ErrorKind变种。 其次,它在这些外部错误上实现了From,所以他们可以转换成我们的错误。新的错误变体很容易地添加。许多恼人的样板代码被淘汰。

错误的链化

但这个箱子提供的非常酷的东西是 error-链化.

作为一个 用户 ,当一个方法只是’抛出’一个通用的 I/O 错误时,这是烦人的。 好吧,它不能打开一个文件,很好,但这又是什么文件? 简单点来说,这个信息对我有什么用处?

error_chain 给出了 error-链化 答案, 这有助于解决过度通用错误的问题。 当我们尝试打开文件时,我们可以懒洋洋地用?,看着它变成io::Error, 或者你可以选择 链化 这错误。


# #![allow(unused_variables)]
#fn main() {
// 普通错误
let f = File::open(&file)?;

// 一个特殊的错误链
let f = File::open(&file).chain_err(|| "unable to read the damn file")?;
#}

这里是该程序的新版本, 没有 导入’foreign’错误,只是默认值:

#[macro_use]
extern crate error_chain;

mod errors {
    error_chain!{
    }

}
use errors::*;

fn run() -> Result<()> {
    use std::env::args;
    use std::fs::File;
    use std::io::BufReader;
    use std::io::prelude::*;

    let file = args().skip(1).next()
        .ok_or(Error::from("filename needed"))?;

    ///////// 显式链化! ///////////
    let f = File::open(&file).chain_err(|| "unable to read the damn file")?;

    let mut l = 0;
    for line in BufReader::new(f).lines() {
        let line = line.chain_err(|| "cannot read a line")?;
        println!("{}", line);
        l += 1;
        if l == 10 {
            break;
        }
    }

    Ok(())
}


fn main() {
    if let Err(e) = run() {
        println!("error {}", e);

        /////// 查看错误链... ///////
        for e in e.iter().skip(1) {
            println!("caused by: {}", e);
        }

        std::process::exit(1);
    }
}
// $ cargo run foo
// error unable to read the damn file
// caused by: No such file or directory (os error 2)

所以chain_err方法接受原始错误,并创建一个包含原始错误的新错误 - 这可以无限期地持续下去。 这个闭包函数期待那些能 转换 为错误的值。

Rust 宏可以明显地为您节省大量的打字工作。 error-chain甚至提供了一个取代整个主程序的捷径:

quick_main!(run);

(run就是所有行动的地点,无需管其他。 )

线程,网络和共享

目录

改变不可变的

如果你感觉很猪头 (如我),你想知道是否 有过 可能避开借用检查器的限制。

考虑下面的小程序,它编译和运行没有问题。

// cell.rs
use std::cell::Cell;

fn main() {
    let answer = Cell::new(42);

    assert_eq!(answer.get(), 42);

    answer.set(77);

    assert_eq!(answer.get(), 77);
}

answer 已经改变了 - 但是answer 变量 是不可变的!

这显然是非常安全的,因为单元格内的值只能通过setget访问。 这正是盛大名称 内部可变性。 通常被称为 遗传可变性: 如果我有一个结构值v,如果v本身是可写的,那么我可以加个字段v.aCell值放宽了这个规则,因为我们可以用set改变其中包含的值,即使 cell 本身不可变。

然而,Cell只适用于Copy类型 (例如,派生了Copytrait 的原始类型和用户类型)。

对于其他值,我们必须得到一个可以工作的引用,可变或不可变。这RefCell提供的是 - 您明确要求,一个包含值的引用:

// refcell.rs
use std::cell::RefCell;

fn main() {
    let greeting = RefCell::new("hello".to_string());

    assert_eq!(*greeting.borrow(), "hello");
    assert_eq!(greeting.borrow().len(), 5);

    *greeting.borrow_mut() = "hola".to_string();

    assert_eq!(*greeting.borrow(), "hola");
}

再次,greeting不是可变的声明!

明确的解引用操作符*,可能在 Rust 中有点混乱,因为通常你不需要它 - 例如greeting.borrow().len()因为方法调用会隐含地解引用,所以很好。 但是你 确实 需要*,从greeting.borrow()把底层&String拉出,或者从greeting.borrow_mut()&mut String拉出。

RefCell并不总是安全的,因为从这些方法返回的任何引用必须遵循通常的规则。


# #![allow(unused_variables)]
#fn main() {
    let mut gr = greeting.borrow_mut(); // gr 是一个可变借用
    *gr = "hola".to_string();

    assert_eq!(*greeting.borrow(), "hola"); //<== 这里我们失败了!
....
thread 'main' panicked at 'already mutably borrowed: BorrowError'
#}

你能在有可变借用的情况下,再搞个不可变借用 - 这很重要 - 违反规则的事情发生在 运行时。 解决方案 (一如既往) 是尽可能地限制可变借用的作用域 - 在这种情况下,您可以在此处放置两行区域块{},以便可变引用gr在我们再次借用之前释放。

所以,若没有理由,这不应该是一个你使用的功能,除非你 会得到一个编译时错误。 这些类型在通常规则下做不到的情况下,提供 动态借用 的策略。

共享的引用

目前来说,值与其借来的引用之间的关系在编译时已经清楚明了。 值是所有者,且引用不能长命过它。 但许多案例根本不适合这种整洁的模式。 例如,假设我们有一个Player结构和一个Role结构。 一个Player存有多个Role对象引用的 一个 Vec。 这些值之间并没有一个整齐的一对一关系,并且rustc难以合作。

Rc工作上像Box- 分配堆内存和值会移到该内存。如果你克隆一个Box,它会分配一个值的深拷贝。 但克隆一个Rc是便宜的,因为每次你克隆它只是更新一个到 相同的数据引用计数 。 这是一种古老且非常流行的内存管理策略,例如用于 iOS/MacOS 上的 Objective C 运行时。 (在现代 C ++中,它是用std::shared_ptr。 )

译者: 扩展阅读,非官方中文:Rc<T> 引用计数智能指针

当一个Rc被释放时,引用计数递减。 当该计数变为零时,拥有的值将被丢弃并释放内存。

// rc1.rs
use std::rc::Rc;

fn main() {
    let s = "hello dolly".to_string();
    let rs1 = Rc::new(s); // s 移动 堆; ref 计为 1
    let rs2 = rs1.clone(); // ref 计为 2

    println!("len {}, {}", rs1.len(), rs2.len());
} //  rs1 和 rs2 释放, 字符串 挂了.

您可以根据自己的喜好,制作尽可能多的最初值的引用 - 这会再次 动态借用 。 您不必盯着T及其引用&T值之间的关系。因有一些运行时间成本,所以它不是你选择的 第一 解决方案,但它使共享模式成为可能,这是借用检查器所允许的。 注意Rc为您提供不可变的共享引用,因为若是可变引用,会破坏借用的基本规则之一。(超过一个可变引用,导致竟态问题)

在一个Player的情况下,它现在将它的 角色定位(roles) 存为Vec<Rc<Role>>并且工作很好 - 我们可以添加或删除 Role,但不能在创建后 更改 他们。

但是,如果每个Player都有一个对 team 的引用,team 也就是个对一些Player引用的 Vec? 那么一切都变得不可变,因为Player所有的值需要被存储为Rc! 而这时候RefCell变得很有必要。 team 可能被定义为Vec<Rc<RefCell<Player>>>。也许现在想用borrow_mut更改一个Player值,与此同时不会 提出 对 一个Player的引用 ‘查房’。 例如,假设我们有一条规则,即如果某位球员变强,那么队伍的所有人都会变得更强:


# #![allow(unused_variables)]
#fn main() {
    for p in &self.team {
        p.borrow_mut().make_stronger();
    }
#}

所以应用程序代码不是太糟糕,但类型签名会有点吓人。 你总是可以用一个type别名,来简化它们:


# #![allow(unused_variables)]
#fn main() {
type PlayerRef = Rc<RefCell<Player>>;
#}

多线程

在过去的二十年中,从原始处理速度转向具有多核的 CPU。 因此,充分利用现代计算机的唯一方法是保持所有这些核心繁忙。 正如我们所看到的,通过Command可以在后台 spawn 子进程,但这仍存在一个同步问题: 我们不能不等,因为我们不确切地知道这些孩子何时完成。

还有其他原因需要分开 执行线程, 当然。 例如,您锁定整个进程,只为了等待 I/O 的堵塞。

Spawn 线程在 Rust 中很简单 - 喂给spawn一个在后台执行的闭包,就可以了。

// thread1.rs
use std::thread;
use std::time;

fn main() {
    thread::spawn(|| println!("hello"));
    thread::spawn(|| println!("dolly"));

    println!("so fine");
    // 稍等一下
    thread::sleep(time::Duration::from_millis(100));
}
// so fine
// hello
// dolly

显然,只是”稍等一下”并不是一个非常严格的解决方案! 更好的是,在返回的对象上调用join - 然后主线程会等待生成的线程结束。

// thread2.rs
use std::thread;

fn main() {
    let t = thread::spawn(|| {
        println!("hello");
    });
    println!("wait {:?}", t.join());
}
// hello
// wait Ok(())

这是一个有趣的变化: 强制新线程恐慌。


# #![allow(unused_variables)]
#fn main() {
    let t = thread::spawn(|| {
        println!("hello");
        panic!("I give up!");
    });
    println!("wait {:?}", t.join());
#}

我们如预期般 panic,但只有恐慌的线程死亡! 我们仍会打印出来的join错误信息。 所以是的,恐慌并不总是致命的,但线程相对昂贵,所以这不应被视为处理恐慌的常规方式。

hello
thread '<unnamed>' panicked at 'I give up!', thread2.rs:7
note: Run with `RUST_BACKTRACE=1` for a backtrace.
wait Err(Any)

返回的对象可以用来跟踪多个线程:

// thread4.rs
use std::thread;

fn main() {
    let mut threads = Vec::new();

    for i in 0..5 {
        let t = thread::spawn(move || {
            println!("hello {}", i);
        });
        threads.push(t);
    }

    for t in threads {
        t.join().expect("thread failed");
    }
}
// hello 0
// hello 2
// hello 4
// hello 3
// hello 1

Rust 坚持我们处理连接失败的情况 - 即该线程发生恐慌。 (当发生这种情况时,你通常不会退出主程序,只记下错误,重试等)

没有特定的线程执行顺序 (不同运行提供不同顺序) ,这就是关键 - 它们确实是 独立的执行线程。 多线程不难; 并发 才难 - 管理和 同步多个执行的线程.

线程不借

线程中的闭包函数有可能捕获值,但通过 移动,而不是 借用!

// thread3.rs
use std::thread;

fn main() {
    let name = "dolly".to_string();
    let t = thread::spawn(|| {
        println!("hello {}", name);
    });
    println!("wait {:?}", t.join());
}

以下是有用的错误消息:

error[E0373]: closure may outlive the current function, but it borrows `name`, which is owned by the current function
 --> thread3.rs:6:27
  |
6 |     let t = thread::spawn(|| {
  |                           ^^ may outlive borrowed value `name`
7 |         println!("hello {}", name);
  |                             ---- `name` is borrowed here
  |
help: to force the closure to take ownership of `name` (and any other referenced variables), use the `move` keyword, as shown:
  |     let t = thread::spawn(move || {

很好解释! 想象一下使用move的情况,从一个函数产生这个线程 - 在函数调用结束和原name被释放之后,该线程还可能活着。所以添加move解决了我们的问题。

但这是一个 移动 ,所以name可能只会出现在一个线程中! 我想强调的 ,这可能为共享引用,但他们需要有静态生命周期:


# #![allow(unused_variables)]
#fn main() {
let name = "dolly";
let t1 = thread::spawn(move || {
    println!("hello {}", name);
});
let t2 = thread::spawn(move || {
    println!("goodbye {}", name);
});
#}

name在整个项目期间存在(静态),所以rustc对闭包不会长命过name感到满意。 但是,大多数有趣的引用没有静态生命周期!

线程无法共享相同的环境 - 这是 Rust 的 设计。 特别是,他们不能共享常规引用,因为闭包会移动捕获的变量。

共享引用 还好,因为他们的生命周期是’与需要的一样长’ - 但你不能为此而使用Rc. 这是因为Rc不是 线程安全 的- 它针对非线程情况进行了优化。 幸运的是,使用Rc在这里是个编译错误;编译器一直在你的背后。

对于线程,你需要std::sync::Arc- ‘Arc’代表’原子引用计数’。 也就是说,它保证了引用计数将在一个逻辑操作中被修改。 为了保证这一点,它必须确保操作被锁定,以便只有当前线程才能访问。clone实际上的成本比 copy 仍要低得多。

// thread5.rs
use std::thread;
use std::sync::Arc;

struct MyString(String);

impl MyString {
    fn new(s: &str) -> MyString {
        MyString(s.to_string())
    }
}

fn main() {
    let mut threads = Vec::new();
    let name = Arc::new(MyString::new("dolly"));

    for i in 0..5 {
        let tname = name.clone();
        let t = thread::spawn(move || {
            println!("hello {} count {}", tname.0, i);
        });
        threads.push(t);
    }

    for t in threads {
        t.join().expect("thread failed");
    }
}

这里,虽然我们MyString不实现clone,但我故意创建了一个String的包装类型 (一个’新类型’)。 共享引用 可以 clone!

共享引用name通过使用clone一个新引用,传递给每个新线程并将其移入闭包。 这有点冗长,但这是一种安全模式。 恰恰因为问题如此不可预测,安全在并发中很重要。 一个程序可能在你的机器上运行良好,但偶尔会在服务器上崩溃,通常在周末。 更糟糕的是,这些问题的症状不容易诊断。

通道

有线程间发送数据的方法. 这是, Rust 在使用 通道. std::sync::mpsc::channel()返回,一个由 接收器{receiver} 通道 和 寄件人{sender} 通道组成的元组。每个线程都收到了使用clone制作的发件人副本,和调用send。 同时主线程在接收器上调用recv

‘MPSC’代表’Multiple Producer Single Consumer’。 我们创建了多个试图发送到通道的线程,且主线程”消耗”这通道。

// thread9.rs
use std::thread;
use std::sync::mpsc;

fn main() {
    let nthreads = 5;
    let (tx, rx) = mpsc::channel();

    for i in 0..nthreads {
        let tx = tx.clone();
        thread::spawn(move || {
            let response = format!("hello {}", i);
            tx.send(response).unwrap();
        });
    }

    for _ in 0..nthreads {
        println!("got {:?}", rx.recv());
    }
}
// got Ok("hello 0")
// got Ok("hello 1")
// got Ok("hello 3")
// got Ok("hello 4")
// got Ok("hello 2")

由于线程在结束执行之前,会发送响应,显然这可能随时发生,因此这示例无需 joinrecv将阻塞,并且如果发送者通道断开连接,将返回一个错误。 recv_timeout只会在给定的时间段内阻塞,并且可能还会返回一个超时错误。

send从不阻塞,这很有用,因为线程可以在不等待接收器处理的情况下,推出数据。另外,通道有缓冲,所以可以发生多个发送操作,按顺序接收。

但是,不堵塞的情况下,Ok并不意味着’已成功发送消息’!

一个sync_channel 堵塞发送。如果参数为零,则发送会阻塞,直到 接收 发生。 线程必须满足或 会合{rendezvous} (根据声音原则,法语中大多数声音听起来更好。 )


# #![allow(unused_variables)]
#fn main() {
    let (tx, rx) = mpsc::sync_channel(0);

    let t1 = thread::spawn(move || {
        for i in 0..5 {
            tx.send(i).unwrap();
        }
    });

    for _ in 0..5 {
        let res = rx.recv().unwrap();
        println!("{}",res);
    }
    t1.join().unwrap();
#}

在调用recv期间,若没有相应send,我们会很容易错误。例如,少个循环for i in 0..4,那么线程结束后,tx被扔掉,然后recv将失败。如果线程恐慌,导致其栈解散,释放任何值,也会错误。

如果sync_channel是用一个非零参数n创建的,那么它的行为就像一个具有最大尺寸n的队列-send只会在尝试添加,超过n个值到队列时,才会堵塞.

通道是强类型的 - 在这里通道有类型i32- 但类型推理使其隐藏。 如果您需要传递不同类型的数据,那么枚举是表达这一点的好方法。

同步

我们来看看 同步join是非常基本的操作,只是去等一个特定的线程完成。 一个sync_channel去同步两个线程 - 在最后一个例子中,衍生线程和主线程完全锁定在一起。

同步的 Barrier(屏障) 是一个检查点,在 所有 的点都到位前,线程必须等着。都到位后,才可以像以前一样继续前进。 屏障是随着我们想要等待的线程们一起创建的。和以前一样,我们使用use Arc与所有线程共享 Barrier。

// thread7.rs
use std::thread;
use std::sync::Arc;
use std::sync::Barrier;

fn main() {
    let nthreads = 5;
    let mut threads = Vec::new();
    let barrier = Arc::new(Barrier::new(nthreads));

    for i in 0..nthreads {
        let barrier = barrier.clone();
        let t = thread::spawn(move || {
            println!("before wait {}", i);
            barrier.wait();
            println!("after wait {}", i);
        });
        threads.push(t);
    }

    for t in threads {
        t.join().unwrap();
    }
}
// before wait 2
// before wait 0
// before wait 1
// before wait 3
// before wait 4
// after wait 4
// after wait 2
// after wait 3
// after wait 0
// after wait 1

半随机的线程启动,全部满足后,继续。它就像一种可恢复join,当您需要将工作片段分散到不同的线程,并且想在所有线程完成时,采取一些行动,这时会有用。

共享的状态

线程怎么样 修改 共享状态?

回想一下RC<RefCell<T>>策略,它是在共享引用上 动态 做一个可变的借用。 而在线程上相当于RefCell的,就是Mutex- 你可以通过调用lock来获得可变引用。 当存在此引用,其他线程将无法访问它。 互斥{mutex}代表’相互排斥’ - 我们锁定了一段代码,以便只有一个线程可以访问它,然后解锁它。 你用lock方法锁上,并在该引用被释放时解锁。

// thread9.rs
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;

fn main() {
    let answer = Arc::new(Mutex::new(42));

    let answer_ref = answer.clone();
    let t = thread::spawn(move || {
        let mut answer = answer_ref.lock().unwrap();
        *answer = 55;
    });

    t.join().unwrap();

    let ar = answer.lock().unwrap();
    assert_eq!(*ar, 55);

}

这不像使用RefCell那样简单,因为如果另一个线程在锁定时发生恐慌,那么请求互斥锁就可能失败。 (在这种情况下,文档的实际建议是用unwrap退出线程,因为事情严重错了!)

要这个可变的借用的存在,尽可能短更为重要,因为只要互斥锁被锁定,其他的线程都 堵塞。 这不应该你花大价钱的地方! 所以通常这样的代码会像这样使用:


# #![allow(unused_variables)]
#fn main() {
// ... 在线程 do something
// 获得一个锁上的引用 并 短暂使用它
{
    let mut data = data_ref.lock().unwrap();
    // 修改数据
}
//... 线程继续
#}

更高级别的操作

最好找到更高级的线程化方法,而不是自己管理同步。 一个例子是当你需要并发做事并收集结果时。 一个非常酷的箱子是pipeliner它有一个非常直接的 API。 这里是该箱的’你好,世界!’例子- 一个迭代器给我们输入,我们在值上执行n`次并行操作。

extern crate pipeliner;
use pipeliner::Pipeline;

fn main() {
    for result in (0..10).with_threads(4).map(|x| x + 1) {
        println!("result: {}", result);
    }
}
// result: 1
// result: 2
// result: 5
// result: 3
// result: 6
// result: 7
// result: 8
// result: 9
// result: 10
// result: 4

这当然是一个笨例子,因为该操作计算起来非常便宜,但显示了并行运行代码的容易程度。

这有些更有用的东西. 并行执行网络操作非常有用,因为它们可能需要时间,并且在开始工作之前,您不希望等待它们 所有 完成。

这个例子非常粗糙 (相信我,有更好的方法可以做到这一点),但这里我们要关注这个实践。 我们重用定义在第 4 节中的这个shell函数,在一系列 IP4 地址上调用ping

extern crate pipeliner;
use pipeliner::Pipeline;

use std::process::Command;

fn shell(cmd: &str) -> (String,bool) {
    let cmd = format!("{} 2>&1",cmd);
    let output = Command::new("/bin/sh")
        .arg("-c")
        .arg(&cmd)
        .output()
        .expect("no shell?");
    (
        String::from_utf8_lossy(&output.stdout).trim_right().to_string(),
        output.status.success()
    )
}

fn main() {
    let addresses: Vec<_> = (1..40).map(|n| format!("ping -c1 192.168.0.{}",n)).collect();
    let n = addresses.len();

    for result in addresses.with_threads(n).map(|s| shell(&s)) {
        if result.1 {
            println!("got: {}", result.0);
        }
    }

}

我的家庭网络上的结果如下所示:

got: PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=43.2 ms

--- 192.168.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 43.284/43.284/43.284/0.000 ms
got: PING 192.168.0.18 (192.168.0.18) 56(84) bytes of data.
64 bytes from 192.168.0.18: icmp_seq=1 ttl=64 time=0.029 ms

--- 192.168.0.18 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.029/0.029/0.029/0.000 ms
got: PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=110 ms

--- 192.168.0.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 110.008/110.008/110.008/0.000 ms
got: PING 192.168.0.5 (192.168.0.5) 56(84) bytes of data.
64 bytes from 192.168.0.5: icmp_seq=1 ttl=64 time=207 ms
...

在前半秒内,活动的地址会非常快速地输出,然后等待不好的结果。不然的话,我们会等待一分钟,才能看到活动地址! 您现在可以继续从输出中删除 ping 时间等事情,尽管这只能在 Linux 上运行。 ping是普遍的,但确切的输出格式在每个平台都是不同的。 为了做得更好,我们需要使用跨平台的 Rust 网络 API,所以让我们进入网络。

解决地址问题的更好方法

如果你 想要可用,但不详细的 ping 统计信息,std::net::ToSocketAddrs trait 会为你做任何 DNS 解析:

use std::net::*;

fn main() {
    for res in "google.com:80".to_socket_addrs().expect("bad") {
        println!("got {:?}", res);
    }
}
// got V4(216.58.223.14:80)
// got V6([2c0f:fb50:4002:803::200e]:80)

它是一个迭代器,因为通常一个域名有多个相关联的接口 - Google 同时有 IPV4 和 IPV6 的接口。

所以,我们自然地使用这种方法来重写 pipeliner 示例。 大多数网络协议都使用地址和端口:

extern crate pipeliner;
use pipeliner::Pipeline;

use std::net::*;

fn main() {
    let addresses: Vec<_> = (1..40).map(|n| format!("192.168.0.{}:0",n)).collect();
    let n = addresses.len();

    for result in addresses.with_threads(n).map(|s| s.to_socket_addrs()) {
        println!("got: {:?}", result);
    }
}
// got: Ok(IntoIter([V4(192.168.0.1:0)]))
// got: Ok(IntoIter([V4(192.168.0.39:0)]))
// got: Ok(IntoIter([V4(192.168.0.2:0)]))
// got: Ok(IntoIter([V4(192.168.0.3:0)]))
// got: Ok(IntoIter([V4(192.168.0.5:0)]))
// ....

这比 ping 示例快得多,因为它只是检查 IP 地址是否有效 - 如果我们为它提供了一个实际域名列表,DNS 查找可能需要一些时间,这时并行性显得格外重要。

令人惊讶的是,它能工作。 标准库中的所有内容实现Debug的事实,非常适合勘探和调试。 迭代器正在返回Result (有Ok) 和这个Result是一个IntoIterSocketAddrSocketAddr是一个带有 ipv4 或 ipv6 地址的枚举。 为什么是IntoIter? 由于套接字可能有多个地址 (例如,ipv4 和 ipv6) 。


# #![allow(unused_variables)]
#fn main() {
    for result in addresses.with_threads(n)
        .map(|s| s.to_socket_addrs().unwrap().next().unwrap())
    {
        println!("got: {:?}", result);
    }
// got: V4(192.168.0.1:0)
// got: V4(192.168.0.39:0)
// got: V4(192.168.0.3:0)
#}

这也能工作,惊讶吧。 首先 unwarp摆脱了Result,然后我们明确地将第一个值(Result 类型)从迭代器中取出(next)。当我们给出一个无意义的地址时 (比如没有端口的地址名称),Result通常会变得很糟糕。

TCP 客户端服务器

Rust 为最常用的网络协议 TCP ,提供了一个直接的接口。它具有很强的抗错能力,是我们网络世界的基础 - 的数据被发送和接收,并带有确认性。 相比之下,UDP 将数据包发送到外面,就不管确认性 - 有一个笑话是”我可以告诉你一个关于 UDP 的笑话,但你可能得不到这笑话。” (Jokes about networking are only funny for a specialized meaning of the word ‘funny’)

但是,错误处理是对于网络来说很 非常 重要,因为任何事情都可能发生,并且最终会发生。

TCP 作为客户端/服务器工作的模型; 服务器监听一个地址和一个特定的 网络端口,并且客户端连接到该服务器。建立连接后,客户端和服务器可以用套接字进行通信。

TcpStream::connect需要可以转换成一个SocketAddr的任何结构,在这里是,一直使用的纯 string。

Rust 实现一个简单的 TCP 客户端很简单 - 一个TcpStream结构是可读和可写的。 像往常一样,我们必须将Read,Write和其他std::io tarit 纳入作用域:

// client.rs
use std::net::TcpStream;
use std::io::prelude::*;

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:8000").expect("connection failed");

    write!(stream,"hello from the client!\n").expect("write failed");
 }

服务器并不复杂,我们建立了一个监听器并等待连接。 当客户端连接时,我们在服务器端得到一个TcpStream。 在这种情况下,我们读取客户端写入字符串的所有内容。

// server.rs
use std::net::TcpListener;
use std::io::prelude::*;

fn main() {

    let listener = TcpListener::bind("127.0.0.1:8000").expect("could not start server");

    // accept connections and get a TcpStream
    for connection in listener.incoming() {
        match connection {
            Ok(mut stream) => {
                let mut text = String::new();
                stream.read_to_string(&mut text).expect("read failed");
                println!("got '{}'", text);
            }
            Err(e) => { println!("connection failed {}", e); }
        }
    }
}

这里我随机选择了一个无用端口的端口号,但是大多数的端口被赋予一些含义.

请注意,双方必须就协议达成一致 - 客户希望能够向 流{stream} 写入文本,并且服务器期望从 流{stream} 中读取文本。 如果他们不玩同一个游戏,那么情况就会发生在一方被阻塞的情况下,等待从未到来的字节。

检查错误非常重要 - 网络 I/O 可能因多种原因失败,并且在本地文件系统上,可能会定期发生一次 ‘蓝月亮’(blue moon) 的错误。 有人可以网线传输,另一方可能会崩溃,等等可能的情况。这个小服务器不是很健壮,因为它会在第一次读取错误时崩溃。

这是一个更坚实的服务器,可以在不崩溃的情况下处理错误。它还从数据流明确读取一个 行{line} ,这是使用了IO::BufReader创造一个IO::BufRead,和我们调用read_line

// server2.rs
use std::net::{TcpListener, TcpStream};
use std::io::prelude::*;
use std::io;

fn handle_connection(stream: TcpStream) -> io::Result<()>{
    let mut rdr = io::BufReader::new(stream);
    let mut text = String::new();
    rdr.read_line(&mut text)?;
    println!("got '{}'", text.trim_right());
    Ok(())
}

fn main() {

    let listener = TcpListener::bind("127.0.0.1:8000").expect("could not start server");

    // 接受 连接 和获得一个 TcpStream
    for connection in listener.incoming() {
        match connection {
            Ok(stream) => {
                if let Err(e) = handle_connection(stream) {
                    println!("error {:?}", e);
                }
            }
            Err(e) => { print!("connection failed {}\n", e); }
        }
    }
}

handle_connectionread_line可能会失败,但由此产生的错误是安全处理的。

像这样的单向通信当然是有用的 - 例如,通过网络提供的一组服务,希望在一个中心位置将他们的状态报告集中在一起。 但期望有礼貌的回复是合理的,即使只有个’好’字!

一个基本的’echo’服务器的简单例子。 客户端将一些以换行符结尾的文本写入服务器,并使用换行符接收相同的文本 - stream 是可读写的.

// client_echo.rs
use std::io::prelude::*;
use std::net::TcpStream;

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:8000").expect("connection failed");
    let msg = "hello from the client!";

    write!(stream,"{}\n", msg).expect("write failed");

    let mut resp = String::new();
    stream.read_to_string(&mut resp).expect("read failed");
    let text = resp.trim_right();
    assert_eq!(msg,text);
}

服务器有一个有趣点。 只要改改handle_connection:


# #![allow(unused_variables)]
#fn main() {
fn handle_connection(stream: TcpStream) -> io::Result<()>{
    let mut ostream = stream.try_clone()?;
    let mut rdr = io::BufReader::new(stream);
    let mut text = String::new();
    rdr.read_line(&mut text)?;
    ostream.write_all(text.as_bytes())?;
    Ok(())
}
#}

这是一个简单的双向套接字通信的常见问题; 我们想要读取一行,因此需要将可读 stream 提供给BufReader- 但它 消耗 stream ! 所以我们必须克隆这个 stream,创建一个引用相同底层套接字的新结构。从此我们,幸福生活。

目录

Rust 中的面向对象

世界各地的人们,以前的编程语言,是以某种方式实现面向对象编程 (OOP) 的可能性很大:

  • ‘类’作为生成 对象 (通常被称为 实例) 并定义唯一的类型的工厂。
  • 类可能 继承 其他类 (父母) 的数据 (字段) 和行为 (方法)。
  • 如果 B 继承 A,那么将 B 的一个实例传递给希望接收 A (子类)的函数,是可能的。
  • 一个对象应该隐藏它的数据 (封装) ,只用方法操作。

面向对象的 设计理念 在于识别类 (’名词’) 和方法 (’动词’) ,然后建立它们之间的关系,关心它 是一个 什么 和它 有一个 什么。

在旧版”星际迷航”系列中,医生会对船长说: “这是人生,吉姆,但不是我们所知道的人生”。 这非常适用于 Rust 的面向对象风格: 它会带来震撼,因为 Rust 数据容器类型 (结构,枚举和元组) 都是很笨的,虽然你可以在其上定义方法,使数据本身是私有的,搭上所有常用的封装策略,但是它们之间都是 不相干的类型。 没有父类,没有继承 (除了Deref强制转换的个例。)

Rust 中各种数据类型之间的关系,由所拥有的 trait 来确立。 要学好 Rust 的很大一部分,是理解标准库 trait 是如何操作的,因为这是把所有数据类型粘在一起的意识网络。

trait 很有趣,因为它们与主流语言的概念之间没有一一对应的关系。 这取决于你是站在动态还是静态的角度思考。 在动态的情况下,它们更像 Java 或 Go 的接口。

trait 对象

考虑一下,第一个介绍 trait 的例子:


# #![allow(unused_variables)]
#fn main() {
trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("eight-byte float {}", self)
    }
}
#}

受到很大影响的小程序,如下:

fn main() {
    let answer = 42;
    let maybe_pi = 3.14;
    let v: Vec<&Show> = vec![&answer,&maybe_pi];
    for d in v.iter() {
        println!("show {}",d.show());
    }
}
// show four-byte signed 42
// show eight-byte float 3.14

这是 Rust 需要一些类型指导的一种情况 - 我指定了一个 Vec,其存有实现了 Show 的引用 。请注意 i32f64彼此之间是没有任何关系的,但他们都知道show`方法,因为它们都实现了相同的 trait 。 这些名称方法是 虚构的,因为不同类型的实际方法有不同的代码,而正确的那个方法要根据 运行时 信息决定。或trait 对象信息。

就是你可以将不同类型的对象放在同一个 Vec 的原因(Vec<&Show>)。 如果您有 Java 或 Go 背景,您可以把Show想成是接口。

细化下 - 我们把值放进Box。一个盒子包含对分配在堆上数据的引用,并在行为上非常像引用 - 它是一个 智能指针。 像引用,当盒子超出作用域和Drop会启动,然后释放内存。


# #![allow(unused_variables)]
#fn main() {
let answer = Box::new(42);
let maybe_pi = Box::new(3.14);

let show_list: Vec<Box<Show>> = vec![question,answer];
for d in &show_list {
    println!("show {}",d.show());
}
#}

不同之处在于,您现在可以使用该 Vec,当成一个引用去传递,或者不必跟踪任何借用的引用就可以将其传递出去。 当 Vec 被释放时,这些盒子值跟被释放,并且所有的内存都被回收。

动物

出于某种原因,任何关于面向对象和继承的讨论,似乎最终都会讨论到动物。 它创造了一个不错的故事: “看,猫是食肉动物,而食肉动物是动物”。

但我会从 Ruby 宇宙的经典口号: “如果它嘎嘎叫,那就是鸭子” 开始。 你所有的对象必须做的,就是定义 嘎嘎{quacks}方法和,狭义来看,它就是鸭子。


# #![allow(unused_variables)]
#fn main() {
trait Quack {
    fn quack(&self);
}

struct Duck ();

impl Quack for Duck {
    fn quack(&self) {
        println!("quack!");
    }
}

struct RandomBird {
    is_a_parrot: bool
}

impl Quack for RandomBird {
    fn quack(&self) {
        if ! self.is_a_parrot {
            println!("quack!");
        } else {
            println!("squawk!");
        }
    }
}

let duck1 = Duck();
let duck2 = RandomBird{is_a_parrot: false};
let parrot = RandomBird{is_a_parrot: true};

let ducks: Vec<&Quack> = vec![&duck1,&duck2,&parrot];

for d in &ducks {
    d.quack();
}
// quack!
// quack!
// squawk!
#}

在这里,我们有两种完全不同的类型 (其中一个非常笨,甚至没有数据) ,并且是的,它们都能quacks()。 其中一个的行为有点奇怪 (比如:一只鸭子(duck)),但他们共享相同的方法名称,Rust 可以从类型安全的方式,保存这类对象的集合。

类型安全是一件奇妙的事情。若没有静态类型,你甚至可以插入一个 进入这个 Quackers 集合,最终导致运行时的混乱。

这是一个有趣的:


# #![allow(unused_variables)]
#fn main() {
// 为什么不是!
impl Quack for i32 {
    fn quack(&self) {
        for i in 0..*self {
            print!("quack {} ",i);
        }
        println!("");
    }
}

let int = 4;

let ducks: Vec<&Quack> = vec![&duck1,&duck2,&parrot,&int];
...
// quack!
// quack!
// squawk!
// quack 0 quack 1 quack 2 quack 3
#}

我能说什么? 它嘎嘎声叫,它一定是一只鸭子。 有趣的是,你可以把你的 trait 应用到任何 Rust 值,而不仅仅是”对象”。

(因quack以引用类型的方式传递,需要有明确的解引用符号*,来得到整数。)

然而,你只能用同一个库的 trait 和类型来做这件事,标准库是不允许打’补丁’的,这是另一个 Ruby 人的做法 (也还不是最受欢迎的做法) 。

到目前为止,这个 Quack trait 表现得非常像 Java 接口,并且同现代 Java 接口一样,实现 必要 方法, 提供 这个方法的默认实现。 (该Iteratortrait 就是一个很好的例子。 )

但是,请注意 trait 不属于 定义类型 ,和为任何类型实现新的 trait ,但要受到同一个库的限制。

还能要求,只接收Quack实现者的引用:


# #![allow(unused_variables)]
#fn main() {
fn quack_ref (q: &Quack) {
    q.quack();
}

quack_ref(&d);
#}

而这,就是 Rust 风格的’类’。

由于我们在这里进行 101 编程语言比较,所以我提个, Go 对这个嘎嘎工程的一个有趣看法 - 如果有一个 Go 接口Quack,和具有quack方法的一个类型,那么类型就满足了Quack接口,不需要明确的定义。这也打破了定义好的 Java 模型,并且允许编译时填鸭式类型,代价是一些清晰和类型安全。

但是填鸭式类型有一个问题。OOP 的坏标志之一是,太多的方法有一些通用方法名称,如run。”如果它已经有了 run(),它必是能运行的”,听起来不像鸭鸭那么友善! 所以这让 Go 接口变成了 偶然 有效。在 Rust,虽然DebugDisplaytrait 两者都定义了fmt方法,但他们真的就是不同的事情。

所以 Rust 的 trait 允许传统 多态 OOP。但继承怎么办呢? 人们常指 实现继承 ,Rust 则是 接口继承。就好像一位 Java 程序员不去extend(扩展),改为implements(实现)。实际上,这是推荐的做法来自 Alan Holub。他说:

我曾经参加过一个 Java 用户组会议,James Gosling (Java 的发明人) 是这个会议的功能

演讲者. 在令人难忘的问答环节中,有人问他: “如果你能再一次做 Java,

你会改变什么?””我会放弃 class ,”他回答说,在笑声平息后,

他解释说,真正的问题不是类本身,而是实现继承{implementation inheritance},老是在扩展关系{extends relationship}。

接口继承 (实现关系{the implements relationshup}) 是可取的。

尽可能避免实现继承{implementation inheritance}

所以即使在 Java 中,你也可能过度使用类。

实现继承有一些严重的问题。但它的确很 方便。 如有个臃肿的基类,叫动物和它有很多有用的功能 (它甚至可能暴露它的内部!),到了我们的派生类,就可以使用。也就是说,它是一种代码重用的形式。但是代码重用是一个单独的问题。

理解 Rust 时,区分 实现/接口继承 很重要。

请注意, trait 可能有 已提供 的方法。想想Iterator- 你只 需要 重写next方法,却免费获得大量的方法。 这与现代 Java 接口的”default”方法类似。下面,我们只定义name,和upper_case是定义好(默认)的。 我们 可以 覆盖upper_case,但没有 必要


# #![allow(unused_variables)]
#fn main() {
trait Named {
    fn name(&self) -> String;

    fn upper_case(&self) -> String {
        self.name().to_uppercase()
    }
}

struct Boo();

impl Named for Boo {
    fn name(&self) -> String {
        "boo".to_string()
    }
}

let f = Boo();

assert_eq!(f.name(),"boo".to_string());
assert_eq!(f.upper_case(),"BOO".to_string());
#}

这是个 如同 代码重用的示例,是真的,但注意,它不适用于数据,只适用于接口!

鸭子和泛型

Rust 中一个泛型友好的鸭子函数,就是这样一个简单的例子:


# #![allow(unused_variables)]
#fn main() {
fn quack<Q> (q: &Q)
where Q: Quack {
    q.quack();
}

let d = Duck();
quack(&d);
#}

类型参数是 任何 实现了Quack的类型。quack与上节提到的quack_ref之间有一个重要的区别。 这函数的主体会为 每个 调用类型进行编译,并且不需要虚构方法; 这些函数可以完全内联编译。不同的方式使用 Quack trait ,作为在泛型类型上的一个 约束

这是相当于 C ++的泛型quack (注意这个const) :

template <class Q>
void quack(const Q& q) {
    q.quack();
}

请注意,类型参数不受任何限制。

这是非常多的编译时鸭式输入 - 如果我们传递一个不存在quack方法的类型的引用,那么编译器会抱怨没有 quack 方法。 至少这个错误是在编译时发现的,但是当一个类型被意外有quack时会更糟,Go 就可能发生。 相关的更多模板函数和类会导致可怕的错误消息,因为 没有 对泛型的限制。

你可以定义一个函数,它可以处理在 Quacker 指针上的迭代:

template <class It>
void quack_everyone (It start, It finish) {
    for (It i = start; i != finish; i++) {
        (*i)->quack();
    }
}

每个 迭代器类型It都实现。 Rust 的等价物多少更具挑战性:


# #![allow(unused_variables)]
#fn main() {
fn quack_everyone <I> (iter: I)
where I: Iterator<Item=Box<Quack>> {
    for d in iter {
        d.quack();
    }
}

let ducks: Vec<Box<Quack>> = vec![Box::new(duck1),Box::new(duck2),Box::new(parrot),Box::new(int)];

quack_everyone(ducks.into_iter());
#}

Rust 中的迭代器不是鸭类型的,必须是实现了Iterator的类型 ,在这种情况下,迭代器提供了一些盒化Quack。 所涉及的类型没有歧义,值必须满足Quack。 通常,函数签名是一个 Rust 泛型函数的最具挑战性的事情,这就是为什么我建议阅读标准库的源代码 - 实现 通常比 声明简单得多!

这里唯一的类型参数是实际的迭代器类型,意味着,任何可以具有Box<Duck>序列的迭代器都能用,而不仅仅是一个 Vec 迭代器。

继承

面向对象设计的一个常见问题是,试图将事情强加到一个 是一个 什么的关系中,而忽视 有一个 什么的关系。 四人帮 二十二年前在他们的设计模式书中,说过”首选继承布局”。

这里有一个例子: 你想模拟一些公司的员工,并且雇员{Employee}似乎是一个类的好名字。然后,经理 是一个 员工 (这是真的) ,所以我们开始用一个构建我们的层次结构:

Employee的子类经理{Manager}。这并不像看起来那么流畅。 也许是我们对识别重要名词感到厌倦,也许我们 (无意识地) 认为经理和员工是不同种类的动物? 更好的方式,是雇员 有一个 一个 Roles(角色)的集合,然后一个经理,仅仅是一个有更多的责任和能力的Employee

或考虑车辆 - 从自行车到 300 吨矿车。 有多种方式可以考虑车辆,道路需求 (全地形,城市,铁路等) ,电源来源 (电力,柴油,柴油电力等) ,运货物还是人等等。当您根据一个方面,去创建任何固定层次的类,都会忽略所有其他方面。也就是说,可能有多种的车辆分类!

布局在 Rust 中更为重要,原因很明显,您无法从基类以惰性方式继承功能。

布局还是很重要,因为借用检查器足够聪明,可以知道借来的不同结构字段都是独立的借用。你可以有一个字段的可变借用,同时拥有另一个字段的不可变借用,等等。

Rust 不能说,一个方法只访问一个字段,所以为了实现方便,这些字段应该用自己的方法来构造。 (结构的 外部 接口,可以是任何你喜欢使用的合适 trait。 )

“拆分借来的{split borrowing}”的一个具体例子,会使这个更清晰。

有个拥有一些字符串的结构,和一个方法,是说能可变借用第一字符串。


# #![allow(unused_variables)]
#fn main() {
struct Foo {
    one: String,
    two: String
}

impl Foo {
    fn borrow_one_mut(&mut self) -> &mut String {
        &mut self.one
    }
    ....
}
#}

(这是 Rust 命名约定的一个例子 - 这类方法应该以_mut结尾)

现在,一种借用两个字符串的方法,重用第一种方法:


# #![allow(unused_variables)]
#fn main() {
    fn borrow_both(&self) -> (&str,&str) {
        (self.borrow_one_mut(), &self.two)
    }
#}

这会失败! 因我们既有个self的可变借用,有个 self的不可变借用。 如果 Rust 允许这样的情况发生,那么无法保证 不可变引用(借用) 不会改变。

解决方案很简单:


# #![allow(unused_variables)]
#fn main() {
    fn borrow_both(&self) -> (&str,&str) {
        (&self.one, &self.two)
    }
#}

好了,因为借用检查员,认为这些是独立的借用。所以想象这些字段是一些任意类型,你可以看到在这些字段上调用的方法,不会导致借用问题。

使用Deref是一种限制但非常重要的”继承”,这是’解引用’符号*(语法糖)的 实际 trait 。String实现了Deref<Target=str>,所以&str上定义的所有方法,自动都可用于String! 类似的,Foo的方法可以直接调用Box<Foo>,有些人觉得这有点... 神奇,但它是非常方便能力。 现代 Rust 中有一种更简单的语言,但使用起来并不令人愉快。 它确实应该用于,一个具有所有权-可变的类型和一个简单的借用类型的情况。

一般来说, 这就是 Rust 中的 trait 继承:


# #![allow(unused_variables)]
#fn main() {
trait Show {
    fn show(&self) -> String;
}

trait Location {
    fn location(&self) -> String;
}

trait ShowTell: Show + Location {}
#}

最后一个 trait 简单地将我们两个不同的 trait 合并为一个,尽管它可以指定其他方法。

现在的情况和以前一样:


# #![allow(unused_variables)]
#fn main() {
#[derive(Debug)]
struct Foo {
    name: String,
    location: String
}

impl Foo {
    fn new(name: &str, location: &str) -> Foo {
        Foo{
            name: name.to_string(),
            location: location.to_string()
        }
    }
}

impl Show for Foo {
    fn show(&self) -> String {
        self.name.clone()
    }
}

impl Location for Foo {
    fn location(&self) -> String {
        self.location.clone()
    }
}

impl ShowTell for Foo {}
#}

现在,如果我有Foo类型的foo值,那么对该值的引用将会满足&Show&Location或是&ShowTell (这暗示着两者) 三个。

这是一个有用的小宏:


# #![allow(unused_variables)]
#fn main() {
macro_rules! dbg {
    ($x:expr) => {
        println!("{} = {:?}",stringify!($x),$x);
    }
}
#}

它需要一个参数 (用$x表示) 必须是一个’表达式(expression)’。 我们打印出它的值,和一个 字符串化 的版本。 C 程序员会在这一点上有些 得意,这意味着如果我传递了1 + 2 (一个表达式) stringify!(1 + 2)是字面字符串”1 + 2”。 这会为我们在玩代码时节省一些打字的时间:


# #![allow(unused_variables)]
#fn main() {
let foo = Foo::new("Pete","bathroom");
dbg!(foo.show());
dbg!(foo.location());

let st: &ShowTell = &foo;

dbg!(st.show());
dbg!(st.location());

fn show_it_all(r: &ShowTell) {
    dbg!(r.show());
    dbg!(r.location());
}

let boo = Foo::new("Alice","cupboard");
show_it_all(&boo);

fn show(s: &Show) {
    dbg!(s.show());
}

show(&boo); // `Show`引用传递给`show`

// foo.show() = "Pete"
// foo.location() = "bathroom"
// st.show() = "Pete"
// st.location() = "bathroom"
// r.show() = "Alice"
// r.location() = "cupboard"
// s.show() = "Alice"
#}

这些就 面向对象,但不是你习惯的那种。

请注意,Show引用传递给show,它不可能是 动态 升级为ShowTell! 占据更多动态类系统范畴的语言,允许您检查给定对象是否是类的实例,然后对该类型执行动态转换。 一般来说这不是一个好主意,特别是不能在 Rust 中工作,因为Show引用已经”忘记”它最初是一个ShowTell引用。

你总有选择: 多态,通过 trait 对象,或是单态,通过泛型约束的 trait 。 现代 C ++和 Rust 标准库倾向于采用泛型路由,但多态路由并未过时。 您必须了解’路’的不同 - 泛型生成最快的代码,且可以内联。 这可能会导致代码膨胀。 但并非所有事情都要 尽可能快- 有时某个程序运行的生命周期中,可能只发生”那么”几次。

最后,这里有一个总结:

  • class所扮演的角色在数据和 traits 之间共享。
  • 结构和枚举是笨的,虽然你可以定义方法和做数据隐藏。
  • 使用Deref trait ,可以对数据进行一个子类型化的 限制 形式。
  • trait 没有任何数据,但可以实现任何类型 (不仅仅是结构)。
  • trait 可以从其他 trait 继承。
  • trait 可以提供方法,允许接口代码重用。
  • trait 给你两个虚构方法 (多态) 和泛型约束 (单态)。

示例: Windows API

GUI 工具包是广泛使用传统 OOP 的领域之一。 一个EditControl或者一个ListWindow是一个Window等等。这使得编写 Rust 绑定到 GUI 工具包,比使用它更困难。

Win32 编程在 Rust 中可以直接完成,它比原来的 C 稍微笨拙一点。 当我从 C 到 C ++ 毕业时,我想要更干净的东西,并且做了我自己的 OOP 包装。

一个典型的 Win32 API 函数是ShowWindow,用于控制窗口的可见性。 现在,一个EditControl有一些专门的功能,但它都是用 Win32 HWND (’handle to window’) 不透明的值完成的。若你想要EditControl也有一个show方法,传统上这将通过实现继承来完成,但您 想要为每种类型输出所有这些继承的方法! 而 Rust trait 提供了一个解决方案,这会有一个Window trait :


# #![allow(unused_variables)]
#fn main() {
trait Window {
    // 你需要定义这个!
    fn get_hwnd(&self) -> HWND;

    // 所有这些都将提供
    fn show(&self, visible: bool) {
        unsafe {
         user32_sys::ShowWindow(self.get_hwnd(), if visible {1} else {0})
        }
    }

    // .....在 Windows 上运行的大量方法

}
#}

所以,EditControl的实现结构只能包含一个HWND,并通过定义一种方法实现Window; EditControl是一种继承自Window的 trait ,并定义了扩展接口。比如像ComboxBox这样的 - 其行为像一个EditControl 可以通过 trait 继承轻松实现 一个ListWindow

Win32 API (’32’不再意味着’32 位’) 实际上是面向对象的,但是老一辈,受 Alan Kay 定义的影响: 对象包含隐藏的数据,并且由 消息{messages} 控制。因此,任何 Windows 应用程序的核心都有一个消息循环,各种窗口 (称为’窗口类’) 都用它们自己的 switch 语句实现这些方法。 其中有一个消息,可能有不同的实现,叫WM_SETTEXT: 标签的文本更改,顶级窗口的标题会变化。

这里是一个相当有前途的最小 Windows GUI 框架。 但根据我的口味,有太多了unwrap实例 - 其中一些甚至没有错误。

这是因为 NWG 正在利用消息的松散动态性质。通过适当的类型安全接口,编译时会捕获更多的错误。

下一版的 Rust 编程语言手册中, Rust 对面向对象的含义进行了很好的讨论。

目录

用 nom 解析文本

nom (文档在这里) 是 Rust 的解析器库,它非常值得新手投资。

如果你必须解析一个已知的数据格式,比如 CSV 或者 JSON,那么最好使用一个专门库,像Rust CSV或者第 4 节讨论的 JSON 库。

同样,对配置文件使用专用的解析器,比如ini要么toml。 (后一个特别酷,因为它与 Serde 框架结合在一起,就像我们看到的serde_json

但是如果文本不规则,或者某种格式,那么你需要扫描那些文本,但不是通过写很多乏味的字符串处理代码。 常建议去看看正则表达式,但认识久后,会感到沮丧,因正则表达式可能并不透明。 nom 提供了一种解析文本的方法。足够强大,大体上讲就是,组合更简单的解析器。 正则表达式有其局限性,例如,使用正则表达式来解析 HTML,不怎么行吧,但是你 可以 使用 nom 解析 HTML。 如果你有兴趣编写自己的编程语言,nom 是一个很好的借鉴,作为领略这条艰难的道路的开端。

有一些用于学习 nom 的优秀教程,但我想从 hello-world 级开始建立一些初步的熟悉感。 您需要了解的基本知识 - 首先,nom 一直是宏,其次,nom 倾向于使用切片 ,而不是字符串。 第一要点,是你必须特别小心才能获得 nom 表达式,因为错误信息不会很友善。 第二要点是 nom 可以用于 任何 数据格式,而不仅仅是文本。 人们使用 nom 解码二进制协议和文件头。它也可以在 UTF-8 以外的编码中,与”文本”合作。

nom 的最新版本与字符串切片工作良好,不过,您需要使用以_s结尾的宏.

#[macro_use]
extern crate nom;

named!(get_greeting<&str,&str>,
    tag_s!("hi")
);

fn main() {
    let res = get_greeting("hi there");
    println!("{:?}",res);
}
// Done(" there", "hi")

named!宏会创建函数,函数需要一些输入类型(&[u8]默认) 并将第二个类型返回到尖括号中。 tag_s!匹配字符流中的一个字面字符串,其值是表示该文字的字符串切片。(如果你想与&[u8]合作,那用tag!宏。)

我们用一个&str,调用定义的get_greeting解析器,并返回一个IResult。实际上,我们得到了匹配的值。

看看” there” - 这是匹配后剩下的字符串切片。

我们想忽略空格。tag_s!包进ws!,我们就可以在空格,制表符或换行符的任何位置匹配”hi”:

named!(get_greeting<&str,&str>,
    ws!(tag_s!("hi"))
);

fn main() {
    let res = get_greeting("hi there");
    println!("{:?}",res);
}
// Done("there", "hi")

结果就像之前的”hi”,只不过剩下的字符串是”there”!,空格已被跳过。

很好地匹配了”hi”,尽管这还不是很有用。

让我们匹配”hi”“bye”。 alt!宏 (”备选项”) 采用|符号分割解析器表达式,这样就可以匹配其中的 任何 。 请注意,您可以在这里使用空格来使解析器函数更易于阅读:


# #![allow(unused_variables)]
#fn main() {
named!(get_greeting<&str>,
    ws!(alt!(tag_s!("hi") | tag_s!("bye")))
);
println!("{:?}", get_greeting(" hi "));
println!("{:?}", get_greeting(" bye "));
println!("{:?}", get_greeting("  hola "));
// Done("", "hi")
// Done("", "bye")
// Error(Alt)
#}

最后一场匹配失败了,因为没有其他备选项能匹配”hola”.

显然,我们需要了解IResult类型多一点,但首先让我们比较这与正则表达式的解决方案:


# #![allow(unused_variables)]
#fn main() {
    let greetings = Regex::new(r"\s*(hi|bye)\s*").expect("bad regex"); // (hi|bye) 两种可能性
    let caps = greetings.captures(" hi ").expect("match failed");
    println!("{:?}",caps);
// Captures({0: Some(" hi "), 1: Some("hi")})
#}

正则表达式肯定更 紧凑{compact}! 我们需要将()放在,由|分隔的两种可能性中, 所以我们让 greeting 捕获(captures) 些什么或者没有。第一个结果是整个字符串,第二个是匹配的捕获。 (’|’是正则表达式中所谓的’备选’操作符,这是alt!宏语法的动机名。)

但,这是一个非常简单的正则表达式,它们很快就会变得复杂。因作为一种文本微语言,你必须转义重要的字符如*(。 如果我想匹配”(hi)”或”(bye)”,则正则表达式变为 \s*\((hi|bye)\)\s*,但是 nom 解析器只是变成了alt!(tag_s!("(hi)") | tag_s!("(bye)"))

这也是一个重量级的依赖。在这款相当微弱的 i5 笔记本电脑上,nom 的例子大约需要 0.55 秒的时间编译完成,这并不比”Hello world”慢多少。 但正则表达式的例子大约需要 0.90s。 该 Nom 示例的剥离版本生成的可执行文件约为 0.3Mb (与静态链接的 Rust 程序大约一样),而正则表达式为 0.8Mb。

一个 Nom 解析器返回什么

IResult与标准Result类型有一个有趣的区别 - 有三种可能性:

  • Done- 成功 - 您将得到结果和剩余的字节
  • Error- 未能解析 - 你得到一个错误
  • 不完全{Imcomplete}- 需要更多数据

我们可以写一个dump泛型函数,处理可以调试打印的任何返回值。 也说明下to_result方法会返回一个常规Result- 这可能是大多数情况下,用到的方法,因它不是返回值,就是返回一个错误。

#[macro_use]
extern crate nom;
use nom::IResult;
use std::str::from_utf8;
use std::fmt::Debug;

fn dump<T: Debug>(res: IResult<&str,T>) {
    match res {
      IResult::Done(rest, value) => {println!("Done {:?} {:?}",rest,value)},
      IResult::Error(err) => {println!("Err {:?}",err)},
      IResult::Incomplete(needed) => {println!("Needed {:?}",needed)}
    }
}


fn main() {
    named!(get_greeting<&str,&str>,
        ws!(
            alt!( tag_s!("hi") | tag_s!("bye"))
        )
    );

    dump(get_greeting(" hi "));
    dump(get_greeting(" bye hi"));
    dump(get_greeting("  hola "));

    println!("result {:?}", get_greeting(" bye  ").to_result());
}
// Done Ok("") "hi"
// Done Ok("hi") "bye"
// Err Alt
// result Ok("bye")

解析器返回任何未解析的文本,并且能够表明没有足够的输入字符,对 流{stream} 解析非常有用。常见情况下,to_result会是你的朋友。

合并解析器

让我们继续 greeting(问候) 示例,并设想问候语包含”hi”或”bye”,再加上一个名字。Nom::alpha匹配一系列字母字符。pair!宏将收集两个匹配解析器的结果,作成一个元组:


# #![allow(unused_variables)]
#fn main() {
    named!(full_greeting<&str,(&str,&str)>,
        pair!(
            get_greeting,
            nom::alpha
        )
    );

    println!("result {:?}", full_greeting(" hi Bob  ").to_result());
// result Ok(("hi", "Bob"))
#}

现在,进一步想象,这个 greeter 有点害羞或不知道其他人的名字: 让我们把名字变成可选。 自然而然,元组的第二个值变成了Option


# #![allow(unused_variables)]
#fn main() {
    named!(full_greeting<&str, (&str,Option<&str>)>,
        pair!(
            get_greeting,
            opt!(nom::alpha)
        )
    );

    println!("result {:?}", full_greeting(" hi Bob  ").to_result());
    println!("result {:?}", full_greeting(" bye ?").to_result());
// result Ok(("hi", Some("Bob")))
// result Ok(("bye", None))
#}

留意下,将现有的问候语解析器与,一个名称挑选的解析器合并起来很简单,结果就是名字是可选的。 见识到 nom 的强大力量了吧,这也是它被称为”解析器组合库”的原因。 您可以从更简单的解析器构建复杂的解析器,您可以单独测试它们。 (在这一点上,等价的正则表达式开始看起来像一个 Perl 程序: 正则表达式不能很好地合并。 )

但是,我们还不能葛优瘫! full_greeting(" bye")可能会有Imcomplete错误。 Nom 知道”bye”后面可能会有一个名字,并希望我们给它更多的数据。 解析器工作的时候到了,你可以把文件按块喂它吃,但是在这里我们需要告诉 nom 输入已完成。


# #![allow(unused_variables)]
#fn main() {
    named!(full_greeting<&str,(&str,Option<&str>)>,
        pair!(
            get_greeting,
            opt!(complete!(nom::alpha)) // 已完成
        )
    );

    println!("result {:?}", full_greeting(" bye ").to_result());
// result Ok(("bye", None))
#}

解析数字

nom 提供了一个digit函数,它与一系列数字相匹配。 所以我们使用map!,将字符串转换为整数,并返回完整的Result类型。


# #![allow(unused_variables)]
#fn main() {
use nom::digit;
use std::str::FromStr;
use std::num::ParseIntError;

named!(int8 <&str, Result<i8,ParseIntError>>,
    map!(digit, FromStr::from_str)
);

named!(int32 <&str, Result<i32,ParseIntError>>,
    map!(digit, FromStr::from_str)
);

println!("{:?}", int8("120"));
println!("{:?}", int8("1200"));
println!("{:?}", int8("x120"));
println!("{:?}", int32("1202"));

// Done("", Ok(120))
// Done("", Err(ParseIntError { kind: Overflow }))
// Error(Digit)
// Done("", Ok(1202))
#}

所以我们得到的是,一个解析器的IResult,包含转换而来的Result- 当然,在这里失败的原因不止一种。 请注意,我们的转换函数的主体具有完全相同的代码; 而实际转换取决于函数的返回类型。

整数可能有标志。 我们可以将整数作为一对来捕获,其中的第一个值可能是一个符号,第二个值可能是后面的任何数字。

考虑:


# #![allow(unused_variables)]
#fn main() {
named!(signed_digits<&str, (Option<&str>,&str)>,
    pair!(
        opt!(alt!(tag_s!("+") | tag_s!("-"))),  // 会是一个标志吗?
        digit
    )
);

println!("signed {:?}", signed_digits("4"));
println!("signed {:?}", signed_digits("+12"));
// signed Done("", (None, "4"))
// signed Done("", (Some("+"), "12"))
#}

当我们对中间结果不感兴趣时,只需要所有的匹配输入,那recognize!是你需要的。


# #![allow(unused_variables)]
#fn main() {
named!(maybe_signed_digits<&str,&str>,
    recognize!(signed_digits)
);

println!("signed {:?}", maybe_signed_digits("+12"));
// signed Done("", "+12")
#}

使用这种技术,我们可以识别浮点数。 同样,我们在所有这些匹配项上,将字节切片映射到字符串切片。 tuple!是泛型化的pair!,尽管我们对这里生成的元组不感兴趣。 complete!是需要解决不完整的问候时的相同问题 - “12”是没有可选浮点数部分的有效数字。


# #![allow(unused_variables)]
#fn main() {
named!(floating_point<&str,&str>,
    recognize!(
        tuple!(
            maybe_signed_digits,
            opt!(complete!(pair!(
                tag_s!("."),
                digit
            ))),
            opt!(complete!(pair!(
                alt!(tag_s!("e") | tag_s!("E")),
                maybe_signed_digits
            )))
        )
    )
);
#}

通过定义一个宏小助手,搞一些测试。 如果floating_point匹配它给出的所有字符串,那测试通过。


# #![allow(unused_variables)]
#fn main() {
macro_rules! nom_eq {
    ($p:expr,$e:expr) => (
        assert_eq!($p($e).to_result().unwrap(), $e)
    )
}

nom_eq!(floating_point, "+2343");
nom_eq!(floating_point, "-2343");
nom_eq!(floating_point, "2343");
nom_eq!(floating_point, "2343.23");
nom_eq!(floating_point, "2e20");
nom_eq!(floating_point, "2.0e-6");
#}

(虽然有时候,感觉宏有点 肮脏,但让你的测试漂亮是件好事。)

然后我们可以解析和转换浮点数。在这里,是不顾一切错误的乌托邦式例子:


# #![allow(unused_variables)]
#fn main() {
    named!(float64<f64>,
        map_res!(floating_point, FromStr::from_str)
    );
#}

注意的是,要逐步构建复杂的解析器,那么首先,单独测试每个部分。这是解析器组合器相较于正则表达式的强大优势。这是分而治之的经典编程策略。

多个匹配进行操作

我们与pairs!tuple!见过面了,它将固定数量的匹配捕获项,作成 Rust 元组。

这有many0many1- 他们都捕获无限数量的匹配项,作成 Vec。 不同的是,前一个可能会捕获”零或多个”,后一个则是”一个或多个” (如正则表达式*+之间的差异) 所以many1!(ws!(float64))会解析”1 2 3”到vec![1.0,2.0,3.0],但会在空字符串上会失败。

fold_many0是一个 递算 操作。使用二元运算符将匹配值组合为单个值。例如,这就是 Rust 开发者 以前如何对迭代器进行求和sum加入; 这个fold从一个初始值 (这里是零) 开始,启动 累加器(acc) ,并+迭代器的迭代值v,并返回给acc,继续。

流程就像acc = 0 + v0(第一次),acc = v0 + v1(第二次)...


# #![allow(unused_variables)]
#fn main() {
    let res = [1,2,3].iter().fold(0,|acc,v| acc + v);
    println!("{}",res);
    // 6
#}

以下是 nom 等价物:


# #![allow(unused_variables)]
#fn main() {
    named!(fold_sum<&str,f64>,
        fold_many1!(
            ws!(float64),
            0.0,
            |acc, v| acc + v
        )
    );

    println!("fold {}", fold_sum("1 2 3").to_result().unwrap());
    //fold 6
#}

到目前为止,我们必须捕获每个表达式,或者只是用recognize!抓住所有匹配的字节:


# #![allow(unused_variables)]
#fn main() {
    named!(pointf<(f64,&[u8],f64)>,
        tuple!(
            float64,
            tag_s!(","),
            float64
        )
    );

    println!("got {:?}", nom_res!(pointf,"20,52.2").unwrap());
 //got (20, ",", 52.2)
#}

对于更复杂的表达式,捕获所有解析器的结果,会导致相当不整洁的类型!我们可以做得更好.

do_parse!让你只提取你感兴趣的值- 感兴趣的匹配项用>>分割: 格式是 name:parser}。最后,括号里有一个代码块。


# #![allow(unused_variables)]
#fn main() {
    #[derive(Debug)]
    struct Point {
        x: f64,
        y: f64
    }

    named!(pointf<Point>,
        do_parse!(
            first: float64 >>
            tag_s!(",") >>
            second: float64
            >>
            (Point{x: first, y: second}) // first  second 是 临时值
        )
    );

    println!("got {:?}", nom_res!(pointf,"20,52.2").unwrap());
// got Point { x: 20, y: 52.2 }
#}

对标签值 (就是那个逗号)不感兴趣,但我们将两个浮点值分配给用于构建结构的临时值。最后的代码可以是任何 Rust 表达式,随你。

解析算术表达式

随着必要的知识建立,我们可以做简单的算术表达式。

这是用正则表达式,无法真正完成的一个很好的例子。

这个想法是从下往上建立表达式。表达式由 terms 组成,如加或减。 terms 由 factors 组成,它们相乘或除。 和(现在)factor 只是浮点数:


# #![allow(unused_variables)]
#fn main() {
    named!(factor<f64>,
        ws!(float64)
    );

    named!(term<&str,f64>, do_parse!(
        init: factor >>
        res: fold_many0!(
            tuple!(
                alt!(tag_s!("*") | tag_s!("/")),
                factor
            ),
            init,
            |acc, v:(_,f64)| {
                if v.0 == "*" {acc * v.1} else {acc / v.1}
            }
        )
        >> (res)
    ));

    named!(expr<&str,f64>, do_parse!(
        init: term >>
        res: fold_many0!(
            tuple!(
                alt!(tag_s!("+") | tag_s!("-")),
                term
            ),
            init,
            |acc, v:(_,f64)| {
                if v.0 == "+" {acc + v.1} else {acc - v.1}
            }
        )
        >> (res)
    ));
#}

这更准确地表达了我们的定义 - 一个表达式至少包含一个 term,和零或多个加减项。

我们不collect它们,而用适当的 fold 操作。 (这是 Rust 不能很好地处理表达式类型的情况之一,所以我们需要一个类型提示。) 这样做会建立正确的 运算符优先级 -*总是比+优先等等。我们在这里需要浮点数断言,而正好有一个箱.

approx ="0.1.1",添加到您的 Cargo.toml 中,就可以开始了:


# #![allow(unused_variables)]
#fn main() {
#[macro_use]
extern crate approx;
...
    assert_relative_eq!(fold_sum("1 2 3").to_result().unwrap(), 6.0);
#}

我们来定义一个方便的小宏,来测试。stringify!将表达式转换为,我们可以输入expr的字符串字面量, 然后将测试得到的值,与 Rust 的执行结果进行比较。


# #![allow(unused_variables)]
#fn main() {
    macro_rules! expr_eq {
        ($e:expr) => (assert_relative_eq!(
            expr(stringify!($e).to_result().unwrap(),
            $e)
        )
    }

    expr_eq!(2.3);
    expr_eq!(2.0 + 3.0 - 4.0);
    expr_eq!(2.0*3.0 - 4.0);
#}

这非常酷 - 只需几行即可获得 表达式评估器! 但它能变得更好。 我们在因素{factor}解析器,增加了一个数字的替代方案 - 能包含在括号内的表达式:


# #![allow(unused_variables)]
#fn main() {
    named!(factor<&str,f64>,
        alt!(
            ws!(float64) |
            ws!(delimited!( tag_s!("("), expr, tag_s!(")") ))
        )
    );

    expr_eq!(2.2*(1.1 + 4.5)/3.4);
    expr_eq!((1.0 + 2.0)*(3.0 + 4.0*(5.0 + 6.0)));
#}

最厉害的是现在,能 递归 定义这堆表达式!

delimited!的特别魔力,在于括号可以嵌套 - nom 确保括号匹配。

我们现在已经拥有,超越正则表达式的能力了,0.5Mb 的剥离版可执行文件仍然是”hello world”正则表达式程序的一半大小。

目录

痛点

可以说 Rust 是一门比大多数”主流”语言更难学的语言。 有特别的人不觉得这么难,但要注意’特别’的真正含义 - 他们是 例外的。 许多人先要挣扎一番,后才成功。 最初的艰难是不能预测你的未来!

我们来自世界各地,处于各种编程语言的情况下,这意味着,存在以前主流语言的遗留思维,如 Python 之类的”动态”语言 或 C ++之类的”静态”语言,或其他的。 但无论你过去的思维方式是怎么样的, Rust 都有很大的不同,需要转变思路。有经验的聪明人加入 Rust 学习,觉得说,以他们的聪明才智,却不能立即获得回报,他们会感到失望; 自我认识较低的人则认为自己不够”聪明”。

对于那些具有动态语言经验的人 (包括 Java,我想),所有的一切都是一个引用&,并且所有引用默认都是可变的。还有垃圾收集功能 确实 让编写内存安全的程序更容易。 而以内存成本和可预测性为代价,JVM 进展非常迅速。 通常这种成本被认为是值得的 - 传统的新想法认为程序员的生产力 比 计算机的性能更重要。

但,世界上大多数电脑 - 如处理汽车阀门控制等之类的真正重要事情 - 并不具备大量资源,甚至连一个便宜笔记本电脑都比不上,和他们需要的是 实时 响应。同样,基础软件基础架构需要正确,稳健和快速 (旧工程的三体)。 而这大部分,都是本质上不安全的 C 和 C ++ 完成的,这个不安全的 总成本 应该是我们所要正视的。也许你组合项目起来,飞快,但 在这之后 真正的开发才刚刚开始。

系统语言无法承担垃圾回收,因为它们是其他所有东西依赖的基础。只要你认为合适,他们就让你自由地浪费资源。

但如果没有垃圾回收,内存就必须以其他方式进行管理。 手动内存管理 - 我抓住内存,使用它,并明确地将其退回 - 很难做对。 您可以在几周内学会够用的 C 语言,以提高工作效率,而危险性也随之而来 - 要成为一名安全的 C 语言程序员需要花费数年时间,并检查每种可能的错误情况。

Rust 像现代 C ++ 一样管理内存 - 随着对象被破坏,其内存被回收。 你可以在堆上分配内存Box,但只要在函数结束,Box’超出范围’时,弃内存就会被回收。所以 Rust 有像new这样的事情,但没有删除{delete}。 你可以创建一个File和在最后,文件 (一个宝贵的资源)会自动被关闭。 在 Rust 中,这被称为 扔掉{dropping}

你需要共享资源 - 复制一切都是非常低效的 - 这就是事情变得有趣的地方。 C ++也有引用,尽管 Rust 引用更像 C 指针 - 你需要使用*r才能用引用指向的值 {value},你需要加上&,一个值才作为引用类型传递。

Rust 的 借用检查器 确保在原始值被销毁后,引用不可能存在。

类型推断

“静态”和”动态”之间的区别不是一切。 与大多数事情一样,还有很多可以发挥的区域。 C 是静态类型的 (每个变量在编译时,都有一个类型),但弱类型 (例如,void*可以指向 任何{anything}); Python 是动态类型的 (类型与值相关,而不是变量),但却是强类型的。 Java 是静态/非常强类型的 (有反射「reflection」功能,这就像方便,但危险的阀门), Rust 是静态/强类型的,运行时没有反射。

Java 因需要在麻木细节中, 键入 所有的类型而出名, Rust 更喜欢 推断 类型。 这通常是一个好主意,但这也确实意味着,有时你需要计算出实际类型。 当会看见let n = 100,并想知道 - 这是什么样的整数? 默认情况下,它会是i32- 一个四字节有符号整数。 现在大家都同意 C 的未指定整数类型 (比如intlong) 是一个坏主意; 最好明确类型。 你可以随时拼写出类型,如let n: u32 = 100,或者强制类型,如let n = 100u32。 但是类型推断比这要强! 如果你声明let n = 100,然后rustc全部知道,n一定是 一些 整数类型。 如果之后,你想把n传递到一个函数,其期望一个u64类型,那么这一定是这种类型的n!

之后,你尝试给n到期望u32函数,rustc不会让你这样做,因为n已被束缚到u64,和它 将不会 采取简单,且隐式的方法为您转换该整数。这是来自类型的强力攻势 - 没有任何一点转换,和‘促销活动’让你的生活更流畅,这样做的同时,整数不会溢出突然咬住你的屁股。你必须明确地表明n转换,如n as u32- 一个 Rust 类型。 幸好,rustc善于以”可行”的方式打败坏家伙 - 也就是说,您可以按照编译器的意见来解决问题.

所以, Rust 代码可以非常明确的类型:

let mut v = Vec::new();
// v 被推断为  Vec<i32>类型
v.push(10);
v.push(20);
v.push("hello") <--- 不能这样做,盆友!

不能将字符串放入一个整数 Vec 是一个功能,而不是一个错误。 动态类型的灵活性也是一个诅咒。

(如果你将需要 整数和字符串放入同一个 Vec,那么 Rust 枚举类型是安全地使用它的方法。 )

有时,你需要至少给一个类型 暗示. collect是一个梦幻般的迭代器方法,但它需要一个提示。比如说,我有一个迭代器,它返回char,然后collect可有两种方式:

// 一个  char vec ['h','e','l','l','o']
let v: Vec<_> = "hello".chars().collect();
// 一个 "doy" 字符串
let m: String = "dolly".chars().filter(|&c| c != 'l').collect();

当对某个变量的类型感到不确定时,总会有能,强行让rustc在错误消息中显示实际类型名称的技巧:

let x: () = var;

rustc可能会选择十分特定类型。这里我们想把不同的引用放入一个 Vec,但需要使用&Debug明确声明类型。

use std::fmt::Debug;

let answer = 42;
let message = "hello";
let float = 2.7212;

let display: Vec<&Debug> = vec![&message, &answer, &float];

for d in display {
    println!("got {:?}", d);
}

可变引用

规则是: 一次只有一个可变引用。 原因在于,当 到处都是 都是可变性引用,那跟踪他们就很难。在笨蛋小程序中不明显,但在大型代码库中可能会变得糟糕。

进一步的限制是,当已有一个可变引用时,你不能再拥有不可变引用, 否则,任何有这些引用的人都不能保证他们不会改变。 C ++也有不可变的引用 (例如const string&) ,但是 不能 给你这个保证,因为有人可能在你背后,保留一个string&引用并修改它。

如果您习惯每个引用都是可变的语言,那这会是一个挑战! 不安全的”放松”语言,取决于人们了解他们自己的计划,并秉直地决定不做坏事。 但是大型项目是由不止一个人编写的,并且超出了个人,详细理解的能力。

更气人 事情是,借用检查器并不像它所描述的那样聪明。

let mut m = HashMap::new();
m.insert("one", 1);
m.insert("two", 2);

if let Some(r) = m.get_mut("one") { // <-- m 的可变引用
    *r = 10;
} else {
    m.insert("one", 1); // 不能再次可变借用!
}

显然这不是 真的 违反规则,除非如果我们得到了None,而实际上并没有从 map 上借用任何东西。

有各种丑陋的解决方法:

let mut found = false;
if let Some(r) = m.get_mut("one") {
    *r = 10;
    found = true;
}
if ! found {
    m.insert("one", 1);
}

这很糟糕,但它起作用,因为烦人的借用保留在第一个 if 语句中。

这里更好的方法是使用HashMapentry API.

use std::collections::hash_map::Entry;

match m.entry("one") {
    Entry::Occupied(e) => {
        *e.into_mut() = 10;
    },
    Entry::Vacant(e) => {
        e.insert(1);
    }
};

非词汇生命周期 在今年(2018)某个时候到达, 借用检查器获得更少的挫败感。

借用检查器 还是 了解一些重要的案例,然而。 如果你有一个结构,字段可以独立借用。所以组合构造是你的朋友; 一个大结构体应该包含更小的结构体,它们有自己的方法。 定义大结构体上的所有可变方法,会导致你不能修改内容的情况,即使这些方法可能只涉及一个字段。

对于可变数据,有一些独立处理部分数据的特别方法。例如,如果你有一个可变切片,那么split_at_mut将它分成两个可变切片。 这是完全安全的,因为 Rust 知道切片不重叠。

引用和生命周期

Rust 不能允许一个引用长命过值的情况。 否则,我们会有一个”悬挂引用”,它指的是一个已死亡的值 - 一个错误是不可避免的。

rustc往往可以对函数的生命周期做出合理的假设:

fn pair(s: &str, ch: char) -> (&str, &str) {
    if let Some(idx) = s.find(ch) {
        (&s[0..idx], &s[idx+1..])
    } else {
        (s, "")
    }
}
fn main() {
    let p = pair("hello:dolly", ':');
    println!("{:?}", p);
}
// ("hello", "dolly")

这是非常安全的,因为我们处理好了未找到分隔符的情况。 rustc在这里假定元组中的两个字符串都是,从作为一个传递给函数参数的字符串中借用的。

明确地说,函数定义如下所示:

fn pair<'a>(s: &'a str, ch: char) -> (&'a str, &'a str) {...}

'a符号表示输出字符串活得 至少与输入字符串一样长 。这并不是说一样的生命周期,我们可以在任何时候放弃它们(引用),只是它们无法离开s

所以,rustc生命周期免写,使常见案例更漂亮。

现在,如果该函数收到 两个 字符串,那么您需要明确地进行生命周期注释,来告诉 Rust 哪个输出字符串是从哪个输入字符串中借用的。

当一个结构借用一个引用时,你总是需要一个明确的生命周期:

struct Container<'a> {
    s: &'a str
}

这再次坚称,结构不能长命过引用。 对于结构和函数,生命周期都需要在<>中声明,当作一个类型参数。

闭包是非常方便和强大的功能 - Rust 迭代器的很多强大之处都来自它们。但是如果你存储它们,你必须指定一个生命周期。 这是因为闭包基本上是一个可以调用,已生成的结构,并且默认情况下,是借用它的环境。这里的linear闭包有不可变的引用mc

let m = 2.0;
let c = 0.5;

let linear = |x| m*x + c; // 借用/引用
let sc = |x| m*x.cos()
...

linearsc都为Fn(x: f64)->f64类型,但他们是 不是 同类 - 他们有不同的类型和大小! 所以要存储它们,你必须做出一个Box<Fn(x: f64)->f64 + 'a>

非常烦人,如果你习惯了 JavaScript 或 Lua 的流畅闭包,但 C ++与 Rust 类似,同样需要std::function存储不同的闭包,给虚拟调用一点点惩罚。

字符串

在开始时,经常会对 Rust 字符串感到恼火。 有不同的方式来创建它们。但感觉它们都冗长:

let s1 = "hello".to_string();
let s2 = String::from("dolly");

“hello” 不是 已经是 一个字符串? 好吧,在某种程度上。 String是一个具有 所有权 字符串,分配在堆上; 字符串字面量”hello”是&str类型的 (”字符串切片”) ,并可能被烘焙到可执行文件 (”静态”) 或从一个String借用而来的。 系统语言需要这种区别 - 考虑一个微型微控制器,它有 一点 RAM 和更多的 ROM 。 字面字符串将被存储在 ROM (”只读”) 中 ,这既便宜又更少功耗。

但是 (你可能会说) 在 C ++中,它非常简单啊:

std::string s = "hello";

是短,但字符串对象真正的创建,被隐藏起来了。 因此, Rust 喜欢to_string明确分配内存。 另一方面,借用(引用)一个 C ++字符串需要c_str,而 C 字符串很蠢。

幸运的是, Rust 的情况更好 - 一旦 你接受String&str两者其实都是必要的。 String的方法主要是为了改变字符串,就像push添加一个字符 (在引擎盖下它非常像一个Vec<u8>)。 但是所有&str的方法也可用。得益于Deref同一机制,一个String可以作为&str类型传递给一个函数 - 这就是为什么你很少看到,在函数中定义&String

对应各种 trait,有很多方法可以把&str转换为String。 Rust 需要这些 trait 来处理泛型类型。 作为一个经验法则,任何实现Display,也知道to_string,像42.to_string()

一些操作,可能不会按照直觉行事:

    let s1 = "hello".to_string();
    let s2 = s1.clone();
    assert!(s1 == s2);  // cool
    assert!(s1 == "hello"); // fine
    assert!(s1 == &s2); // WTF?

记得,String&String是不同的类型,和没有为该组合定义==。这可能会让 C ++ 开发者,感到迷糊,因习惯于引用与数值几乎可以互换。 此外,&s2不会 神奇 成为一个&str, 一个 deref 强制 只在分配到 一个&str变量或参数时,才会发生。 (明确的s2.as_str()能工作。)

但是,这有更值得注意的一个 WTF:

let s3 = s1 + s2;  // <--- 不行

你不能连接两个String值,但可以使用&str连接一个String。 此外,您不能使用String连接一个&str。所以大多数人不会使用+,而是使用format!宏,这很方便,但效率不高。

有些字符串操作可用,但工作方式不同。 例如,编程语言通常有一个split方法,能将字符串分解为字符串数组的。Rust 字符串的这个方法,返回一个 迭代器 ,你可以 之后collect 成一个 Vec 。

let parts: Vec<_> = s.split(',').collect();

如果你急着获取 Vec,这有点笨拙。 但是你可以对这部分进行操作, 不用 分配一个 Vec! 例如,split 过程中,最大的字符串的长度?

let max = s.split(',').map(|s| s.len()).max().unwrap();

(使用unwrap是因为空迭代器没有最大值,我们必须覆盖这种错误情况。 )

collect方法返回一个Vec<&str>,其中&str部分是从原始字符串中借用的 - 我们只需要为引用分配内存空间。(这意味着小且固有大小。) 在 C ++中没有像这样的方法,但直到最近才需要单独分配每个子字符串。 (C ++ 17 有std::string_view,其行为像一个 Rust 字符串切片。 )

关于分号的说明

分号是 不是 可选项,但通常,与 C 相同的地方都被省略,例如,在{}代码块之后,他们也不需要enum要么struct (这是一个 C 特性。 )但是,如果该代码块必须有一个 ,那么分号将被丢弃:

    let msg = if ok {"ok"} else {"error"};

请注意,在let声明之后,必须有一个分号在!

如果在x * *字符串之后,加上分号,那么返回的值就是() (像Nothing要么void)。定义函数时常见错误:

fn sqr(x: f64) -> f64 {
    x * x; //  多了个 分号
}

rustc在这种情况下,会给你一个明确的错误。

专怼 C ++问题

Rust 值语义是不同的

在 C ++中,可以定义,类似原始的类型,并复制它们自己。 另外,可以定义移动构造函数,来指示如何将值移出临时上下文。

在 Rust 里,原始类型的行为和预期一样,但是Copytrait 只能在集合类型 (结构{struct},元组{tuple}或枚举{enum}) 本身,只包含可复制类型的情况下定义。 任意类型可能有Clone,但你必须使用clone方法。 Rust 要求任何分配都是明确的,不要隐藏在复制构造函数或赋值运算符中。

所以,复制和移动总是被定义为只是 移动位比特,而不是被覆盖。

如果s1不是Copy值类型,像s2 = s1;导致移动发生,而这 消耗 s1! 所以,当你真的想要一个副本,使用clone

借用通常比复制要好,但是你必须遵循借用规则。 幸运的是,借用 一个可覆盖的行为。 例如,String可以借用成&str,并共享&str的所有的不可变方法。 字符串切片 比 类似的 C ++”借用{borrowing}”操作,更加强大,C ++ 要运用c_str提取一个const char *&str由一个指针,指向一些具有所有权的字节 (或一个字符串字面量) 和 一个 尺寸「size」 组成。 这造就了一些非常有效的内存模式。 你可以有一个Vec<&str>,其中所有的字符串都是从一些底层字符串中借用的 - 你只需要分配该 Vec 内存空间:

例如,按空格拆分:

fn split_whitespace(s: &str) -> Vec<&str> {
    s.split_whitespace().collect()
}

同样,一个 C ++ 的 s.substr(0,2)调用将始终复制字符串,但切片只会借用: &s[0..2].

Vec<T>&[T]之间是一个雷同关系,就像字符串与字符串切片。

共享引用

Rust 有 智能指针,这像 C ++ - 举例,std::unique_ptr相当于是Box。但没必要delete(删除),因为任何内存或其他资源,在盒子超出作用域时都会被回收 (Rust 非常赞同 RAII)。

let mut answer = Box::new("hello".to_string());
*answer = "world".to_string();
answer.push('!');
println!("{} {}", answer, answer.len());

起初,人们发现to_string老烦啦,但,确实 明朗 许多。

注意显式的*取值符号,但智能指针有所不同,它的方法不需要任何特别符号 (我们不用这样写(*answer).push('!'))

显然,只有在原始内容的所有权(者),被明确定义的情况下,借用才有效。在许多设计中是不可能的。

C ++中,std::shared_ptr用处是; 仅复制,那修改公用数据的引用计数。然而,这并不是没有成本的:

  • 即使数据是只读的,不断修改引用计数也会导致缓存失效。
  • std::shared_ptr被设计成线程安全的,也带来了锁定开销。

在 Rust 中的std::rc::Rc,也像共享智能指针一样,它使用了引用计数。 但是,它仅适用于不可变引用! 如果你想要一个线程安全的变体,请使用std::sync::Arc (’原子(Atomic) Rc’)。 所以, Rust 在提供两种变体方面略显笨拙,但你可以避免非线程操作的锁定开销。

正如上所说,应用的都必须是不可变的引用,因为这是 Rust 内存模型的基础。 但是,有一张权限卡: std::cell::RefCell。 如果您有个共享引用定义为Rc<RefCell<T>>,那么你能用它的borrow_mut方法,获得可变借用。 这使 Rust 借用规则变得 动态 起来 - 除此之外像,已有了借用,任何尝试使用borrow_mut的操作,都会引起恐慌。

权限卡仍然是 安全。 恐慌会在任何内存被不当地触动 之前 发生! 异常情况下,他们展开调用栈。所以对这样一个结构化的回溯过程来说,恐慌是个不好的词 - 因其实这是一种有序的回溯,而不是 恐慌,无序的撤退。

完整的Rc<RefCell<T>>类型,看着不舒服,但应用程序代码并不会不爽。 Rust (再次) 更喜欢明确表示。

如果你想线程安全地访问共享状态,那么Arc<T>是唯一的 安全 道路。如果你需要可变权限,那么Arc<Mutex<T>>会帮到你,相当于Rc<RefCell<T>>。而Mutex与通常定义的方式有点不同: 它是一个值的容器。 在 值 上你得到一个 锁{lock},然后可以修改它。

let answer = Arc::new(Mutex::new(10));

// 在其他线程
..
{
  let mut answer_ref = answer.lock().unwrap();
  *answer_ref = 42;
}

为什么有个unwrap? 因为如果前一个线程恐慌了,那么这个就失败。(这是在文档中,unwrap被认为是合理的一个地方,因为显然,事情会走向严重错误,而这时,总要在线程中抓到(unwrap)恐慌。 )

重要的是 (像往常一样使用 Mutex 锁) 这个互斥锁尽可能少地持有。 所以,它们出没在一个有限,短的作用域内是很常见的 - 然后,当可变引用超出作用域时锁定结束。

与 C ++中,显然更简单的情况相比 (use shared*ptr dude),这 Rust 很不好看,但是,现在任何共享状态的 修改 都变得明显, 还有,“互斥锁{Mutex}”锁定模式会强制线程变得安全。

像所有内容一样,会有使用共享引用的警告

迭代器

C ++中的迭代器,定义的很不正式;他们涉及到智能指针,通用c.begin(),从头开始并以c.end()结束。迭代器上的操作,稍后实现为独立的模板函数,如std::find_if

Rust 的迭代器由Iterator trait 定义; next返回一个Option,和当OptionNone时,迭代结束了。

最常见的操作正如下所示的方法,这是find_if的等价方法。 它返回一个Option (若是没有发现,就是一个None) 和这里if let语句可以方便地提取非None状态:

let arr = [10, 2, 30, 5];
if let Some(res) = arr.find(|x| x == 2) {
    // res 是 2
}

不安全和链接列表

Rust stdlib 的某些部分实现使用了unsafe,这不是什么秘密。 这并不妨碍 借用检查员 的传统做法。 要记住的是,”unsafe”具有特别含义 - 表明 Rust 在编译时,无法完全验证的操作。 从 Rust 的角度来看,C ++始终处于不安全的模式! 所以如果一个大的应用程序需要几十行不安全的代码,那很好,因为这些行代码可以仔细检查(明确是 unsafe)。人类可不善于检查 100Kloc +的代码。

我提到这一点的原因,是因为似乎有一种行为模式: 一个有经验的 C ++人 试图实现 链表或树结构,不过沮丧收场。 那么,一个双链表 可能符合安全 Rust 的,秘密在于Rc引用前进,和Weak引用回退。 但是标准库仍能获得了更多的性能,若是不用...指针(But the standard library gets more performance out of using... pointers.)。