勘误
最后更新于:2022-04-01 00:45:19
> 这里收集了一些比较有争议的名词翻译,并在此统一说明。如果你在阅读时遇到无法理解地方,请提issue。
## 向量(vector)
鉴于将`vector`翻译为`向量`容易引起误解,故决定不再对其进行翻译,如果你在本书中看到“向量”一词,这一定是还未修改过来,请自行脑补为`vector`
## 片段(slice)
`slice`译为`切片`,而不是`片段`
## 特性(trait)
`trait`不做翻译
## 特性对象(trait object)
`trait object`译为`trait对象`
8.学院派研究
最后更新于:2022-04-01 00:45:17
一个曾影响过Rust的论文的不完整列表。
推荐进一步了解Rust背景和激发灵感阅读。
> **(注:以下翻译属个人理解,勿作为参考)**
## 类型系统
* [Cyclone语言中基于区域的内存管理(Region based memory management in Cyclone)](http://209.68.42.137/ucsd-pages/Courses/cse227.w03/handouts/cyclone-regions.pdf)
* [Cyclone语言中的手动安全内存管理(Safe manual memory management in Cyclone)](http://www.cs.umd.edu/projects/PL/cyclone/scp.pdf)
* [类型类:使临时多态不再临时(Typeclasses: making ad-hoc polymorphism less ad hoc)](http://www.ps.uni-sb.de/courses/typen-ws99/class.ps.gz)
* [宏综述(Macros that work together)](https://www.cs.utah.edu/plt/publications/jfp12-draft-fcdf.pdf)
* [特性:组合类型的行为(Traits: composable units of behavior)](http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf)
* [消除别名(Alias burying)](http://www.cs.uwm.edu/faculty/boyland/papers/unique-preprint.ps) - 我们尝试了一些相似的内容并放弃了它
* [外部唯一性是足够的(External uniqueness is unique enough)](http://www.computingscience.nl/research/techreps/repo/CS-2002/2002-048.pdf)
* [用于安全并行的唯一性和引用不可变性(Uniqueness and Reference Immutability for Safe Parallelism)](https://research.microsoft.com/pubs/170528/msr-tr-2012-79.pdf)
* [基于区域的内存管理(Region Based Memory Management)](http://www.cs.ucla.edu/~palsberg/tba/papers/tofte-talpin-iandc97.pdf)
## 并发
* [Singularity:软件栈的重新思考(Singularity: rethinking the software stack)](https://research.microsoft.com/pubs/69431/osr2007_rethinkingsoftwarestack.pdf)
* [Singularity操作系统中支持快速和可靠的消息传递的语言(Language support for fast and reliable message passing in singularity OS)](https://research.microsoft.com/pubs/67482/singsharp.pdf)
* [通过work stealing来安排多线程计算(Scheduling multithreaded computations by work stealing)](http://supertech.csail.mit.edu/papers/steal.pdf)
* [多道程序多处理器的线程调度(Thread scheduling for multiprogramming multiprocessors)](http://www.eecis.udel.edu/~cavazos/cisc879-spring2008/papers/arora98thread.pdf)
* [work stealing中的数据局部性(The data locality of work stealing)](http://www.aladdin.cs.cmu.edu/papers/pdfs/y2000/locality_spaa00.pdf)
* [动态环形work stealing双端队列(Dynamic circular work stealing deque)](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.170.1097&rep=rep1&type=pdf) - Chase/Lev双端队列
* [异步-完成并行的work优先和help优先的调度策略(Work-first and help-first scheduling policies for async-finish task parallelism)](http://www.cs.rice.edu/~yguo/pubs/PID824943.pdf) - 比严格的work stealing更宽泛
* [一个Java的fork/join灾难(A Java fork/join calamity)](http://www.coopsoft.com/ar/CalamityArticle.html) - 对Java fork/join库的批判,特别是其在非严格计算时的work stealing实现
* [并发系统的调度技巧(Scheduling techniques for concurrent systems)](http://www.ece.rutgers.edu/~parashar/Classes/ece572-papers/05/ps-ousterhout.pdf)
* [竞争启发调度(Contention aware scheduling)](http://www.blagodurov.net/files/a8-blagodurov.pdf)
* [时间共享多核系统的平衡work stealing(Balanced work stealing for time-sharing multicores)](http://www.cse.ohio-state.edu/hpcs/WWW/HTML/publications/papers/TR-12-1.pdf)
* [三层蛋糕?(Three layer cake)](http://www.upcrc.illinois.edu/workshops/paraplop10/papers/paraplop10_submission_8.pdf)
* [非阻塞半work stealing队列(Non-blocking steal-half work queues)](http://www.cs.bgu.ac.il/~hendlerd/papers/p280-hendler.pdf)
* [Reagents:表现和编写细粒度的并发(Reagents: expressing and composing fine-grained concurrency)](http://www.mpi-sws.org/~turon/reagents.pdf)
* [用于共享内存多处理器的可扩展同步性的算法(Algorithms for scalable synchronization of shared-memory multiprocessors)](https://www.cs.rochester.edu/u/scott/papers/1991_TOCS_synch.pdf)
## 其它
* [只能崩溃的软件(Crash-only software)](https://www.usenix.org/legacy/events/hotos03/tech/full_papers/candea/candea.pdf)
* [编写高性能内存分配器(Composing High-Performance Memory Allocators)](http://people.cs.umass.edu/~emery/pubs/berger-pldi2001.pdf)
* [对手动内存分配的思考(Reconsidering Custom Memory Allocation)](http://people.cs.umass.edu/~emery/pubs/berger-oopsla2002.pdf)
## 关于Rust的论文
* [Rust中的GPU编程(GPU programming in Rust)](http://www.cs.indiana.edu/~eholk/papers/hips2013.pdf)
* [并行闭包:一个基于老观点的新做法(Parallel closures: a new twist on an old idea)](https://www.usenix.org/conference/hotpar12/parallel-closures-new-twist-old-idea) - 并不完全关于Rust,不过是Nicholas D. Matsakis写的
7.词汇表
最后更新于:2022-04-01 00:45:14
不是每位Rustacean都是系统编程或计算机科学背景的,所以我们加上了可能难以理解的词汇解释。
## 数量(Arity)
Arity代表函数或操作所需的参数数量。
~~~
let x = (2, 3);
let y = (4, 6);
let z = (8, 2, 6);
~~~
在上面的例子中`x`和`y`的Arity是`2`,`z`的Arity是`3`。
## 抽象语法树(Abstract Syntax Tree)
当一个编译器编译你程序的时候,它做了很多不同的事。其中之一就是将你程序中的文本转换为一个‘抽象语法树’,或者‘AST’。这个树是你程序结构的表现。例如,`2 + 3`可以转换为一个树:
~~~
+
/ \
2 3
~~~
而`2 + (3 * 4)`看起来像这样:
~~~
+
/ \
2 *
/ \
3 4
~~~
6.10.关联常量
最后更新于:2022-04-01 00:45:12
通过`associated_consts`功能,你像这样可以定义常量:
~~~
trait Foo {
const ID: i32;
}
impl Foo for i32 {
const ID: i32 = 1;
}
fn main() {
assert_eq!(1, i32::ID);
}
~~~
任何`Foo`的定义都必须定义`ID`,不定义的话:
~~~
#![feature(associated_consts)]
trait Foo {
const ID: i32;
}
impl Foo for i32 {
}
~~~
会给出
~~~
error: not all trait items implemented, missing: `ID` [E0046]
impl Foo for i32 {
}
~~~
实现也可以定义一个默认值:
~~~
#![feature(associated_consts)]
trait Foo {
const ID: i32 = 1;
}
impl Foo for i32 {
}
impl Foo for i64 {
const ID: i32 = 5;
}
fn main() {
assert_eq!(1, i32::ID);
assert_eq!(5, i64::ID);
}
~~~
如你所见,当实现`Foo`时,你可以不实现它(关联常量),当作为`i32`时。接着它将会使用默认值。不过,作为`i64`时,我们可以增加我们自己的定义。
关联常量并不一定要关联在一个特性上。一个`struct`的`impl`也行:
~~~
#![feature(associated_consts)]
struct Foo;
impl Foo {
pub const FOO: u32 = 3;
}
~~~
6.9.切片模式
最后更新于:2022-04-01 00:45:10
如果你想在一个切片或数组上匹配,你可以通过`slice_patterns`功能使用`&`:
~~~
#![feature(slice_patterns)]
fn main() {
let v = vec!["match_this", "1"];
match &v[..] {
["match_this", second] => println!("The second element is {}", second),
_ => {},
}
}
~~~
`advanced_slice_patterns`通道让你使用`..`表明在一个切片的模式匹配中任意数量的元素。这个通配符对一个给定的数组只能只用一次。如果在`..`之前有一个标识符,结果会被绑定到那个名字上。例如:
~~~
#![feature(advanced_slice_patterns, slice_patterns)]
fn is_symmetric(list: &[u32]) -> bool {
match list {
[] | [_] => true,
[x, inside.., y] if x == y => is_symmetric(inside),
_ => false
}
}
fn main() {
let sym = &[0, 1, 4, 2, 4, 1, 0];
assert!(is_symmetric(sym));
let not_sym = &[0, 1, 7, 2, 4, 1, 0];
assert!(!is_symmetric(not_sym));
}
~~~
6.8.装箱语法和模式
最后更新于:2022-04-01 00:45:07
目前唯一稳定的创建`Box`的方法是通过`Box::new`方法。并且不可能在一个模式匹配中稳定的析构一个`Box`。不稳定的`box`关键字可以用来创建和析构`Box`。下面是一个用例:
~~~
#![feature(box_syntax, box_patterns)]
fn main() {
let b = Some(box 5);
match b {
Some(box n) if n < 0 => {
println!("Box contains negative number {}", n);
},
Some(box n) if n >= 0 => {
println!("Box contains non-negative number {}", n);
},
None => {
println!("No box");
},
_ => unreachable!()
}
}
~~~
注意这些功能目前隐藏在`box_syntax`(装箱创建)和`box_patterns`(析构和模式匹配)通道之中因为它的语法在未来可能会改变。
## 返回指针
在很多有指针的语言中,你的函数可以返回一个指针来避免拷贝大的数据结构。例如:
~~~
struct BigStruct {
one: i32,
two: i32,
// etc
one_hundred: i32,
}
fn foo(x: Box<BigStruct>) -> Box<BigStruct> {
Box::new(*x)
}
fn main() {
let x = Box::new(BigStruct {
one: 1,
two: 2,
one_hundred: 100,
});
let y = foo(x);
}
~~~
要点是通过传递一个装箱,你只需拷贝了一个指针,而不是那构成了`BigStruct`的一百个`int`值。
上面是Rust中的一个反模式。相反,这样写:
~~~
#![feature(box_syntax)]
struct BigStruct {
one: i32,
two: i32,
// etc
one_hundred: i32,
}
fn foo(x: Box<BigStruct>) -> BigStruct {
*x
}
fn main() {
let x = Box::new(BigStruct {
one: 1,
two: 2,
one_hundred: 100,
});
let y: Box<BigStruct> = box foo(x);
}
~~~
这在不牺牲性能的前提下获得了灵活性。
你可能会认为这会给我们带来很差的性能:返回一个值然后马上把它装箱?难道这在哪里不都是最糟的吗?Rust显得更聪明。这里并没有拷贝。`main`为装箱分配了足够的空间,向`foo`传递一个指向他内存的`x`,然后`foo`直接向`Box`中写入数据。
因为这很重要所以要说两遍:返回指针会阻止编译器优化你的代码。允许调用函数选择它们需要如何使用你的输出。
6.7.基准测试
最后更新于:2022-04-01 00:45:05
Rust也支持基准测试,它可以测试代码的性能。让我们把`src/lib.rs`修改成这样(省略注释):
~~~
#![feature(test)]
extern crate test;
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod test {
use super::*;
use test::Bencher;
#[test]
fn it_works() {
assert_eq!(4, add_two(2));
}
#[bench]
fn bench_add_two(b: &mut Bencher) {
b.iter(|| add_two(2));
}
}
~~~
注意`test`功能通道,它启用了这个不稳定功能。
我们导入了`test`包装箱,它包含了对基准测试的支持。我们也定义了一个新函数,带有`bench`属性。与一般的不带参数的测试不同,基准测试有一个`&mut Bencher`参数。`Bencher`提供了一个`iter`方法,它接收一个闭包。这个闭包包含我们想要测试的代码。
我们可以用`cargo bench`来运行基准测试:
~~~
$ cargo bench
Compiling adder v0.0.1 (file:///home/steve/tmp/adder)
Running target/release/adder-91b3e234d4ed382a
running 2 tests
test tests::it_works ... ignored
test tests::bench_add_two ... bench: 1 ns/iter (+/- 0)
test result: ok. 0 passed; 0 failed; 1 ignored; 1 measured
~~~
我们的非基准测试将被忽略。你也许会发现`cargo bench`比`cargo test`花费的时间更长。这是因为Rust会多次运行我们的基准测试,然后取得平均值。因为我们的函数只做了非常少的操作,我们耗费了`1 ns/iter (+/- 0)`,不过运行时间更长的测试就会有出现偏差。
编写基准测试的建议:
* 把初始代码放于`iter`循环之外,只把你需要测试的部分放入它
* 确保每次循环都做了“同样的事情”,不要累加或者改变状态
* 确保`iter`循环内简短而快速,这样基准测试会运行的很快同时校准器可以在合适的分辨率上调整运转周期
* 确保`iter`循环执行简单的工作,这样可以帮助我们准确的定位性能优化(或不足)
# Gocha:优化
写基准测试有另一些比较微妙的地方:开启了优化编译的基准测试可能被优化器戏剧性的修改导致它不再是我们期望的基准测试了。举例来说,编译器可能认为一些计算并无外部影响并且整个移除它们。
~~~
#![feature(test)]
extern crate test;
use test::Bencher;
#[bench]
fn bench_xor_1000_ints(b: &mut Bencher) {
b.iter(|| {
(0..1000).fold(0, |old, new| old ^ new);
});
}
~~~
得到如下结果:
~~~
running 1 test
test bench_xor_1000_ints ... bench: 0 ns/iter (+/- 0)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
~~~
基准测试运行器提供两种方法来避免这个问题:要么传递给`iter`的闭包可以返回一个随机的值这样强制优化器认为结果有用并确保它不会移除整个计算部分。这可以通过修改上面例子中的`b.iter`调用:
~~~
b.iter(|| {
// note lack of `;` (could also use an explicit `return`).
(0..1000).fold(0, |old, new| old ^ new)
});
~~~
要么,另一个选择是调用通用的`test::black_box`函数,它会传递给优化器一个不透明的“黑盒”这样强制它考虑任何它接收到的参数。
~~~
#![feature(test)]
extern crate test;
b.iter(|| {
let n = test::black_box(1000);
(0..n).fold(0, |a, b| a ^ b)
})
~~~
上述两种方法均未读取或修改值,并且对于小的值来说非常廉价。对于大的只可以通过间接传递来减小额外开销(例如:`black_box(&huge_struct)`)。
执行上面任何一种修改可以获得如下基准测试结果:
~~~
running 1 test
test bench_xor_1000_ints ... bench: 131 ns/iter (+/- 3)
test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured
~~~
然而,即使使用了上述方法优化器还是可能在不合适的情况下修改测试用例。
6.6.链接参数
最后更新于:2022-04-01 00:45:03
这里还有一个方法来告诉rustc如何自定义链接,这就是通过`link_args`属性。这个属性作用于`extern`块并指定当产生构件时需要传递给连接器的原始标记。一个用例将是:
~~~
#![feature(link_args)]
#[link_args = "-foo -bar -baz"]
extern {}
~~~
注意现在这个功能隐藏在`feature(link_args)`通道之后因为它并不是一个被认可的执行链接的方法。目前rustc从shell调用系统的连接器,所以使用额外的命令行参数是可行的,不过这并一定永远可行。将来rustc可能使用LLVM直接链接原生库这样一来`link_args`就毫无意义了。
强烈建议你_不要_使用这个属性,而是使用一个更正式的`[link(...)]`属性作用于`extern`块。
6.5.语言项
最后更新于:2022-04-01 00:45:01
> **注意**:语言项通常由Rust发行版的包装箱提供,并且它自身有一个不稳定的接口。建议使用官方发布的包装箱而不是定义自己的版本。
`rustc`编译器有一些可插入的操作,也就是说,功能不是硬编码进语言的,而是在库中实现的,通过一个特殊的标记告诉编译器它存在。这个标记是`#[lang="..."]`属性并且有不同的值`...`,也就是不同的“语言项”。
例如,`Box`指针需要两个语言项,一个用于分配,一个用于释放。下面是一个独立的程序使用`Box`语法糖进行动态分配,通过`malloc`和`free`:
~~~
#![feature(lang_items, box_syntax, start, no_std, libc)]
#![no_std]
extern crate libc;
extern {
fn abort() -> !;
}
#[lang = "owned_box"]
pub struct Box<T>(*mut T);
#[lang="exchange_malloc"]
unsafe fn allocate(size: usize, _align: usize) -> *mut u8 {
let p = libc::malloc(size as libc::size_t) as *mut u8;
// malloc failed
if p as usize == 0 {
abort();
}
p
}
#[lang="exchange_free"]
unsafe fn deallocate(ptr: *mut u8, _size: usize, _align: usize) {
libc::free(ptr as *mut libc::c_void)
}
#[start]
fn main(argc: isize, argv: *const *const u8) -> isize {
let x = box 1;
0
}
#[lang = "stack_exhausted"] extern fn stack_exhausted() {}
#[lang = "eh_personality"] extern fn eh_personality() {}
#[lang = "panic_fmt"] fn panic_fmt() -> ! { loop {} }
~~~
注意`abort`的使用:`exchange_malloc`语言项假设返回一个有效的指针,所以需要在内部进行检查。
其它语言项提供的功能包括:
* 通过特性重载运算符:`==`,`<`,解引用(`*`)和`+`等运算符对应的特性都有语言项标记;上面4个分别为`eq`,`ord`,`deref`和`add`
* 栈展开和一般故障:`eh_personality`,`fail`和`fail_bounds_checks`语言项
* `std::marker`中用来标明不同类型的特性:`send`,`sync`和`copy`。
* `std::marker`中的标记类型和变化指示器:`covariant_type`和`contravariant_lifetime`等
语言项由编译器延时加载;例如,如果你从未用过`Box`则就没有必要定义`exchange_malloc`和`exchange_free`的函数。`rustc`在一个项被需要而无法在当前包装箱或任何依赖中找到时生成一个错误。
6.4.固有功能
最后更新于:2022-04-01 00:44:58
> **注意**:固有功能将会永远是一个不稳定的接口,推荐使用稳定的libcore接口而不是直接使用编译器自带的功能。
可以像FFI函数那样导入它们,使用特殊的`rust-intrinsic`ABI。例如,如果在一个独立的上下文,但是想要能在类型间`transmute`,并想进行高效的指针计算,你可以声明函数:
~~~
extern "rust-intrinsic" {
fn transmute<T, U>(x: T) -> U;
fn offset<T>(dst: *const T, offset: isize) -> *const T;
}
~~~
跟其它FFI函数一样,它们总是`unsafe`的。
6.3.不使用标准库
最后更新于:2022-04-01 00:44:56
`std`默认被链接到每个Rust包装箱中。在一些情况下,这是不合适的,并且可以通过在包装箱上加入`#![no_std]`属性来避免这一点。
~~~
// a minimal library
#![crate_type="lib"]
#![feature(no_std)]
#![no_std]
~~~
很明显不光库可以使用这一点:你可以在可执行文件上使用`#[no_std]`,控制程序入口点有两种可能的方式:`#[start]`属性,或者用你自己的去替换C语言默认的`main`函数。
被标记为`#[start]`的函数传递的参数格式与C一致:
~~~
#![feature(lang_items, start, no_std)]
#![no_std]
// Pull in the system libc library for what crt0.o likely requires
extern crate libc;
// Entry point for this program
#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
0
}
// These functions and traits are used by the compiler, but not
// for a bare-bones hello world. These are normally
// provided by libstd.
#[lang = "stack_exhausted"] extern fn stack_exhausted() {}
#[lang = "eh_personality"] extern fn eh_personality() {}
#[lang = "panic_fmt"] fn panic_fmt() -> ! { loop {} }
~~~
要覆盖编译器插入的`main`函数,你必须使用`#![no_main]`并通过正确的ABI和正确的名字来创建合适的函数,这也需要需要覆盖编译器的命名改编:
~~~
#![feature(no_std)]
#![no_std]
#![no_main]
#![feature(lang_items, start)]
extern crate libc;
#[no_mangle] // ensure that this symbol is called `main` in the output
pub extern fn main(argc: i32, argv: *const *const u8) -> i32 {
0
}
#[lang = "stack_exhausted"] extern fn stack_exhausted() {}
#[lang = "eh_personality"] extern fn eh_personality() {}
#[lang = "panic_fmt"] fn panic_fmt() -> ! { loop {} }
~~~
目前编译器对能够被可执行文件调用的符号做了一些假设。正常情况下,这些函数是由标准库提供的,不过没有它你就必须定义你自己的了。
这三个函数中的第一个`stack_exhausted`,当检测到栈溢出时被调用。这个函数对于如何被调用和应该干什么有一些限制,不顾如果栈限制寄存器没有被维护则一个线程可以有”无限的栈“,这种情况下这个函数不应该被触发。
第二个函数,`eh_personality`,被编译器的错误机制使用。它通常映射到GCC的特性函数上(查看[libstd实现](http://doc.rust-lang.org/std/rt/unwind/)来获取更多信息),不过对于不会触发恐慌的包装箱可以确定这个函数不会被调用。最后一个函数,`panic_fmt`,也被编译器的错误机制使用。
## 使用libcore
> **注意**:核心库的结构是不稳定的,建议在任何可能的情况下使用标准库。
通过上面的计数,我们构造了一个少见的运行Rust代码的可执行程序。标准库提供了很多功能,然而,这是Rust的生产力所需要的。如果标准库是不足的话,那么可以使用被设计为标准库替代的[libcore](http://doc.rust-lang.org/core/)。
核心库只有很少的依赖并且比标准库可移植性更强。另外,核心库包含编写符合习惯和高效Rust代码的大部分功能。
例如,下面是一个计算由C提供的两个向量的数量积的函数,使用常见的Rust实现。
~~~
#![feature(lang_items, start, no_std, core, libc)]
#![no_std]
extern crate core;
use core::prelude::*;
use core::mem;
#[no_mangle]
pub extern fn dot_product(a: *const u32, a_len: u32,
b: *const u32, b_len: u32) -> u32 {
use core::raw::Slice;
// Convert the provided arrays into Rust slices.
// The core::raw module guarantees that the Slice
// structure has the same memory layout as a &[T]
// slice.
//
// This is an unsafe operation because the compiler
// cannot tell the pointers are valid.
let (a_slice, b_slice): (&[u32], &[u32]) = unsafe {
mem::transmute((
Slice { data: a, len: a_len as usize },
Slice { data: b, len: b_len as usize },
))
};
// Iterate over the slices, collecting the result
let mut ret = 0;
for (i, j) in a_slice.iter().zip(b_slice.iter()) {
ret += (*i) * (*j);
}
return ret;
}
#[lang = "panic_fmt"]
extern fn panic_fmt(args: &core::fmt::Arguments,
file: &str,
line: u32) -> ! {
loop {}
}
#[lang = "stack_exhausted"] extern fn stack_exhausted() {}
#[lang = "eh_personality"] extern fn eh_personality() {}
~~~
注意这里有一个额外的`lang`项与之前的例子不同,`panic_fmt`。它必须由libcore的调用者定义因为核心库声明了恐慌,但没有定义它。`panic_fmt`项是这个包装箱的恐慌定义,并且它必须确保不会返回。
正如你在例子中所看到的,核心库尝试在所有情况下提供Rust的功能,不管平台的要求如何。另外一些库,例如`liballoc`,为libcore增加了进行其它平台相关假设的功能,不过这依旧比标准库更有可移植性。
6.2.内联汇编
最后更新于:2022-04-01 00:44:54
为了极端底层操作和性能要求,你可能希望直接控制CPU。Rust通过`asm!`宏来支持使用内联汇编。语法大体上与GCC和Clang相似:
~~~
asm!(assembly template
: output operands
: input operands
: clobbers
: options
);
~~~
任何`asm`的使用需要功能通道(需要在包装箱上加上`#![feature(asm)]`来允许使用)并且当然也需要写在`unsafe`块中
> **注意**:这里的例子使用了x86/x86-64汇编,不过所有平台都受支持。
## 汇编模板
`assembly template`是唯一需要的参数并且必须是原始字符串(就是`""`)
~~~
#![feature(asm)]
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn foo() {
unsafe {
asm!("NOP");
}
}
// other platforms
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
fn foo() { /* ... */ }
fn main() {
// ...
foo();
// ...
}
~~~
(`feature(asm)`和`#[cfg]`从现在开始将被忽略。)
输出操作数,输入操作数,覆盖和选项都是可选的不过你必选加上正确数量的`:`如果你要省略它们的话:
~~~
asm!("xor %eax, %eax"
:
:
: "eax"
);
~~~
空格也是无所谓的:
~~~
asm!("xor %eax, %eax" ::: "eax");
~~~
## 操作数
输入和输出操作数都有相同的格式:`: "constraints1"(expr1), "constraints2"(expr2), ..."`。输出操作数表达式必须是可变的左值,或还未赋值的:
~~~
fn add(a: i32, b: i32) -> i32 {
let mut c = 0;
unsafe {
asm!("add $2, $0"
: "=r"(c)
: "0"(a), "r"(b)
);
}
c
}
fn main() {
assert_eq!(add(3, 14159), 14162)
}
~~~
如果你想在这里使用真正的操作数,然而,要求你在你想使用的寄存器上套上大括号`{}`,并且要求你指明操作数的大小。这在非常底层的编程中是很有用的,这时你使用哪个寄存器是很重要的:
~~~
let result: u8;
asm!("in %dx, %al" : "={al}"(result) : "{dx}"(port));
result
~~~
## 覆盖(Clobbers)
一些指令修改可能保存有不同值寄存器所以我们使用覆盖列表来告诉编译器不要假设任何装载在这些寄存器的值是有效的。
~~~
// Put the value 0x200 in eax
asm!("mov $0x200, %eax" : /* no outputs */ : /* no inputs */ : "eax");
~~~
输入和输出寄存器并不需要列出因为这些信息已经通过给出的限制沟通过了。因此,任何其它的被使用的寄存器应该隐式或显式的被列出。
如果汇编修改了代码状态寄存器`cc`则需要在覆盖中被列出,如果汇编修改了内存,`memory`也应被指定。
## 选项(Options)
最后一部分,`options`是Rust特有的。格式是逗号分隔的基本字符串(也就是说,`:"foo", "bar", "baz"`)。它被用来指定关于内联汇编的额外信息:
目前有效的选项有:
1. _volatile_ - 相当于gcc/clang中的`__asm__ __volatile__ (...)`
2. _alignstack_ - 特定的指令需要栈按特定方式对齐(比如,SSE)并且指定这个告诉编译器插入通常的栈对齐代码
3. _intel_ - 使用intel语法而不是默认的AT&T语法
~~~
let result: i32;
unsafe {
asm!("mov eax, 2" : "={eax}"(result) : : : "intel")
}
println!("eax is currently {}", result);
~~~
6.1.编译器插件
最后更新于:2022-04-01 00:44:52
## 介绍
`rustc`可以加载编译器插件,它是由用户提供的库用来扩充编译器的行为,例如新的语法扩展,lint检查等。
一个插件是带有设计好的用来在`rustc`中注册扩展的_注册_(_registrar_)函数的一个动态库包装箱。其它包装箱可以使用`#![plugin(...)]`属性来装载这个扩展。查看[rustc::plugin](http://doc.rust-lang.org/rustc/plugin/)文档来获取更多关于定义和装载插件的机制。
如果属性存在的话,`#![plugin(foo(... args ...))]`传递的参数并不由`rustc`自身解释。它们被传递给插件的`Registry`[args方法](http://doc.rust-lang.org/rustc/plugin/registry/struct.Registry.html#method.args)。
在绝大多数情况中,一个插件应该_只_通过`#![plugin]`而不通过`extern crate`来使用。链接一个插件会将`libsyntax`和`librustc`加入到你的包装箱的依赖中。基本上你不会希望如此除非你在构建另一个插件。`plugin_as_library`lint会检查这些原则。
通常的做法是将插件放到它们自己的包装箱中,与任何那些会被库的调用者使用的`macro_rules!`宏或Rust代码分开。
## 语法扩展
插件可以有多种方法来扩展Rust的语法。一种语法扩展是宏过程。它们与[普通宏](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.35.Macros%20%E5%AE%8F.md)的调用方法一样,不过扩展是通过执行任意Rust代码在编译时操作[语法树](http://doc.rust-lang.org/syntax/ast/)进行的。
让我们写一个实现了罗马数字的插件[roman_numerals.rs](https://github.com/rust-lang/rust/blob/master/src/test/auxiliary/roman_numerals.rs)。
~~~
#![crate_type="dylib"]
#![feature(plugin_registrar, rustc_private)]
extern crate syntax;
extern crate rustc;
use syntax::codemap::Span;
use syntax::parse::token;
use syntax::ast::{TokenTree, TtToken};
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::ext::build::AstBuilder; // trait for expr_usize
use rustc::plugin::Registry;
fn expand_rn(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree])
-> Box<MacResult + 'static> {
static NUMERALS: &'static [(&'static str, u32)] = &[
("M", 1000), ("CM", 900), ("D", 500), ("CD", 400),
("C", 100), ("XC", 90), ("L", 50), ("XL", 40),
("X", 10), ("IX", 9), ("V", 5), ("IV", 4),
("I", 1)];
let text = match args {
[TtToken(_, token::Ident(s, _))] => token::get_ident(s).to_string(),
_ => {
cx.span_err(sp, "argument should be a single identifier");
return DummyResult::any(sp);
}
};
let mut text = &*text;
let mut total = 0;
while !text.is_empty() {
match NUMERALS.iter().find(|&&(rn, _)| text.starts_with(rn)) {
Some(&(rn, val)) => {
total += val;
text = &text[rn.len()..];
}
None => {
cx.span_err(sp, "invalid Roman numeral");
return DummyResult::any(sp);
}
}
}
MacEager::expr(cx.expr_u32(sp, total))
}
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_macro("rn", expand_rn);
}
~~~
我们可以像其它宏那样使用`rn!()`:
~~~
#![feature(plugin)]
#![plugin(roman_numerals)]
fn main() {
assert_eq!(rn!(MMXV), 2015);
}
~~~
与一个简单的`fn(&str) -> u32`函数相比的优势有:
* (任意复杂程度的)转换都发生在编译时
* 输入验证也在编译时进行
* 可以扩展并允许在模式中使用,它可以有效的为任何数据类型定义新语法。
除了宏过程,你可以定义新的类[derive](http://doc.rust-lang.org/reference.html#derive)属性和其它类型的扩展。查看[Registry::register_syntax_extension](http://doc.rust-lang.org/rustc/plugin/registry/struct.Registry.html#method.register_syntax_extension)和[SyntaxExtension enum](http://doc.rust-lang.org/syntax/ext/base/enum.SyntaxExtension.html)。对于更复杂的宏例子,查看[regex_macros](https://github.com/rust-lang/regex/blob/master/regex_macros/src/lib.rs)。
## 提示与技巧
这里提供一些[宏调试的提示](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.35.Macros%20%E5%AE%8F.md#debugging-macro-code)。
你可以使用[syntax::parse](http://doc.rust-lang.org/syntax/parse/)来将记号树转换为像表达式这样的更高级的语法元素:
~~~
fn expand_foo(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree])
-> Box<MacResult+'static> {
let mut parser = cx.new_parser_from_tts(args);
let expr: P<Expr> = parser.parse_expr();
~~~
看完[libsyntax解析器代码](https://github.com/rust-lang/rust/blob/master/src/libsyntax/parse/parser.rs)会给你一个解析基础设施如何工作的感觉。
保留你解析所有的[Span](http://doc.rust-lang.org/syntax/codemap/struct.Span.html),以便更好的报告错误。你可以用[Spanned](http://doc.rust-lang.org/syntax/codemap/struct.Spanned.html)包围你的自定数据结构。
调用[ExtCtxt::span_fatal](http://doc.rust-lang.org/syntax/ext/base/struct.ExtCtxt.html#method.span_fatal)将会立即终止编译。相反最好调用[ExtCtxt::span_err](http://doc.rust-lang.org/syntax/ext/base/struct.ExtCtxt.html#method.span_err)并返回[DummyResult](http://doc.rust-lang.org/syntax/ext/base/struct.DummyResult.html),这样编译器可以继续并找到更多错误。
为了打印用于调试的语法段,你可以同时使用[span_note](http://doc.rust-lang.org/syntax/ext/base/struct.ExtCtxt.html#method.span_note)和[syntax::print::pprust::*_to_string](http://doc.rust-lang.org/syntax/print/pprust/#functions)。
上面的例子使用[AstBuilder::expr_usize](http://doc.rust-lang.org/syntax/ext/build/trait.AstBuilder.html#tymethod.expr_usize)产生了一个普通整数。作为一个`AstBuilder`特性的额外选择,`libsyntax`提供了一个[准引用宏](http://doc.rust-lang.org/syntax/ext/quote/)的集合。它们并没有文档并且非常边缘化。然而,这些将会是实现一个作为一个普通插件库的改进准引用的好的出发点。
## Lint插件
插件可以扩展[Rust Lint基础设施](http://doc.rust-lang.org/reference.html#lint-check-attributes)来添加额外的代码风格,安全检查等。你可以查看[src/test/auxiliary/lint_plugin_test.rs](https://github.com/rust-lang/rust/blob/master/src/test/auxiliary/lint_plugin_test.rs)来了解一个完整的例子,我们在这里重现它的核心部分:
~~~
declare_lint!(TEST_LINT, Warn,
"Warn about items named 'lintme'")
struct Pass;
impl LintPass for Pass {
fn get_lints(&self) -> LintArray {
lint_array!(TEST_LINT)
}
fn check_item(&mut self, cx: &Context, it: &ast::Item) {
let name = token::get_ident(it.ident);
if name.get() == "lintme" {
cx.span_lint(TEST_LINT, it.span, "item is named 'lintme'");
}
}
}
#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
reg.register_lint_pass(box Pass as LintPassObject);
}
~~~
那么像这样的代码:
~~~
#![plugin(lint_plugin_test)]
fn lintme() { }
~~~
将产生一个编译警告:
~~~
foo.rs:4:1: 4:16 warning: item is named 'lintme', #[warn(test_lint)] on by default
foo.rs:4 fn lintme() { }
^~~~~~~~~~~~~~~
~~~
Lint插件的组件有:
* 一个或多个`declare_lint!`调用,它定义了[Lint](http://doc.rust-lang.org/rustc/lint/struct.Lint.html)结构
* 一个用来存放lint检查所需的所有状态(在我们的例子中,没有)
* 一个定义了如何检查每个语法元素的[LintPass](http://doc.rust-lang.org/rustc/lint/trait.LintPass.html)实现。一个单独的`LintPass`可能会对多个不同的`Lint`调用`span_lint`,不过它们都需要用`get_lints`方法进行注册。
Lint过程是语法遍历,不过它们运行在编译的晚期,这时类型信息时可用的。`rustc`的[内建lint](https://github.com/rust-lang/rust/blob/master/src/librustc/lint/builtin.rs)与lint插件使用相同的基础构架,并提供了如何访问类型信息的例子。
由插件定义的语法通常通过[属性和插件标识](http://doc.rust-lang.org/reference.html#lint-check-attributes)控制,例如,`[#[allow(test_lint)]]`,`-A test-lint`。这些标识符来自于`declare_lint!`的第一个参数,经过合适的大小写和标点转换。
你可以运行`rustc -W help foo.rs`来见检查lint列表是否为`rustc`所知,包括由`foo.rs`加载的插件。
6.Rust开发版
最后更新于:2022-04-01 00:44:49
Rust提供了3种发行渠道:开发版(每日构建),beta版和稳定版。不稳定功能只在Rust开发版中可用。对于这个进程的更多细节,查看[作为可支付的稳定性](http://blog.rust-lang.org/2014/10/30/Stability.html)。
要安装Rust开发版,你可以使用`rustup.sh`:
~~~
$ curl -s https://static.rust-lang.org/rustup.sh | sudo sh -s -- --channel=nightly
~~~
如果你担心使用`curl | sudo sh`的[潜在不安全性](http://curlpipesh.tumblr.com/),请继续阅读并查看我们下面的免责声明。并且你也可以随意使用下面这个两步安装脚本以便可以检查我们的安装脚本:
~~~
$ curl -L https://static.rust-lang.org/rustup.sh -O
$ sudo sh rustup.sh
~~~
如果你用Windows,请直接下载[32位](https://static.rust-lang.org/dist/rust-nightly-i686-pc-windows-gnu.exe)或者[64位](https://static.rust-lang.org/dist/rust-nightly-x86_64-pc-windows-gnu.exe)安装包然后运行即可。
## 卸载
如果不幸的,你再也不想使用Rust了:(,当然这不要紧。也许Rust不是你的菜(原文:不是所有人都会认为什么语言非常好)。运行下面的卸载脚本:
~~~
$ sudo /usr/local/lib/rustlib/uninstall.sh
~~~
如果你使用Windows安装包进行安装的话,重新运行`.exe`文件,它会提供一个卸载选项。
你可以在任何时候重新运行脚本来更新Rust。在现在这个时间,你将会频繁更新Rust,因为Rust还未发布1.0版本,经常更新人们会认为你在使用最新版本的Rust。
不过这带来了另外一个问题(传说中的免责声明?):一些同学确实有理由对我们让他们运行`curl | sudo sh`感到非常反感。他们理应如此。从根本上说,当你运行上面的脚本时,代表你相信是一些好人在维护Rust,他们不会黑了你的电脑做坏事。对此保持警觉是一个很好的天性。如果你是这些强迫症患者(大雾),请检阅以下文档,[从源码编译Rust](https://github.com/rust-lang/rust#building-from-source)或者[官方二进制文件下载](http://www.rust-lang.org/install.html)。我们保证这将不会一直作为安装Rust的方法:这只是为了方便大家在Alpha(现在是Beta了)时期更新Rust。
当然,我们需要提到官方支持的平台:
* Windows (7, 8, Server 2008 R2)
* Linux (2.6.18 or later, various distributions), x86 and x86-64
* OSX 10.7 (Lion) or greater, x86 and x86-64
Rust在以上平台进行了广泛的测试,当然还在一些其他平台,比如Android。不过进行了越多测试的环境,越有可能正常工作。
最后,关于Windows。Rust将Windows作为第一级平台来发布,不过说实话,WIndows的集成体验并没有Linux/OS X那么好。我们正在改善它!如果有情况它不能工作了,这是一个bug。如果这种发生了,请让我知道。任何一次提交都在Windows下进行了测试,就像其它平台一样。
如果你已安装Rust,你可以打开一个Shell,然后输入:
~~~
$ rustc --version
~~~
你应该看到版本号,提交的hash值,提交时间和构建时间:
~~~
rustc 1.0.0-nightly (f11f3e7ba 2015-01-04) (built 2015-01-06)
~~~
如果你做到了,那么Rust已经正确安装!此处应有掌声!
如果你遇到什么错误,这里有几个地方你可以获取帮助。最简单的是通过[Mibbit](http://chat.mibbit.com/?server=irc.mozilla.org&channel=%23rust)访问[Rust IRC频道 irc.mozilla.org](irc://irc.mozilla.org/#rust)。点击上面的链接,你就可以与其它Rustaceans(简单理解为Ruster吧)聊天,我们会帮助你。其它的地方有[the /r/rust subreddit](http://www.reddit.com/r/rust)和[Stack Overflow](http://stackoverflow.com/questions/tagged/rust)。
5.36.裸指针
最后更新于:2022-04-01 00:44:47
Rust的标准库中有一系列不同的智能指针类型,不过这有两个类型是十分特殊的。Rust的安全大多来源于编译时检查,不过裸指针并没有这样的保证,使用它们是[`unsafe`](http://doc.rust-lang.org/nightly/book/unsafe.html)的。
`*const T`和`*mut T`在Rust中被称为“裸指针”。有时当编写特定类型的库时,为了某些原因你需要绕过Rust的安全保障。在这种情况下,你可以使用裸指针来实现你的库,同时暴露一个安全的接口给你的用户。例如,`*`指针允许别名,允许用来写共享所有权类型,甚至是内存安全的共享内存类型(`Rc`和`Arc`类型都是完全用Rust实现的)。
这里有一些你需要记住的裸指针不同于其它指针的地方。它们是:
* 不能保证指向有效的内存,甚至不能保证是非空的(不像`Box`和`&`);
* 没有任何自动清除,不像`Box`,所以需要手动管理资源;
* 是普通旧式类型,也就是说,它不移动所有权,这又不像`Box`,因此Rust编译器不能保证不出像释放后使用这种bug;
* 被认为是可发送的(如果它的内容是可发送的),因此编译器不能提供帮助确保它的使用是线程安全的;例如,你可以从两个线程中并发的访问`*mut i32`而不用同步。
* 缺少任何形式的生命周期,不像`&`,因此编译器不能判断出悬垂指针;
* 除了通过`*const T`直接不允许改变外,没有别名或可变性的保障。
## 基础
创建一个裸指针是灰常安全的:
~~~
let x = 5;
let raw = &x as *const i32;
let mut y = 10;
let raw_mut = &mut y as *mut i32;
~~~
然而,解引用它则不行。这个不能工作:
~~~
let x = 5;
let raw = &x as *const i32;
println!("raw points at {}", *raw);
~~~
它给出这个错误:
~~~
error: dereference of unsafe pointer requires unsafe function or block [E0133]
println!("raw points at{}", *raw);
^~~~
~~~
当你解引用一个裸指针,你要为它并不指向正确的地方负责。为此,你需要`unsafe`:
~~~
let x = 5;
let raw = &x as *const i32;
let points_at = unsafe { *raw };
println!("raw points at {}", points_at);
~~~
关于裸指针的更多操作,查看[它们的API文档](http://doc.rust-lang.org/nightly/std/primitive.pointer.html)。
## FFI
裸指针在FFI中很有用:Rust的`*const T`和`*mut T`分别与C中的`const T*`和`T*`类似。关于它们的应用,查看[FFI章节](http://doc.rust-lang.org/nightly/book/ffi.html)。
## 引用和裸指针
在运行时,指向一份相同数据的裸指针`*`和引用有相同的表现。事实上,在安全代码中`&T`引用会隐式的转换为一个`*const T`同时它们的`mut`变体也有类似的行为(这两种转换都可以显式执行,分别为`value as *const T`和`value as *mut T`)。
反其道而行之,从`*const`到`&`引用,是不安全的。一个`&T`总是有效的,所以,最少,`*const T`裸指针必须指向一个`T`的有效实例。进一步,结果指针必须满足引用的别名和可变性法则。编译器假设这些属性对任何引用都是有效的,不管它们是如何创建的,因而所以任何从裸指针来的转换都断言它们成立。程序猿_必须_保证它。
推荐的转换方法是
~~~
let i: u32 = 1;
// explicit cast
let p_imm: *const u32 = &i as *const u32;
let mut m: u32 = 2;
// implicit coercion
let p_mut: *mut u32 = &mut m;
unsafe {
let ref_imm: &u32 = &*p_imm;
let ref_mut: &mut u32 = &mut *p_mut;
}
~~~
与使用`transmute`相比更倾向于`&*x`解引用风格。或者比需要的更强大,并且更严格的操作更难以错误使用;例如,它要求`x`是一个指针(不像`transmute`)。
5.35.宏
最后更新于:2022-04-01 00:44:45
到目前为止你已经学到了不少Rust提供的抽象和重用代码的工具了。这些代码重用单元有丰富的语义结构。例如,函数有类型标记,类型参数有特性限制并且能重载的函数必须属于一个特定的特性。
这些结构意味着Rust核心抽象拥有强大的编译时正确性检查。不过作为代价的是灵活性的减少。如果你识别出一个重复代码的模式,你会发现把它们解释为泛型函数,特性或者任何Rust语义中的其它结构很难或者很麻烦。
宏允许我们在_句法_水平上进行抽象。宏是一个“可扩展”句法形式的速记。这个扩展发生在编译的早期,在任何静态检查之前。因此,宏可以实现很多Rust核心抽象不能做到的代码重用模式。
缺点是基于宏的代码更难懂,因为它很少利用Rust的内建规则。就像一个常规函数,一个通用的宏可以在不知道其实现的情况下使用。然而,设计一个通用的宏困难的!另外,在宏中的编译错误更难解释,因为它在扩展代码上描述问题,恶如不是在开发者使用的代码级别。
这些缺点让宏成了所谓“最后求助于的功能”。这并不是说宏的坏话;只是因为它是Rust中需要真正简明,良好抽象的代码的部分。切记权衡取舍。
## 定义一个宏
你可能见过`vec!`宏。用来初始化一个任意数量元素的[vector](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.17.Vectors.html)。
~~~
let x: Vec<u32> = vec![1, 2, 3];
~~~
这不可能是一个常规函数,因为它可以接受任何数量的参数。不过我们可以想象的到它是这些代码的句法简写:
~~~
let x: Vec<u32> = {
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
};
~~~
我们可以使用宏来实现这么一个简写:[1](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.35.Macros%20%E5%AE%8F.html#1)
~~~
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
~~~
哇哦,这里有好多新语法!让我们分开来看。
~~~
macro_rules! vec { ... }
~~~
这里我们定义了一个叫做`vec`的宏,跟用`fn vec`定义一个`vec`函数很相似。再罗嗦一句,我们通常写宏的名字时带上一个感叹号,例如`vec!`。感叹号是调用语法的一部分用来区别宏和常规函数。
## 匹配
宏通过一系列_规则_定义,它们是模式匹配的分支。上面我们有:
~~~
( $( $x:expr ),* ) => { ... };
~~~
这就像一个`match`表达式分支,不过匹配发生在编译时Rust的语法树中。最后一个分支(这里只有一个分支)的分号是可选的。`=>`左侧的“模式”叫_匹配器_(_matcher_)。它有[自己的语法](http://doc.rust-lang.org/reference.html#macros)。
`$x:expr`匹配器将会匹配任何Rust表达式,把它的语法树绑定到元变量`$x`上。`expr`标识符是一个_片段分类符_(_fragment specifier_)。在[宏进阶章节](http://doc.rust-lang.org/book/advanced-macros.html)(已被本章合并,坐等官方文档更新)中列举了所有可能的分类符。匹配器写在`$(...)`中,`*`会匹配0个或多个表达式,表达式之间用逗号分隔。
除了特殊的匹配器语法,任何出现在匹配器中的Rust标记必须完全相符。例如:
~~~
macro_rules! foo {
(x => $e:expr) => (println!("mode X: {}", $e));
(y => $e:expr) => (println!("mode Y: {}", $e));
}
fn main() {
foo!(y => 3);
}
~~~
将会打印:
~~~
mode Y: 3
~~~
而这个:
~~~
foo!(z => 3);
~~~
我们会得到编译错误:
~~~
error: no rules expected the token `z`
~~~
## 扩展
宏规则的右边是正常的Rust语法,大部分是。不过我们可以拼接一些匹配器中的语法。例如最开始的例子:
~~~
$(
temp_vec.push($x);
)*
~~~
每个匹配的`$x`表达式都会在宏扩展中产生一个单独`push`语句。扩展中的重复与匹配器中的重复“同步”进行(稍后介绍更多)。
因为`$x`已经在表达式匹配中声明了,我们并不在右侧重复`:expr`。另外,我们并不将用来分隔的逗号作为重复操作的一部分。相反,我们在重复块中使用一个结束用的分号。
另一个细节:`vec!`宏的右侧有_两对_大括号。它们经常像这样结合起来:
~~~
macro_rules! foo {
() => {{
...
}}
}
~~~
外层的大括号是`macro_rules!`语法的一部分。事实上,你也可以`()`或者`[]`。它们只是用来界定整个右侧结构的。
内层大括号是扩展语法的一部分。记住,`vec!`在表达式上下文中使用。要写一个包含多个语句,包括`let`绑定,的表达式,我们需要使用块。如果你的宏只扩展一个单独的表达式,你不需要内层的大括号。
注意我们从未_声明_宏产生一个表达式。事实上,直到宏被展开之前我们都无法知道。足够小心的话,你可以编写一个能在多个上下文中扩展的宏。例如,一个数据类型的简写可以作为一个表达式或一个模式。
## 重复(Repetition)
重复运算符遵循两个原则:
1. `$(...)*`对它包含的所有`$name`都执行“一层”重复
2. 每个`$name`必须有至少这么多的`$(...)*`与其相对。如果多了,它将是多余的。
这个巴洛克宏展示了外层重复中多余的变量。
~~~
macro_rules! o_O {
(
$(
$x:expr; [ $( $y:expr ),* ]
);*
) => {
&[ $($( $x + $y ),*),* ]
}
}
fn main() {
let a: &[i32]
= o_O!(10; [1, 2, 3];
20; [4, 5, 6]);
assert_eq!(a, [11, 12, 13, 24, 25, 26]);
}
~~~
这就是匹配器的大部分语法。这些例子使用了`$(...)*`,它指“0次或多次”匹配。另外你可以用`$(...)+`代表“1次或多次”匹配。每种形式都可以包括一个分隔符,分隔符可以使用任何除了`+`和`*`的符号。
这个系统基于[Macro-by-Example](http://www.cs.indiana.edu/ftp/techreports/TR206.pdf)(PDF链接)。
## 卫生(Hygiene)
一些语言使用简单的文本替换来实现宏,它导致了很多问题。例如,这个C程序打印`13`而不是期望的`25`。
~~~
#define FIVE_TIMES(x) 5 * x
int main() {
printf("%d\n", FIVE_TIMES(2 + 3));
return 0;
}
~~~
扩展之后我们得到`5 * 2 + 3`,并且乘法比加法有更高的优先级。如果你经常使用C的宏,你可能知道标准的习惯来避免这个问题,或更多其它的问题。在Rust中,你不需要担心这个问题。
~~~
macro_rules! five_times {
($x:expr) => (5 * $x);
}
fn main() {
assert_eq!(25, five_times!(2 + 3));
}
~~~
元变量`$x`被解析成一个单独的表达式节点,并且在替换后依旧在语法树中保持原值。
宏系统中另一个常见的问题是_变量捕捉_(_variable capture_)。这里有一个C的宏,使用了[GNU C 扩展](https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html)来模拟Rust表达式块。
~~~
#define LOG(msg) ({ \
int state = get_log_state(); \
if (state > 0) { \
printf("log(%d): %s\n", state, msg); \
} \
})
~~~
这是一个非常糟糕的用例:
~~~
const char *state = "reticulating splines";
LOG(state)
~~~
它扩展为:
~~~
const char *state = "reticulating splines";
int state = get_log_state();
if (state > 0) {
printf("log(%d): %s\n", state, state);
}
~~~
第二个叫做`state`的参数参数被替换为了第一个。当打印语句需要用到这两个参数时会出现问题。
等价的Rust宏则会有理想的表现:
~~~
macro_rules! log {
($msg:expr) => {{
let state: i32 = get_log_state();
if state > 0 {println!("log({}): {}", state, $msg);
}
}};
}
fn main() {
let state: &str = "reticulating splines";
log!(state);
}
~~~
这之所以能工作时因为Rust有一个[卫生宏系统](http://en.wikipedia.org/wiki/Hygienic_macro)。每个宏扩展都在一个不同的_语法上下文_(_syntax context_)中,并且每个变量在引入的时候都在语法上下文中打了标记。这就好像是`main`中的`state`和宏中的`state`被画成了不同的“颜色”,所以它们不会冲突。
这也限制了宏在被执行时引入新绑定的能力。像这样的代码是不能工作的:
~~~
macro_rules! foo {
() => (let x = 3);
}
fn main() {
foo!();
println!("{}", x);
}
~~~
相反你需要在执行时传递变量的名字,这样它会在语法上下文中被正确标记。
~~~
macro_rules! foo {
($v:ident) => (let $v = 3);
}
fn main() {
foo!(x);
println!("{}", x);
}
~~~
这对`let`绑定和loop标记有效,对[items](http://doc.rust-lang.org/reference.html#items)无效。所以下面的代码可以编译:
~~~
macro_rules! foo {
() => (fn x() { });
}
fn main() {
foo!();
x();
}
~~~
## 递归宏
一个宏扩展中可以包含更多的宏,包括被扩展的宏自身。这种宏对处理树形结构输入时很有用的,正如这这个(简化了的)HTML简写所展示的那样:
~~~
macro_rules! write_html {
($w:expr, ) => (());
($w:expr, $e:tt) => (write!($w, "{}", $e));
($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => {{
write!($w, "<{}>", stringify!($tag));
write_html!($w, $($inner)*);
write!($w, "</{}>", stringify!($tag));
write_html!($w, $($rest)*);
}};
}
fn main() {
use std::fmt::Write;
let mut out = String::new();
write_html!(&mut out,
html[
head[title["Macros guide"]]
body[h1["Macros are the best!"]]
]);
assert_eq!(out,
"<html><head><title>Macros guide</title></head>\
<body><h1>Macros are the best!</h1></body></html>");
}
~~~
## 调试宏代码
运行`rustc --pretty expanded`来查看宏扩展后的结果。输出表现为一个完整的包装箱,所以你可以把它反馈给`rustc`,它会有时会比原版产生更好的错误信息。注意如果在同一作用域中有多个相同名字(不过在不同的语法上下文中)的变量的话`--pretty expanded`的输出可能会有不同的意义。这种情况下`--pretty expanded,hygiene`将会告诉你有关语法上下文的信息。
`rustc`提供两种语法扩展来帮助调试宏。目前为止,它们是不稳定的并且需要功能入口(feature gates)。
* `log_syntax!(...)`会打印它的参数到标准输出,在编译时,并且不“扩展”任何东西。
* `trace_macros!(true)`每当一个宏被扩展时会启用一个编译器信息。在扩展后使用`trace_macros!(false)`来关闭它。
## 句法要求
即使Rust代码中含有未扩展的宏,它也可以被解析为一个完整的[语法树](http://kaisery.gitbooks.io/rust-book-chinese/content/content/7.Glossary%20%E8%AF%8D%E6%B1%87%E8%A1%A8.md#abstract-syntax-tree)。这个属性对于编辑器或其它处理代码的工具来说十分有用。这里也有一些关于Rust宏系统设计的推论。
一个推论是Rust必须确定,当它解析一个宏扩展时,宏是否代替了
* 0个或多个项
* 0个或多个方法
* 一个表达式
* 一个语句
* 一个模式
一个块中的宏扩展代表一些项,或者一个表达式/语句。Rust使用一个简单的规则来解决这些二义性。一个代表项的宏扩展必须是
* 用大括号界定的,例如`foo! { ... }`
* 分号结尾的,例如`foo!(...);`
另一个展开前解析的推论是宏扩展必须包含有效的Rust记号。更进一步,括号,中括号,大括号在宏扩展中必须是封闭的。例如,`foo!([)`是不允许的。这让Rust知道宏何时结束。
更正式一点,宏扩展体必须是一个_记号树_(_token trees_)的序列。一个记号树是一系列递归的
* 一个由`()`,`[]`或`{}`包围的记号树序列
* 任何其它单个记号
在一个匹配器中,每一个元变量都有一个_片段分类符_(_fragment specifier_),确定它匹配的哪种句法。
* `ident`:一个标识符。例如:`x`,`foo`
* `path`:一个合适的名字。例如:`T::SpecialA`
* `expr`:一个表达式。例如:`2 + 2`;`if true then { 1 } else { 2 }`;`f(42)`
* `ty`:一个类型。例如:`i32`;`Vec`;`&T`
* `pat`:一个模式。例如:`Some(t)`;`(17, 'a')`;`_`
* `stmt`:一个单独语句。例如:`let x = 3`
* `block`:一个大括号界定的语句序列。例如:`{ log(error, "hi"); return 12; }`
* `item`:一个项。例如:`fn foo() { }`,`struct Bar`
* `meta`:一个“元项”,可以在属性中找到。例如:`cfg(target_os = "windows")`
* `tt`:一个单独的记号树
对于一个元变量后面的一个记号有一些额外的规则:
* `expr`变量必须后跟一个`=>`,`,`,`;`
* `ty`和`path`变量必须后跟一个`=>`,`,`,`:`,`=`,`>`,`as`
* `pat`变量必须后跟一个`=>`,`,`,`=`
* 其它变量可以后跟任何记号
这些规则为Rust语法提供了一些灵活性以便将来的扩展不会破坏现有的宏。
宏系统完全不处理解析模糊。例如,`$($t:ty)* $e:expr`语法总是会解析失败,因为解析器会被强制在解析`$t`和解析`$e`之间做出选择。改变扩展在它们之前分别加上一个记号可以解决这个问题。在这个例子中,你可以写成`$(T $t:ty)* E $e:exp`。
## 范围和宏导入/导出
宏在编译的早期阶段被展开,在命名解析之前。这有一个缺点是与语言中其它结构相比,范围对宏的作用不一样。
定义和扩展都发生在同一个深度优先,字典顺序的包装箱的代码遍历中。那么在模块范围内定义的宏对同模块的接下来的代码是可见的,这包括任何接下来的子`mod`项。
一个定义在`fn`函数体内的宏,或者任何其它不在模块范围内的地方,只在它的范围内可见。
如果一个模块有`subsequent`属性,它的宏在子`mod`项之后的父模块也是可见的。如果它的父模块也有`macro_use`属性那么在父`mod`项之后的祖父模块中也是可见的,以此类推。
`macro_use`属性也可以出现在`extern crate`。在这个上下文中它控制那些宏从外部包装箱中装载,例如
~~~
#[macro_use(foo, bar)]
extern crate baz;
~~~
如果属性只是简单的写成`#[macro_use]`,所有的宏都会被装载。如果没有`#[macro_use]`属性那么没有宏被装载。只有被定义为`#[macro_export]`的宏可能被装载。
装载一个包装箱的宏_而不_链接到输出,使用`#[no_link]`。
一个例子:
~~~
macro_rules! m1 { () => (()) }
// visible here: m1
mod foo {
// visible here: m1
#[macro_export]
macro_rules! m2 { () => (()) }
// visible here: m1, m2
}
// visible here: m1
macro_rules! m3 { () => (()) }
// visible here: m1, m3
#[macro_use]
mod bar {
// visible here: m1, m3
macro_rules! m4 { () => (()) }
// visible here: m1, m3, m4
}
// visible here: m1, m3, m4
~~~
当这个库被用`#[macro_use] extern crate`装载时,只有`m2`会被导入。
Rust参考中有一个[宏相关的属性列表](http://doc.rust-lang.org/reference.html#macro-related-attributes)。
## `$crate`变量
当一个宏在多个包装箱中使用时会产生另一个困难。让我们说`mylib`定义了
~~~
pub fn increment(x: u32) -> u32 {
x + 1
}
#[macro_export]
macro_rules! inc_a {
($x:expr) => ( ::increment($x) )
}
#[macro_export]
macro_rules! inc_b {
($x:expr) => ( ::mylib::increment($x) )
}
~~~
`inc_a`只能在`mylib`内工作,同时`inc_b`只能在库外工作。进一步说,如果用户有另一个名字导入`mylib`时`inc_b`将不能工作。
Rust(目前)还没有针对包装箱引用的卫生系统,不过它确实提供了一个解决这个问题的变通方法。当从一个叫`foo`的包装箱总导入宏时,特殊宏变量`$crate`会展开为`::foo`。相反,当这个宏在同一包装箱内定义和使用时,`$crate`将展开为空。这意味着我们可以写
~~~
#[macro_export]
macro_rules! inc {
($x:expr) => ( $crate::increment($x) )
}
~~~
来定义一个可以在库内外都能用的宏。这个函数名字会展开为`::increment`或`::mylib::increment`。
为了保证这个系统简单和正确,`#[macro_use] extern crate ...`应只出现在你包装箱的根中,而不是在`mod`中。这保证了`$crate`扩展为一个单独的标识符。
## 深入(The deep end)
之前的介绍章节提到了递归宏,但并没有给出完整的介绍。还有一个原因令递归宏是有用的:每一次递归都给你匹配宏参数的机会。
作为一个极端的例子,可以,但极端不推荐,用Rust宏系统来实现一个[位循环标记](http://esolangs.org/wiki/Bitwise_Cyclic_Tag)自动机。
~~~
macro_rules! bct {
// cmd 0: d ... => ...
(0, $($ps:tt),* ; $_d:tt)
=> (bct!($($ps),*, 0 ; ));
(0, $($ps:tt),* ; $_d:tt, $($ds:tt),*)
=> (bct!($($ps),*, 0 ; $($ds),*));
// cmd 1p: 1 ... => 1 ... p
(1, $p:tt, $($ps:tt),* ; 1)
=> (bct!($($ps),*, 1, $p ; 1, $p));
(1, $p:tt, $($ps:tt),* ; 1, $($ds:tt),*)
=> (bct!($($ps),*, 1, $p ; 1, $($ds),*, $p));
// cmd 1p: 0 ... => 0 ...
(1, $p:tt, $($ps:tt),* ; $($ds:tt),*)
=> (bct!($($ps),*, 1, $p ; $($ds),*));
// halt on empty data string
( $($ps:tt),* ; )
=> (());
}
~~~
练习:使用宏来减少上面`bct!`宏定义中的重复。
## 常用宏(Common macros)
这里有一些你会在Rust代码中看到的常用宏
### `panic!`
这个宏导致当前线程恐慌。你可以传给这个宏一个信息通过:
~~~
panic!("oh no!");
~~~
### `vec!`
`vec!`的应用遍及本书,所以你可能已经见过它了。它方便创建`Vec`:
~~~
let v = vec![1, 2, 3, 4, 5];
~~~
它也让你可以用重复值创建vector。例如,100个`0`:
~~~
let v = vec![0; 100];
~~~
### `assert!`和`assert_eq!`
这两个宏用在测试中。`assert!`获取一个布尔值,而`assert_eq!`获取两个值并比较它们。对了就通过,错了就`panic!`(注:原书是Truth passes, success panic!s,个人认为不对)。像这样:
~~~
// A-ok!
assert!(true);
assert_eq!(5, 3 + 2);
// nope :(
assert!(5 < 3);
assert_eq!(5, 3);
~~~
### `try!`
`try!`用来进行错误处理。它获取一些可以返回`Result`的数据,并返回`T`如果它是`Ok`,或`return`一个`Err(E)`如果出错了。像这样:
~~~
use std::fs::File;
fn foo() -> std::io::Result<()> {
let f = try!(File::create("foo.txt"));
Ok(())
}
~~~
它比这么写要更简明:
~~~
use std::fs::File;
fn foo() -> std::io::Result<()> {
let f = File::create("foo.txt");
let f = match f {
Ok(t) => t,
Err(e) => return Err(e),
};
Ok(())
}
~~~
### `unreachable!`
这个宏用于当你认为一些代码不应该被执行的时候:
~~~
if false {
unreachable!();
}
~~~
有时,编译器可能会让你编写一个不同的你认为将永远不会执行的分支。在这个例子中,用这个宏,这样如果你以错误结尾,你会为此得到一个`panic!`。
~~~
let x: Option<i32> = None;
match x {
Some(_) => unreachable!(),
None => println!("I know x is None!"),
}
~~~
### `unimplemented!`
`unimplemented!`宏可以被用来当你尝试去让你的函数通过类型检查,同时你又不想操心去写函数体的时候。一个这种情况的例子是实现一个要求多个方法的特性,而你只想一次搞定一个。用`unimplemented!`定义其它的直到你准备好去写它们了。
## 宏程序(Procedural macros)
如果Rust宏系统不能做你想要的,你可能想要写一个[编译器插件](http://kaisery.gitbooks.io/rust-book-chinese/content/content/6.1.Compiler%20Plugins%20%E7%BC%96%E8%AF%91%E5%99%A8%E6%8F%92%E4%BB%B6.md)。与`macro_rules!`宏相比,它能做更多的事,接口也更不稳定,并且bug将更难以追踪。相反你得到了可以在编译器中运行任意Rust代码的灵活性。为此语法扩展插件有时被称为_宏程序_(_procedural macros_)。
* * *
1. 在libcollections中的`vec!`的实际定义与我们在这展示的有所不同,出于效率和可重用性的考虑。
5.34.`Deref`强制多态
最后更新于:2022-04-01 00:44:42
标准库提供了一个特殊的特性,[`Deref`](http://doc.rust-lang.org/nightly/std/ops/trait.Deref.html)。它一般用来重载`*`,解引用运算符:
~~~
use std::ops::Deref;
struct DerefExample<T> {
value: T,
}
impl<T> Deref for DerefExample<T> {
type Target = T;
fn deref(&self) -> &T {
&self.value
}
}
fn main() {
let x = DerefExample { value: 'a' };
assert_eq!('a', *x);
}
~~~
这对编写自定义指针类型很有用。然而,这里有一个与`Deref`相关的语言功能:“解引用强制多态(deref coercions)”。规则如下:如果你有一个`U`类型,和它的实现`Deref`,(那么)`&U`的值将会自动转换为`&T`。这是一个例子:
~~~
fn foo(s: &str) {
// borrow a string for a second
}
// String implements Deref<Target=str>
let owned = "Hello".to_string();
// therefore, this works:
foo(&owned);
~~~
在一个值的前面用`&`号获取它的引用。所以`owned`是一个`String`,`&owned`是一个`&String`,而因为`impl Deref for String`,`&String`将会转换为`&str`,而它是`foo()`需要的。
这就是了。这是Rust唯一一个为你进行一个自动转换的地方,不过它增加了很多灵活性。例如,`Rc`类型实现了`Deref`,所以这可以工作:
~~~
use std::rc::Rc;
fn foo(s: &str) {
// borrow a string for a second
}
// String implements Deref<Target=str>
let owned = "Hello".to_string();
let counted = Rc::new(owned);
// therefore, this works:
foo(&counted);
~~~
我们所做的一切就是把我们的`String`封装到了一个`Rc`里。不过现在我们可以传递`Rc`给任何我们有一个`String`的地方。`foo`的签名并无变化,不过它对这两个类型都能正常工作。这个例子有两个转换:`Rc`转换为`String`接着是`String`转换为`&str`。只要类型匹配Rust将可以做任意多次这样的转换。
标准库提供的另一个非常通用的实现是:
~~~
fn foo(s: &[i32]) {
// borrow a slice for a second
}
// Vec<T> implements Deref<Target=[T]>
let owned = vec![1, 2, 3];
foo(&owned);
~~~
向量可以`Deref`为一个片段。
## `Deref`和方法调用
当调用一个方法时`Deref`也会出现。换句话说,这两个(应该是`T`和`&T`)在Rust中是一样的:
~~~
struct Foo;
impl Foo {
fn foo(&self) { println!("Foo"); }
}
let f = Foo;
f.foo();
~~~
即便`f`不是一个引用,而`foo`获取`&self`,这也是可以工作的。因为这些都是一样的:
~~~
f.foo();
(&f).foo();
(&&f).foo();
(&&&&&&&&f).foo();
~~~
一个`&&&&&&&&&&&&&&&&Foo`类型的值仍然可以调用`Foo`定义的方法,因为编译器会插入足够多的`*`来使类型正确。而正因为它插入`*`,它用了`Deref`。
5.33.运算符和重载
最后更新于:2022-04-01 00:44:40
Rust允许有限形式的运算符重载。这里有特定的运算符可以被重载。为了支持一个类型间特定的运算符,这里有一个你可以实现的特定的特性,它接着重载运算符。
例如,`+`运算符可以通过`Add`特性重载:
~~~
use std::ops::Add;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let p1 = Point { x: 1, y: 0 };
let p2 = Point { x: 2, y: 3 };
let p3 = p1 + p2;
println!("{:?}", p3);
}
~~~
在`main`中,我们可以对我们的两个`Point`用`+`号,因为我们已经为`Point`实现了`Add`。
这里有一系列可以这样被重载的运算符,并且所有与之相关的特性都在[`std::ops`](http://doc.rust-lang.org/nightly/std/ops/)模块中。查看它的文档来获取完整的列表。
实现这些特性要遵循一个模式。让我们仔细看看[`Add`](http://doc.rust-lang.org/nightly/std/ops/trait.Add.html):
~~~
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
~~~
这里总共涉及到3个类型:你`impl Add`的类型,`RHS`,它默认是`Self`,和`Output`。对于一个表达式`let z = x + y`,`x`是`Self`类型的,`y`是`RHS`,而`z`是`Self::Output`类型。
~~~
impl Add<i32> for Point {
type Output = f64;
fn add(self, rhs: i32) -> f64 {
// add an i32 to a Point and get an f64
}
}
~~~
将允许你这样做:
~~~
let p: Point = // ...
let x: f64 = p + 2i32;
~~~
5.32.不定长类型
最后更新于:2022-04-01 00:44:38
大部分类型有一个特定的大小,以字节为单位,它们在编译时是已知的。例如,一个`i32`是32位大,或者4个字节。然而,这里有些类型有益于表达,缺没有一个定义的大小。它们叫做“不定长”或者“动态大小”类型。一个例子是`[T]`。这个类型代表一个特定数量`t`的序列。不过我们并不知道有多少,所以大小是未知的。
Rust知道几个这样的类型,不过它们有一些限制。这有三个:
1. 我们只能通过指针操作一个不定长类型的实例。`&[T]`刚好能正常工作,不过`[T]`不行。一个`&[T]`能正常工作,不过一个`[T]`不行。
2. 变量和参数不能拥有动态大小类型。
3. 只有一个`struct`的最后一个字段可能拥有一个动态大小类型;其它字段必须不能。枚举变量必须不能用动态大小类型作为数据。
所以为什么这很重要?好吧,因为`[T]`只能用在一个指针之后,如果我们没有对不定长类型的语言支持,它将不可能这么写:
~~~
impl Foo for str {
~~~
或者
~~~
impl<T> Foo for [T] {
~~~
相反,你将不得不这么写:
~~~
impl Foo for &str {
~~~
意味深长的是,这个实现将只能用于[引用](http://doc.rust-lang.org/nightly/book/references-and-borrowing.html),并且不能用于其它类型的指针。通过`impl for str`,所有指针,包括(在一些地方,这里会有bug需要修复)用户自定义的智能指针,可以使用这个`impl`。
## `?Sized`
如果你想要写一个接受动态大小类型的函数,你可以使用这个特殊的限制,`?Sized`:
~~~
struct Foo<T: ?Sized> {
f: T,
}
~~~
这个`?`,读作“`T`可能是`Sized`的”,意味着这个限制是特殊的:它让我们的匹配更宽松,而不是相反。这几乎像每个`T`都隐式拥有`T: Sized`一样,`?`放松了这个默认(限制)。
5.31.关联类型
最后更新于:2022-04-01 00:44:35
关联类型是Rust类型系统中十分强力的一部分。它涉及到‘类型族’的概念,换句话说,就是把多种类型归于一类。这个描述可能比较抽象,所以让我们深入研究一个例子。如果你想编写一个`Graph`特性,你需要泛型化两个类型:点类型和边类型。所以你可能会像这样写一个特性,`Graph`:
~~~
trait Graph<N, E> {
fn has_edge(&self, &N, &N) -> bool;
fn edges(&self, &N) -> Vec<E>;
// etc
}
~~~
虽然这可以工作,不过显得很尴尬,例如,任何需要一个`Graph`作为参数的函数都需要泛型化的`N'ode和`E'dge类型:
~~~
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 { ... }
~~~
我们的距离计算并不需要`Edge`类型,所以函数签名中`E`只是写着玩的。
我们需要的是对于每一种`Graph`类型,都使用一个特定的的`N'ode和`E'dge类型。我们可以用关联类型来做到这一点:
~~~
trait Graph {
type N;
type E;
fn has_edge(&self, &Self::N, &Self::N) -> bool;
fn edges(&self, &Self::N) -> Vec<Self::E>;
// etc
}
~~~
现在,我们使用一个抽象的`Graph`了:
~~~
fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> uint { ... }
~~~
这里不再需要处理`E'dge类型了。
让我们更详细的回顾一下。
## 定义关联类型
让我们构建一个`Graph`特性。这里是定义:
~~~
trait Graph {
type N;
type E;
fn has_edge(&self, &Self::N, &Self::N) -> bool;
fn edges(&self, &Self::N) -> Vec<Self::E>;
}
~~~
十分简单。关联类型使用`type`关键字,并出现在特性体和函数中。
这些`type`声明跟函数定义一样。例如,如果我们想`N`类型实现`Display`,这样我们就可以打印出点类型,我们可以这样写:
~~~
use std::fmt;
trait Graph {
type N: fmt::Display;
type E;
fn has_edge(&self, &Self::N, &Self::N) -> bool;
fn edges(&self, &Self::N) -> Vec<Self::E>;
}
~~~
## 实现关联类型
就像任何特性,使用关联类型的特性用`impl`关键字来提供实现。下面是一个`Graph`的简单实现:
~~~
struct Node;
struct Edge;
struct MyGraph;
impl Graph for MyGraph {
type N = Node;
type E = Edge;
fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
true
}
fn edges(&self, n: &Node) -> Vec<Edge> {
Vec::new()
}
}
~~~
这个可笑的实现总是返回`true`和一个空的`Vec`,不过它提供了如何实现这类特性的思路。首先我们需要3个`struct`,一个代表图,一个代表点,还有一个代表边。如果使用别的类型更合理,也可以那样做,我们只是准备使用`struct`来代表这3个类型。
接下来是`impl`行,它就像其它任何特性的实现。
在这里,我们使用`=`来定义我们的关联类型。特性使用的名字出现在`=`的左边,而我们`impl`的具体类型出现在右边。最后,我们在函数声明中使用具体类型。
## 特性对象和关联类型
这里还有另外一个我们需要讨论的语法:特性对象。如果你创建一个关联类型的特性对象,像这样:
~~~
let graph = MyGraph;
let obj = Box::new(graph) as Box<Graph>;
~~~
你会得到两个错误:
~~~
error: the value of the associated type `E` (from the trait `main::Graph`) must
be specified [E0191]
let obj = Box::new(graph) as Box<Graph>;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24:44 error: the value of the associated type `N` (from the trait
`main::Graph`) must be specified [E0191]
let obj = Box::new(graph) as Box;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~
我们不能这样创建一个特性对象,因为我们并不知道关联的类型。相反,我们可以这样写:
~~~
let graph = MyGraph;
let obj = Box::new(graph) as Box<Graph<N=Node, E=Edge>>;
~~~
`N=Node`语法允许我们提供一个具体类型,`Node`,作为`N`类型参数。`E=Edge`也是一样。如果我们不提供这个限制,我们不能确定应该`impl`那个来匹配特性对象。