绅士地介绍 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
基础
目录
- 你好,世界!
- 循环和条件语句
- 开始堆积木吧
- 函数类型是明确的
- 学习在哪里找到绳子
- 数组和切片
- 切和割
- 可选(Option)值
- 向量
- 迭代器
- 更多关于向量
- 字符串
- 插曲: 获取命令行参数
- 匹配
- 读取文件
你好,世界!
自从第一个 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
(四字节有符号整数)。
让我们做一个改变0
到0.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常数可以是f32
或f64
类型,而这些都是非常不同的。)
让我引用一个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 项目一直抛开import
和exclude
这些,会使讨论”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
, 如果返回的值是None
的Option
类型。 - 类型要匹配,因get
返回一个引用。所以你必须写成&i32
与&-1
。最后再次使用*
获得i32
类型值。
# #![allow(unused_variables)] #fn main() { let last = *slice.get(5).unwrap_or(&-1); #}
很容易漏写&
,但你有编译器的帮助。 如果它是-1
,rustc
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
更智能。 实际上,&str
和String
有一个很好的的相似关系
就是&[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
一个字符,和pop
出String
结尾:
// 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 它
- 变量的范围
- 元组
- 结构{Structs}
- 生命周期{Lifetimes}开始咬人啦
- 特点{Traits}
- 示例: 遍历浮点范围的迭代器
- 泛型函数
- 简单的枚举
- 枚举的全部荣耀
- 关于匹配的 更多
- 闭包{Closures}
- 三种迭代器
- 具有动态数据的结构
- 泛型结构
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
它太酷了; 我们增加了i32
和f64
两者泛型的 一种新方法 !
熟悉 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
也自动可用。 所以,如果我们实现了Person
的Display
, p.to_string()
也可用了。
Clone
定义了clone
方法,可简单用”#[deriv(Clone)]”进行定义,如果要所有的字段都实现Clone
的话。
示例: 遍历浮点范围的迭代器
之前,我们已经遇到范围表达 (0..n
) ,但它们不适用于浮点值。 ( 强行 去做,最终你会得到一个无趣的 1.0。 )
回想一下,迭代器的非正式定义; 它是一个带有结构体,具有一个可能会返回Some
或None
的next
方法。 在这个过程中,迭代器本身被修改,它保持迭代的状态 (如 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
所 提供的方法 。 你只需要定义Item
和next
,那该表达语句就可为你所用。
// 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 * x
与x
类型没有道理是相同的,例如,两个向量的积是一个标量。
# #![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
枚举有Slow
,Medium
和Fast
的变种。
这些枚举 确 有一个自然的顺序,但你必须问得好。在enum Speed
前面放置#[derive(PartialEq,PartialOrd)]
之后,Speed::Fast > Speed::Slow
和Speed::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 #}
(而这就是Option
和Result
的本质 - 都是枚举。)
我们喜欢这个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
形式 - 因它不知道闭包范围内的变量。闭包函数是从其上下文 借用了 m
和c
。
现在,这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()); #}
子曰: apply
为T
这样的 任何 且具备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,如果Option
是Some
的话,并应用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{清除} 也只是缓存区; 一旦字符串有足够的容量,不会再有分配。
这是我们使用一个块{}
来控制单一借用的情况。line
是buf
的借用,而这个借用必须在我们修改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::BufWriter
像Read
和io::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); #}
PathBuf
与OsString
有亲密的关系,它代表我们直接从系统获得的字符串。(相应的,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_file
和is_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>
,记录到Output
的sudout
字段,就像之前的一样。
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
,并添加一个新模块 - 注意导出明确性。(若没有pub
,bar
只能看看在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 为特有的Serialize
和Deserialize
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
声明依赖于IntoIterator
trait,实际上有三种实现。
然后是索引,由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
的使用关系到字符串Pattern
trait 的怪癖。但我们可以推断出 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::Error
trait,这样 任何 错误都可以转换成一个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
转换自己) 和第二个?
将转换ParseFloatError
到MyError
。
结果如下:
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 : Display
,Debug
和Error
,也默认实现 From
, 所以字符串 可以转换成错误。
我们的src/main.rs
文件看起来像这样。所有的主要程序都是给run
调用,打印出错误,并用非零退出代码结束程序。 error_chain
宏,会在定义error
的模块里面生成所有所需的 - 在一个更大的程序中,你会把error
的模块放在它自己的文件中。 我们需要把放进error
的所有东西,带回到全局作用域,因为我们的代码需要生成的 traits。 默认情况下,随带有一个Error
结构和一个Result
的定义。
我们也要求From
的实现,这样使用foreign_links
,std::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为你创建一个类型Error
,std::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
变量 是不可变的!
这显然是非常安全的,因为单元格内的值只能通过set
和get
访问。 这正是盛大名称 内部可变性。 通常被称为 遗传可变性: 如果我有一个结构值v
,如果v
本身是可写的,那么我可以加个字段v.a
。 Cell
值放宽了这个规则,因为我们可以用set
改变其中包含的值,即使 cell 本身不可变。
然而,Cell
只适用于Copy
类型 (例如,派生了Copy
trait 的原始类型和用户类型)。
对于其他值,我们必须得到一个可以工作的引用,可变或不可变。这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")
由于线程在结束执行之前,会发送响应,显然这可能随时发生,因此这示例无需 join
。 recv
将阻塞,并且如果发送者通道断开连接,将返回一个错误。 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
是一个IntoIter
裹SocketAddr
,SocketAddr
是一个带有 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_connection
的read_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
的引用 。请注意 i32和
f64彼此之间是没有任何关系的,但他们都知道
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 接口一样,实现 必要 方法, 提供 这个方法的默认实现。 (该Iterator
trait 就是一个很好的例子。 )
但是,请注意 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,虽然Debug
和Display
trait 两者都定义了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 元组。
这有many0
和many1
- 他们都捕获无限数量的匹配项,作成 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 的未指定整数类型 (比如int
和long
) 是一个坏主意; 最好明确类型。 你可以随时拼写出类型,如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 语句中。
这里更好的方法是使用HashMap
的entry 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
闭包有不可变的引用m
和c
。
let m = 2.0;
let c = 0.5;
let linear = |x| m*x + c; // 借用/引用
let sc = |x| m*x.cos()
...
linear
和sc
都为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 里,原始类型的行为和预期一样,但是Copy
trait 只能在集合类型 (结构{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
,和当Option
是None
时,迭代结束了。
最常见的操作正如下所示的方法,这是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.)。