Rust 0.11.0文档翻译:Rust语言教程,The Rust Language Tutorial
这个教程正在被重新编写为 开发指南 。在那篇指南完成之前,本教程就是学习Rust 的正确途径。请以拉取请求(pull requests)的形式来提交妳所作出的改进,但是请注意,本教程最终会被删除。
Rust是一门专注于以下特性的编程语言:类型安全、内存安全、并发性和性能。它被用来编写大规模的、高性能的、免除了多种常见错误的软件。 Rust拥有一个复杂的内存模型,它能够鼓励妳采用高效的数据结构和安全的并发模式,能够禁止做出无效的内存访问以避免引起段错误。它是静态类型的语言,并且是编译型的。
作为一个多模式(multi-paradigm)的语言,Rust支持按以下风格编写代码:过程式、函数式和面向对象风格。以下是它的其中一些高级特性:
• 类型推导。本地变量声明中的类型标记是可选的。
• 安全的基于任务的并发性。Rust的轻量级任务之间不共享内存,而是通过消息来通信的。
• 较高阶的函数。高效且灵活的闭包,提供了迭代及其它控制结构
• 模式匹配及代数数据类型。Rust 的枚举类型(比C语言的枚举更强大,类型于函数式编程语言中的代数数据类型)中所提供的模式匹配是一种用来表示程序逻辑的紧凑而又富有表现力的方式。
• 多态性。Rust拥有:以类型为参数(type-parametric)的函数和类型(types);类型(type)类(classes);面向对象风格的接口。
1.1 讨论范围
这是一篇针对Rust 编程语言的介绍性教程。它涵盖了该语言的基础部分,包括:语法、类型系统、内存模型、泛型和模块。其它的教程中会深入讲解特定的语言特性。
此教程假设读者已经熟悉了C语言家族中的一种或几种语言。如果妳理解指针和常见的内存管理技巧,那么在阅读本教程时会更容易。
1.2 排版格式
在整篇教程中,语言关键字及示例代码里定义的标识符是以代码字体( code font )来显示的。
代码片断会有缩进,并且也是以等宽(monospaced)字体显示的。并非所有的代码片断都是完整的程序代码。为了简洁起见,我们经常会显示一些无法单独编译通过的代码片断或程序代码。如果妳想尝试运行这些代码的话,可以使用 fn main() { ... } 来将它们包围起来,同时要注意,确保它们没有引用那些并没有真正定义过的变量名。
警告:Rust是一门仍在开发当中的语言。请注意:该语言在未来可能会发生改变;该语言的实现中可能会有一些瑕疵;还有其它一些特性可能会被终止支持,会以引用文字的形式显示。
有两种方式可安装Rust编译器:用源代码编译;或者,下载针对妳的平台的预编译二进制程序或安装程序。在 安装页面 中包含一些链接,可下载Rust 的每日构建的二进制程序或者最新的当前主版本二进制程序。
对于Linux系统,安装页面上提供了二进制压缩包的下载链接。要使用二进制压缩包来安装Rust编译器的话,则,下载该二进制压缩包,解压它,然后执行压缩包根目录下的install.sh 脚本。
当前,快照版本的二进制程序会在多个平台上编译及通过测试:
Linux (2.6.18或更高版本,多种发行版), x86和x86-64
妳可能会发现它在其它平台上也能工作,但是,那些平台不是我们的首选("tier 1")编译支持环境,在首选环境中更有可能正常工作。
2.1 编译妳的第一个程序
Rust程序,按照习惯,其后缀名是.rs。例如,我们有一个文件hello.rs,其中包含以下代码:
fn main() {
println!("hello?");
}
提示:后面跟一个感叹号的标识符,例如这里的 println! ,是一个宏调用。我们日后再解释宏;现在,妳只需要记住写上感叹号就行了。
如果Rust编译器已经安装好了,那么,运行 rustc hello.rs ,就会产生一个名为 hello 的可执行程序。当妳执行这个可执行程序时,会做出妳所期待的动作。
当Rust编译器遇到错误时,会尝试输出有用的诊断信息。如果妳在程序代码中写错了某个东西(例如,将 println!改成某个不存在的宏),然后编译它的话,那么,妳会看到类似这样的错误信息:
hello.rs:2:5: 2:24 error: macro undefined: 'print_with_unicorns'
hello.rs:2 print_with_unicorns!("hello?");
^~~~~~~~~~~~~~~~~~~
对于一个Rust 程序,最简单的形式就是一个 .rs 文件,其中定义了一些类型和函数。如果其中包含有主( main )函数的话,则会被编译成一个可执行程序。Rust不允许在文件的顶级范围出现非声明型的代码:所有的动作语句(statements)都必须位于函数体中。Rust程序代码也可以被编译成库,并且被其它程序所包含,甚至可以被非Rust 代码的程序包含。
2.2 编辑Rust代码
在 src/etc/kate 目录下有针对Kate 的语法高亮文件。
假设妳曾经使用C语言家族的语言(C++、Java、JavaScript、C#或PHP)来编程的话,那么,Rust看起来会狠熟悉。代码是使用由花括号分隔的代码块组织起来的;有控制结构,用于执行不同的代码分支,以及循环,就像熟悉的 if 和 while 一样;函数调用是这样写的 myfunc(arg1, arg2) ;运算符也是同样书写的,并且与C语言中的运算符具有相同的优先级关系;注释也与C语言相同;模块名是使用双冒号(::)分隔的,与C++一样。
从表面上来看,最大的不同就是,控制结构(例如 if 和 while )头部的条件语句并不要求使用括号包围,而它们的执行语句体本身 必须 使用括号包围。单语句及未使用括号包围的执行语句体是不被允许的。
fn main() {
/* 一个简单的循环 */
loop {
// 一个奇怪的计算
if universe::recalibrate() {
return;
}
}
}
let关键字用于定义一个局部变量。变量默认是不可变的。要想定义一个可以日后对它重新赋值的局部变量的话,则使用 let mut 。
let hi = "hi";
let mut count = 0i;
while count < 10 {
println!("count is {}", count);
count += 1;
}
尽管Rust几乎可以永远推导出局部变量的类型,但是,妳仍然可以指定变量的类型。具体做法就是,在 let语句后面跟上一个冒号,然后写上类型名字。另一方面,静态变量,必须写上类型。
static MONSTER_FACTOR: f64 = 57.8;
let monster_size = MONSTER_FACTOR * 10.0;
let monster_size: int = 50;
局部变量可以屏蔽掉之前的声明,在上面的示例中妳就能看到: monster_size首先被声明为一个 f64 ,之后,又声明了一个 monster_size ,其类型为 int 。不过呢,如果妳真的编译这个示例的话,编译器会判断出第一个 monster_size 未被使用,因此发出一个警告(因为这种情景一般意味着某种代码错误)。如果妳确实要声明这种未被使用的变量的话,那么,可以在它们的名字前面加上一个下划线,这样就可以关掉该警告了,例如 let _monster_size = 50; 。
Rust中的标识符以字母或下划线开头,后面可以跟上任意的字母、数字或下划线组成的序列。我们倾向于使用的命名风格是:函数名、变量名和模块名使用小写,并且使用下划线来分隔单词以提升可读性,而类型名就使用骆驼风格来写。
let my_variable = 100;
type MyType = int; // 内置的原子类型名不是骆驼风格的
3.1 表达式和分号
尽管并非在所有代码中都是如此,但是,在 Rust 的语法和之前的语言(例如C语言)之间有个基本的区别。狠多在C语言中是语句的结构,在Rust 中却是表达式,这样代码可以写得更简洁。例如,妳可能会写这样的代码:
let price;
if item == "salad" {
price = 3.50;
} else if item == "muffin" {
price = 2.25;
} else {
price = 2.00;
}
但是,在Rust中,妳不需要将 price 这个名字重复这么多次:
let price =
if item == "salad" {
3.50
} else if item == "muffin" {
2.25
} else {
2.00
};
这两砣代码之间是等价的:它们根据不同的情况给 price 赋予不同的值。注意,在第二个代码片断的各个代码块中,没有分号。这一点狠重要:在花括号代码块中,如果最后一条语句的末尾不带分号,则,整个代码块都将拥有该语句的值。
换种方式说,在Rust 中,分号会导致 忽略一个表达式的值 。因此,如果在 if 分支中的代码是 { 4; } 的话,则,以上示例代码中只会将 price 赋值为 () (单元值(unit)或者说是空值(void))。但是,不带分号的话,每个分支都会有不同的值,而 price 就会拥有被执行的那个分支的值。
简单来说,所有不是声明语句(declaration)(声明语句包括:对于变量的 let语句;对于函数的 fn语句;以及任意的顶级命名元素,例如特征( traits )、枚举类型( enum types )和静态元素)的语句,都是表达式,包括函数体。
fn is_four(x: int) -> bool {
// 不需要写返回(return)语句。这个表达式的结果就会被用作返回值。
x == 4
}
3.2 基本类型的字面常量(literals)
有通用的带符号和无符号整数类型, int 和 uint ,以及8-、16-、32-和64-位的变种, i8 、 u16 ,等等。整数常量可以按照以下基数格式来写:十进制(144)、十六进制(0x90)、八进制(0o70)或二进制(0b10010000)。每个整数类型都有对应的常量后缀,用于指示该常量的类型: i表示int 、 u表示uint 、 i8表示i8 。
如果整数常量不带类型后缀的话,Rust会根据类型标记和周围代码中的函数特征来推导出该整数的类型。如果根本找不到任何类型信息的话,Rust会假设不带后缀的整数常量的类型为 int 。
let a = 1; // `a` 是 `int`
let b = 10i; // `b` 是 `int`,因为有`i`后缀
let c = 100u; // `c` 是 `uint`
let d = 1000i32; // `d` 是 `i32`
有两种浮点数类型: f32 和 f64 。浮点数的写法是 0.0 、 1e6 或 2.1e-4 。与整数类似,浮点数的常量会被推导为正确的类型。可使用 f32 和 f64后缀来指定常量的类型。
关键字 true 和 false表示bool 类型的常量。
字符类型,即 char类型,是4字节的统一码(Unicode)码位(codepoints),其常量值是使用单引号包围的,例如 'x' 。与C类似,Rust可以理解多个转义字符,也是使用反斜杠表示的,例如\n、\r和\t。字符串常量是以双引号包围的,以允许使用相同的转义字符,但不做其它的额外处理,这一点与PHP或终端脚本(shell)不同。
另一方面,原始字符串常量不对任何转义字符进行处理。写法是 r##"blah"## ,在开头的引号之前和结尾的引号之后,有同样数量的 # ,除了末尾的分隔符之外,其中可以包含任意的字符序列。日后将说明更多关于字符串的事情。
单元(unit)类型,写作 () ,只有一个值,也写作 () 。
3.3 操作符
Rust的操作符没有什么新鲜的。算术运算由 * 、 / 、 % 、 + 和 - (分别是乘法、除法、取余、加法和减法)表示。 - 还可以作为一个一元的前缀操作符,用于取相反数。与C语言一样,也支持按位计算的操作符 >> 、 << 、 & 、 | 和 ^ 。
注意, ! ,用于整数值的话,表示翻转所有的位(按位取反(NOT),类似于C 语言中的 ~ )。
比较操作符就是传统的 == 、 != 、 < 、 > 、 <= 和 >= 。短路(懒惰式)的逻辑操作符包括 && (逻辑与)和 || (逻辑非)。
对于编译期的类型转换,Rust使用二元的 as操作符来表示。它的左侧是一个表达式,右侧是一个类型名,如果存在有意义的转换方式的话,则会将该表达式的结果转换成指定的类型。通常情况下, as 只被用于转换基本的数值类型或指针,并且不可重载。 transmute 可用于进行非安全的转换,像C语言那样在相同尺寸的类型之间做转换。
let x: f64 = 4.0;
let y: uint = x as uint;
assert!(y == 4u);
3.4 语法扩展
语法扩展 ,指的是一些特殊的语法形式,它们没有被内置在语言中,而是由一些库来提供。为了让代码阅读者知道某个命名对象表示的是一个语法 扩展,我们让所有的语法扩展的名字都以 !结尾。标准库中定义了若干语法扩展,其中最有用的就是 format! ,这是一个类似于 sprintf 的文本内容格式化函数,妳经常会在示例代码中见到它,另外还有与它相关的一些宏: print! 、 println! 和 write! 。
format! 的语法是从Python 借用过来的,但也包含了 printf 所拥有的狠多原则。与printf 不同的是, format! 在发现格式字符串中的类型指示符与参数的类型不匹配时,会报告一个编译期错误。
// `{}` 会按照该类型的“默认格式”来输出
println!("{} is {}", "the answer", 43i);
extern crate debug;
// `{:?}` 可用于输出任意类型,但是要求链接到`debug`这个模块(crate)
println!("what is this thing: {:?}", mystery_object);
妳可以使用宏系统来定义妳自己的语法扩展。欲知详情,则阅读 宏教程 。注意,宏的定义目前是一个非稳定特性。
4.1 条件语句
我们已经多次见到 if 表达式了。重新说明一下,花括号是必需的, if语句中的 else子句是可选的,多个 if/else结构可串连在一起:
if false {
println!("that's odd");
} else if true {
println!("right");
} else {
println!("neither true nor false");
}
传递给 if 结构的条件表达式的类型 必须 是 bool (不会进行隐式的类型转换)。如果各个分支代码块拥有结果值的话,那么,各个分支代码块的结果值的类型必须是相同的:
fn signum(x: int) -> int {
if x < 0 { -1 }
else if x > 0 { 1 }
else { 0 }
}
4.2 模式匹配
Rust的 match结构,是对于C 语言的 switch 结构的一个通用化、简洁化的版本。妳在其中写上一个比较值和多个 分支 ,每个分支都是以一个模式来标识的,并且包含着当模式被匹配时执行的代码。
let my_number = 1;
match my_number {
0 => println!("zero"),
1 | 2 => println!("one or two"),
3..10 => println!("three to ten"),
_ => println!("something else")
}
与C语言不同的是,分支之间不存在“接盘”("falling through")关系:只有一个分支会被执行,并且,当它结束时,不需要显式地跳出(break)结构体。
match结构中的分支,由以下部分组成:一个模式( pattern ),接下来是胖箭头 => ,接下来是动作(action)(表达式)。各个分支之间是由逗号分隔的。通常会使用便利写法,用代码块表达式来包括各个分支,这种情况下,可以省掉逗号,如下例所示。字面常量是有效的模式,所匹配的就是它们的字面值。单个分支可以匹配到多个不同的模式,这些模式之间使用管道操作符(|)组合起来,只要其中的每个模式都是绑定到同一组变量(参考下文的“解构”("destructuring"))就可以了。数字常量范围模式,可使用两个小数点来表示,例如 M..N 。下划线(_)是一个通配模式,可匹配任意的单个值。(..)是另一个通配模式,可匹配一个枚举( enum )变量中的一个或多个字段。
match my_number {
0 => { println!("zero") }
_ => { println!("something else") }
}
match结构必须是无遗漏的( exhaustive ):必须有一个能够涵盖所有可能情况的分支。举个例子,在之前这个示例中,如果省略掉含有通配符模式的分支的话,则类型检查器会拒绝这个示例编译通过。
模式匹配的一种强大应用就是 解构 :在模式匹配的过程中将将变量名绑定到特定数据类型的内容上。
注意:以下代码用到了在5.3 小节中才会讲到的元组(tuples)((f64, f64))。当前,妳可以将元组理解成由多个元素组成的列表。
use std:: f64;
fn angle(vector: (f64, f64)) -> f64 {
let pi = f64::consts::PI;
match vector {
(0.0, y) if y < 0.0 => 1.5 * pi,
(0.0, _) => 0.5 * pi,
(x, y) => (y / x).atan()
}
}
写在模式中的变量名,能够匹配到任意值, 并且 ,会在该分支的动作代码中,将该变量的值绑定到所匹配的值。因此, (0.0, y) 会匹配到任意一个其第一个元素为零的元组,并将 y绑定到第二个元素上。 (x, y)能够匹配任意一个含有两个元素的元组,并将两个元素分别绑定到对应的变量上。(0.0,_)会匹配到任意一个其第一个元素为零的元组,并且忽略掉第二个元素。
子模式也可以绑定到变量,写法是 variable @ pattern 。例如:
match age {
a @ 0..20 => println!("{} years old", a),
_ => println!("older than 21")
}
每个match分支都可以有一个保护子句(写作if EXPR),称作模式护卫( pattern guard ),是一个 bool 类型的表达式,它用于决定,当该模式被匹配时,是否要真正执行这个分支。由模式所绑定的变量也处于这个护卫表达式的作用域中。在 angle示例的第一个分支中就展示了模式护卫的用法。
妳已经见到简单的 let 绑定代码了,但是, let 实际上比妳想象的还要美妙。它也支持模式的解构。例如,妳可以按照下面这样来写代码,从一 个元组中提取字段值,以同时声明两个变量:a和b。
let (a, b) = get_tuple_of_two_ints();
Let 语句的绑定只能接受确定的( irrefutable )模式:也就是说,那种一定不会匹配失败的模式。这就使得, let无法将字面常量和大部分枚举变量作为绑定模式,因为,这种模式大部分都不是确定的。例如,以下代码无法通过编译:
let (a, 2) = (1, 2);
4.3 循环
while表示的是一个循环,嘦它的判断条件表达式(类型必须是逻辑型( bool ))的值为真( true ),循环就会一直进行下去。在循环体中,使用关键字 break 可以终止整个循环,使用 continue 可以终止当前这次迭代,而开始下一次迭代。
let mut cake_amount = 8;
while cake_amount > 0 {
cake_amount -= 1;
}
loop表示一个无限循环,并且倾向于用来替代 while true :
let mut x = 5u;
loop {
x += x - 3;
if x % 5 == 0 { break; }
println!("{}", x);
}
这段代码会输出一个怪异的数字序列,当它找到一个能被5整除的数时,就会停止循环。
还有一个for循环,可对指定范围内的数字进行迭代:
for n in range(0u, 5) {
println!("{}", n);
}
以上代码片断会输出从0开始,小于5的整数。
更一般地说,for循环对于任何一个实现了迭代器(Iterator)特征(trait)的东西都有效。数据结构们可以提供一种或多种方法,用于返回能够对它们的内容进行迭代的迭代器。例如,字符串支持以多种方式对它们的内容进行迭代:
let s = "Hello";
for c in s.chars() {
println!("{}", c);
}
以上代码片断会将"Hello"的各个字符按照竖向输出,每个字符后面跟一个换行。
5.1 结构体(Structs)
Rust中的结构体必须在被使用之前用 struct 语法来声明: struct Name { field1: T1, field2: T2 [, ...] } ,其中的 T1 、 T2 、...表示类型。要构造一个结构体的话,则使用相同的语法,但是要省略掉 struct :例如: Point { x: 1.0, y: 2.0 } 。
结构体与C 语言中的结构体非常类似,而且,在内存中的布局也与C语言相同(所以妳可以通过C 语言代码来读取一个Rust 结构体,反之也可以)。使用小数点操作符来访问结构体的字段,例如 mypoint.x 。
struct Point {
x: f64,
y: f64
}
结构体的可变性满足继承关系("inherited mutability"),这就意味着,如果该结构体本身是可变的,则它的任何字段都是可变的。
如果这种类型的某个值(例如mypoint)位于一个可变的内存位置,那么,妳可以这样写代码 mypoint.y += 1.0 。但是对于一个不可变的位置,像这样写代码就会导致一个类型错误。
let mut mypoint = Point { x: 1.0, y: 1.0 };
let origin = Point { x: 0.0, y: 0.0 };
mypoint.y += 1.0; // `mypoint` 是可变的,它的字段也是可变的
origin.y += 1.0; // 错误:对不可变的字段进行赋值
match模式会对结构体进行解构。基本语法是 Name { fieldname: pattern, ... } :
match mypoint {
Point { x: 0.0, y: yy } => println!("{}", yy),
Point { x: xx, y: yy } => println!("{} {}", xx, yy)
}
一般来说,结构体中的字段名字不需要保持它们在类型定义中的顺序。如果妳并不是对某个结构体的所有字段感兴趣的话,那么,可在结构体模式的末尾写上 , .. (例如Name { field1, .. }) 以表明妳要忽略其它的字段。另外,结构体的字段还有一种简写形式,即,简单地将字段名字复用为绑定名字。
match mypoint {
Point { x, .. } => println!("{}", x)
}
5.2 枚举
枚举是有多种替代表达方式的类型。一个简单的枚举中会定义一到多个常量,每个常量都是相同类型的:
enum Direction {
North,
East,
South,
West
}
这个枚举中的每个变种都有一个唯一的、常量的整数标识值。如果未给某个变种指定标识值的话,则,它的值会默认取为前一个变种的值加1.如果第一个变种没有指定标识值的话,则它会默认是0。例如,上例中的 North 为 0 , East 为 1 , South 为 2 , West 为 3 。
如果某个枚举类型的标识值是简单的整数值的话,那么,妳可以使用类型转换操作符 as ,将某个变种转换成它的标识值对应的整型变量( int ):
println!( "{} => {}", North, North as int );
可以将标识值指定为特定的常量值:
enum Color {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff
}
变种不一定非要具有简单的值;它们的值可以设置得更复杂:
enum Shape {
Circle(Point, f64),
Rectangle(Point, Point)
}
对于这种类型的某个值,它或者是一个 Circle ,其中包含一个 Point 结构体和一个 f64 值,或者是一个 Rectangle ,其中包含两个 Point 结构体。对于这种值,在运行时刻的表示当中,会有一个标识符,表明实际是使用哪种形式来储存该值的,这就类似于C 语言中的“带标记的联合体”("tagged union")模式,但是有着更好的静态保护措施。
这个声明语句,定义了一个名为 Shape 的类型,它可用来指代这两种形状,同时定义了两个函数 Circle 和 Rectangle ,可用来构造这种类型的值。要想创建一个新的Circle 变量,则这样写 Circle(Point { x: 0.0, y: 0.0 }, 10.0) 。
所有的这些变种构造函数都可以用作模式。访问某个枚举类型实例的内容的唯一方式,就是在模式匹配中进行解构。例如:
use std::f64;
fn area(sh: Shape) -> f64 {
match sh {
Circle(_, size) => f64::consts::PI * size * size,
Rectangle(Point { x, y }, Point { x: x2, y: y2 }) => (x2 - x) * (y2 - y)
}
}
使用单独的 _ 来忽略单个的字段。使用以下写法来忽略某个变种的所有字段: Circle(..) 。不带字段的(Nullary)枚举模式,则不带括号:
fn point_from_direction(dir: Direction) -> Point {
match dir {
North => Point { x: 0.0, y: 1.0 },
East => Point { x: 1.0, y: 0.0 },
South => Point { x: 0.0, y: -1.0 },
West => Point { x: -1.0, y: 0.0 }
}
}
枚举类型的变种还可以是结构体。例如:
use std::f64;
enum Shape {
Circle { center: Point, radius: f64 },
Rectangle { top_left: Point, bottom_right: Point }
}
fn area(sh: Shape) -> f64 {
match sh {
Circle { radius: radius, .. } => f64::consts::PI * square(radius),
Rectangle { top_left: top_left, bottom_right: bottom_right } => {
(bottom_right.x - top_left.x) * (top_left.y - bottom_right.y)
}
}
}
注意:编译器中对于这种语法的支持,目前是由 #[feature(struct_variant)] 指令来控制的。参考语言手册以了解更多关于这类指令的说明。
5.3 元组
Rust 中的元组与结构体的行为狠类似,唯一的区别就是元组的字段没有名字。因此,妳无法使用小数点来访问到它们的字段。元组可以具有除了0(当然,妳愿意的话,可以将单元符号 () 看作一个空的元组)以外的元数(arity)(元素个数)。
let mytup: (int, int, f64) = (10, 20, 30.0);
match mytup {
(a, b, c) => println!("{}", a + b + (c as int))
}
5.4 元组结构体
Rust中还有元组结构体,其行为既像结构体也像元组,不同之处就在于,元组结构体本身具有名字(所以Foo(1, 2) 与 Bar(1, 2) 的类型不同),而元组结构体中的字段没有名字。
例如:
struct MyTup(int, int, f64);
let mytup: MyTup = MyTup(10, 20, 30.0);
match mytup {
MyTup(a, b, c) => println!("{}", a + b + (c as int))
}
还有一种特殊的元组结构体用法,就是,只给它一个字段,这种用法有时被称为“新类型”( "newtypes")(由Haskell 的"newtype"特性得来)。这种写法被用于定义新的类型,使得,新的类型名不仅仅只是已有某个类型的同义词,而是一种不同的类型。
struct GizmoId(int);
这种类型可以用来区分出那些其内部数据类型完全相同却又不能混用的数据。
struct Inches(int);
struct Centimeters(int);
以上这个定义,使得程序代码中可以用简单的方式来避免混用不同单位的数值。它们的整数值可通过模式匹配来提取:
let length_with_unit = Inches(10);
let Inches(integer_length) = length_with_unit;
println!("length is {} inches", integer_length);
我们已经多次见到函数定义了。与其它的静态声明(例如 type )相同的是,函数可以声明在顶级位置,也可以声明在其它函数内部(或者声明在模块的内部,我们日后再说)。 fn关键字用于定义一个函数。函数有一个参数列表,用括号包围起来,各个参数之间用逗号分隔,每个参数都是 name: type 的形式。箭头 ->用于将参数列表和函数的返回类型分隔开来。
fn line(a: int, b: int, x: int) -> int {
return a * x + b;
}
return关键字用于立即从函数体中返回。后面可以带上一个想要返回其值的表达式。函数还可以通过另一种方式来返回一个值,就是,让它的顶级代码块变成一个表达式。
fn line(a: int, b: int, x: int) -> int {
a * x + b
}
最好是按照Rust 的风格来像这样返回一个值,而不是显式地使用 return 。当妳想在某个函数中较早的地方返回时,可利用 return 。对于那种不返回值的函数,我们说它是返回单元对象 () ,这种情况下,返回类型和返回值都可以从定义中省略。以下两个函数是等价的。
fn do_nothing_the_hard_way() -> () { return (); }
fn do_nothing_the_easy_way() { }
使用分号来作为函数体的结尾的话,就相当于返回 () 。
fn line(a: int, b: int, x: int) -> int { a * x + b }
fn oops(a: int, b: int, x: int) -> () { a * x + b; }
assert!(8 == line(5, 3, 1));
assert!(() == oops(5, 3, 1));
就像 match 表达式和 let 绑定一样,函数的参数也支持模式解构。与 let 一样,参数的模式必须是确定可匹配的,在以下示例中,提取了一个元组的第一个值并且返回了它。
fn first((value, _): (int, f64)) -> int { value }
析构函数 是一个特殊的函数,用于当某个对象不再能够被访问时清理它所使用的资源。析构函数可用来释放资源,例如文件、套接字和堆内存。
当对象的析构函数被调用之后,这些对象便再也无法被访问到,所以不可能因为访问到已被释放的资源而产生动态错误。当某个任务失败时,该任务中所有对象的析构函数都会被调用。
box操作符会在堆上分配内存:
{
// 一个在堆上申请的整数
let y = box 10;
}
// 一旦`y`离开了作用域,析构函数就会释放堆内存
Rust包含了用于在堆上申请内存的语法,因为这是一种常见用法,但是,相同的语义可通过一个带有自定义析构函数的类型来实现。
Rust使用以下方式处理从属关系:将某个对象的生命周期托管给一个变量或者一个任务内部的垃圾收集器。一个对象的拥有者要负责管理该对象的生命周期,在适当的时候调用它的析构函数,并且拥有者还决定着该对象是否可变。
从属关系是递归的,所以,可变性也是递归地继承的,并且,析构函数会销毁从属关系树中拥有的所有的对象。变量就是顶级的拥有者,当它们超出作用域时,便会销毁它们所拥有的对象。
// 这个结构体拥有着`x`和`y`字段中包含的对象
struct Foo { x: int, y: Box<int> }
{
// `a`是这个结构体的拥有者,因此也是结构体中各个字段的拥有者
let a = Foo { x: 5, y: box 10 };
}
// 当`a`离开作用域时,结构体中对应于`Box<int>`字段的析构函数会被调用
// `b`是可变的,于是它的可变性也被它所拥有的对象继承了
let mut b = Foo { x: 5, y: box 10 };
b.x = 10;
如果某个对象中不包含任何的非发送(non-Send)类型,并且自身只包含单个从属关系树,那么,它本身会被赋予发送( Send )特征,这允许它被在各个任务之间来回发送。自定义的析构函数只能在那些具有 Send 特征的类型上直接实现,但是,非发送类型中仍然可以包含带有自定义析构函数的类型。有两个不具有 Send 特征的例子, Gc<T> 和 Rc<T> ,它们是共享从属关系的类型。
enum天生就适合于描述链表,因为它可以表达出一个 List 类型,该类型的实例或者是列表的结尾(Nil),或者是另一个节点(Cons)。其中 Cons 变种的完整定义需要仔细想一想。
enum List {
Cons(...), // 对于List中下一个元素的一个不完整定义
Nil // List的末尾
}
Cons 的一种显而易见的定义就是,包含着列表中的一个元素以及下一个 List 节点。可惜的是,这样会导致一个编译错误。
// error: illegal recursive enum type; wrap the inner value in a box to make it
// representable
enum List {
Cons(u32, List), // 一个元素(`u32`)及链表中的下一个节点
Nil
}
这个错误消息与Rust 对于内存布局的精确控制有关,要想解决这个问题的话,需要先了解包装( boxing )这个概念。
9.1 包装盒(Boxes)
Rust 中,一个值是直接存储在其拥有者内部的。如果一个结构体( struct )中包含了4个 u32 字段,那么,它将达到单个 u32 的4倍那么大。
use std::mem::size_of; // bring `size_of` into the current scope, for convenience
struct Foo {
a: u32,
b: u32,
c: u32,
d: u32
}
assert_eq!(size_of::<Foo>(), size_of::<u32>() * 4);
struct Bar {
a: Foo,
b: Foo,
c: Foo,
d: Foo
}
assert_eq!(size_of::<Bar>(), size_of::<u32>() * 16);
我们在之前的尝试中,所定义的 List 类型里,在 Cons 中直接包含了一个 u32 和一个 List ,这使得它的尺寸最少是二者尺寸之和。这个类型是无效的,因为,算起来,它的尺寸是无限大!
一个有从属的包装盒( owned box )(Box ,位于 std::owned 模块中),使用于动态内存分配功能,以实现了一种不变性,即,其尺寸永远是一个指针的大小,而无论所包含的类型是什么。利用这一点,可以创建出一个有效的 List 定义:
enum List {
Cons(u32, Box<List>),
Nil
}
定义一种像这样的递归数据结构,便是有从属的包装盒的典型示例。跟一个未包装的值类似,一个有从属的包装盒具有单一的所有者,因此被限制于只能创建出树形的数据结构。
研究一下我们的 List 类型的一个实例:
let list = Cons(1, box Cons(2, box Cons(3, box Nil)));
它表达的是一棵有从属的由值组成的树,整棵树中的可变性是一直继承下去的,树中的各个值会随着所有者的销毁而销毁。由于上面例子中的 list 变量是不可变的,所以,整个链表都是不可变的。内存分配本身是由包装盒(box)来实现的,而所有者保有指向它的指针:
注意:以上示意图展示了该枚举对象的逻辑内容。该枚举对象的实际内存布局可能并不是如此。例如,对于以上所示的 List枚举类型,Rust能够确保,在实际的结构中不会有枚举标记字段。参考语言手册,以了解更多信息。
有主的包装盒,是带有析构函数的类型的常见例子。当该包装盒被销毁时,所分配的内存也会被释放。
9.2 移动(Move)语义
在做以下事情时,Rust使用的是浅复制:参数传递、赋值和函数的返回值。在将这个 List 到处传递时,只会对指向该包装盒的指针做复制,而不是隐式地进行堆内存分配。
let xs = Cons(1, box Cons(2, box Cons(3, box Nil)));
let ys = xs; // 浅式复制`Cons(u32, pointer)`
在Rust中,对于带有析构函数的类型(例如 List )的浅复制,被认为是 转移了值的所有权 。当某个值被移动之后,源地址就不再可用,除非重新初始化。
let mut xs = Nil;
let ys = xs;
// 在此处尝试使用`xs`的话,会导致错误
xs = Nil;
// `xs`可被再次使用
只有那些未被移动的变量,才可能被调用其析构函数,另存为析构函数只会被调用一次。
可使用库中所定义的 clone 方法来避免移动:
let x = box 5i;
let y = x.clone(); // `y`是一个新分配的包装盒
let z = x; // 没有分配新的内存,`x`不再可用
clone方法是由 Clone 特征提供的,也可被我们的 List 类型继承。我们日后会详细说明特征。
#[deriving(Clone)]
enum List {
Cons(u32, Box<List>),
Nil
}
let x = Cons(5, box Nil);
let y = x.clone();
// `x`仍然可用!
let z = x;
// 现在,不可用了,因为它已被移动
通过将一个值移动到一个新的所有者当中,可以改变它的可变性:
let r = box 13;
let mut s = r; // 包装盒变为可变的
*s += 1;
let t = s; // 包装盒变为不可变的
有一种简单的方法,可以定义出能够在 List 类型之前拼接内容的函数,那就是,利用移动语义:
enum List {
Cons(u32, Box<List>),
Nil
}
fn prepend(xs: List, value: u32) -> List {
Cons(value, box xs)
}
let mut xs = Nil;
xs = prepend(xs, 1);
xs = prepend(xs, 2);
xs = prepend(xs, 3);
然而,这并不是一个狠灵活的 prepend 定义,因为,它会导致将一个列表的所有权传入,而不是就地修改该数据结构。
9.3 引用
对于List ,其相等性判断函数的特征显然是这样的:
fn eq(xs: List, ys: List) -> bool { /* ... */ }
但是,这会导致两个被比较的列表都移动到函数内部。在比较列表的过程中,并不需要拥有它们的所有权,因此,这个函数应当使用引用( references )(&T)。
fn eq(xs: &List, ys: &List) -> bool { /* ... */ }
引用,是对于一个值的一种 不带所有权 的视图。可使用 & (取地址)操作符来获取一个引用。可使用 * 操作符来解引用。在模式中,例如 match 表达式的分支中,可使用 ref 关键字来以引用的方式绑定到某个变量名,而不是按照取值的方式来绑定。利用引用,可按照如下方式定义一个递归地判断相等性的函数:
fn eq(xs: &List, ys: &List) -> bool {
// 同时比较两个列表中的下一个节点。
match (xs, ys) {
// 如果我们同时遇到了两个列表的末尾,则它们相等。
(&Nil, &Nil) => true,
// 如果两个列表中的当前元素相等,则继续比较。
(&Cons(x, box ref next_xs), &Cons(y, box ref next_ys))
if x == y => eq(next_xs, next_ys),
// 如果当前的两个元素不相等,则两个列表不相等。
_ => false
}
}
let xs = Cons(5, box Cons(10, box Nil));
let ys = Cons(5, box Cons(10, box Nil));
assert!(eq(&xs, &ys));
注意:Rust并不确保自己能够进行 尾调用 优化,但是,在开启了优化选项的情况下,LLVM可以处理类似上面例子这样的简单情况。
9.4 针对其它数据类型的列表
目前,我们这个 List 类型永远是一个容纳32 位无符号整数的列表。利用Rust中对于泛型的支持,我们可以把这个类型变成支持任意元素类型的列表。
之前定义中的 u32 可替换成一个类型参数:
注意:以下代码中引入了泛型,这个东西我们日后在一个单独的小节中介绍。
enum List<T> {
Cons(T, Box<List<T>>),
Nil
}
现在,之前的那种由 u32 组成的 List ,可以表示为 List<u32> 。 prepend 函数的定义也需要更新了:
fn prepend<T>(xs: List<T>, value: T) -> List<T> {
Cons(value, box xs)
}
像这样定义的泛型函数和类型,等价于针对每种类型参数单独实现一个版本。
感谢类型推导,在使用泛型 List<T> 的时候看起来跟之前没什么区别:
let mut xs = Nil; // 未知类型!它是一个`List<T>`,但是此时的`T`有可能是任意类型。
xs = prepend(xs, 10); // 此时,编译器将`xs`的类型推导为`List<int>`。
xs = prepend(xs, 15);
xs = prepend(xs, 20);
以上代码展示了,类型推导使得大部分情况下都不需要写具体的数据类型。以上代码等价于以下明确带有类型的代码:
let mut xs: List<int> = Nil::<int>;
xs = prepend::<int>(xs, 10);
xs = prepend::<int>(xs, 15);
xs = prepend::<int>(xs, 20);
在声明代码中,本语言使用 Type<T, U, V> 来描述一组类型参数,但是在表达式代码中,使用的是 identifier::<T, U, V> ,以消除与 < 操作符之间的歧义。
9.5 定义泛型列表的相等性判断函数
泛型函数会在定义处就进行类型检查,因此,该类型的任何必要的属性都必须预先指定。在我们之前定义的列表相等性比较函数中,依靠了该元素拥有 == 操作符的特性,还利用了 u32 类型没有析构函数的这一点,以实现在不移动所有权的情况下进行复制。
我们可以加上特征包围符( trait bound )和 PartialEq 特征,来要求该类型必须实现 == 操作符。还要再加入两个 ref标记,以避免将元素类型移动出来:
fn eq<T: PartialEq>(xs: &List<T>, ys: &List<T>) -> bool {
// 对两个列表中的下一个节点进行匹配。
match (xs, ys) {
// 如果我们到达了两个列表的末尾,那么它们相等。
(&Nil, &Nil) => true,
// 如果两个列表中的当前元素相等,则继续。
(&Cons(ref x, box ref next_xs), &Cons(ref y, box ref next_ys))
if x == y => eq(next_xs, next_ys),
// 如果当前元素不相等,则两个列表不相等。
_ => false
}
}
let xs = Cons('c', box Cons('a', box Cons('t', box Nil)));
let ys = Cons('c', box Cons('a', box Cons('t', box Nil)));
assert!(eq(&xs, &ys));
现在是为我们的列表类型实现 PartialEq 特征的好机会,这将使得 == 和 != 操作符对于我们的类型可用。我们需要针对 PartialEq 特征提供一个实现( impl ),还需要定义一个 eq 方法。在方法体中, self 这个参数会引用到我们当前为其实现方法的类型的实例。
impl<T: PartialEq> PartialEq for List<T> {
fn eq(&self, ys: &List<T>) -> bool {
// 对两个列表中的下一个节点进行匹配。
match (self, ys) {
// 如果我们到达了两个列表的末尾,那么它们相等。
(&Nil, &Nil) => true,
// 如果两个列表中的当前元素相等,则继续。
(&Cons(ref x, box ref next_xs), &Cons(ref y, box ref next_ys))
if x == y => next_xs == next_ys,
// 如果当前元素不相等,则两个列表不相等。
_ => false
}
}
}
let xs = Cons(5i, box Cons(10i, box Nil));
let ys = Cons(5i, box Cons(10i, box Nil));
// 以下所调用的方法是PartialEq特征的一部分,我们已经针对我们的链表实现了它们。
assert!(xs.eq(&ys));
assert!(!xs.ne(&ys));
// PartialEq特征还使得我们能够使用简短的中缀操作符。
assert!(xs == ys); // `xs == ys`是`xs.eq(&ys)`的缩写
assert!(!(xs != ys)); // `xs != ys`是`xs.ne(&ys)`的缩写
对于被拥有的包装盒的最常见用法,就是,创建出递归的数据结构,例如二分查找树。Rust中的基于特征的泛型系统(本教程中日后会讲到)通常被用于进行静态调度,但是也可以通过包装盒来提供动态调度功能。不同类型的值可能具有不同的尺寸,但是,包装盒能够通过自己所提供的间接层来 抹去 这种不同。
在特殊情况下,这种间接层还能带来性能上的提升或内存上的节省,因为它让变量的值的尺寸变小了。但是,在可以的情况下,还是应当优先使用未包装的值。
注意,通过包装盒来返回巨大的未包装的值,是没有必要的。巨大的值是由一个隐藏的输出参数来返回的,而在哪里储存返回值呢,这个决定应当由调用者来作出:
fn foo() -> (u64, u64, u64, u64, u64, u64) {
(5, 5, 5, 5, 5, 5)
}
let x = box foo(); // 分配一个包装盒,然后将那些整数直接写入
除了与尺寸相关的属性之外,一个被拥有的包装盒,它继承了其拥有者的可变性和生命周期,因而其行为与一个普通的值相同:
let x = 5; // 不可变
let mut y = 5; // 可变
y += 2;
let x = box 5; // 不可变
let mut y = box 5; // 可变
*y += 2; // 此处需要使用`*`操作符来访问到其中所包装的值
对于被拥有的包装盒来说,该包装盒的拥有者便是所指向的内存的拥有者,而引用却与它相反,引用是不带所有权的——它们是“借来的”。妳可以借用到针对任意对象的引用,而编译器会确保,该引用不会在该对象的生命周期之外存在。
举个例子,研究一下下面这个简单的结构体类型, Point:
struct Point {
x: f64,
y: f64
}
我们可以利用这个简单的定义来通过狠多方式分配空间点(points)对象内存。例如,下面的代码中,两个局部变量中都包含着一个点,但是它们是在不同的地方分配的:
let on_the_stack : Point = Point { x: 3.0, y: 4.0 };
let on_the_heap : Box<Point> = box Point { x: 7.0, y: 9.0 };
假设我们要写一段代码,计算任意两个点之间的距离,而不用管它们都是储存在哪里。一种实现方式就是,定义一个函数,它需要两个类型为点的参数——也就是说,它以值的方式来使用那两个点。但是,这会导致,当我们调用该函数时,两个点被复制。对于点来说,这可能没什么,但是,频繁地复制就狠费资源。所以我们要定义一个函数,它以指针来访问那两个点。我们可以利用引用来实现这一点:
fn compute_distance(p1: &Point, p2: &Point) -> f64 {
let x_d = p1.x - p2.x;
let y_d = p1.y - p2.y;
(x_d * x_d + y_d * y_d).sqrt()
}
现在我们可以用多种方式来调用 compute_distance() :
compute_distance(&on_the_stack, on_the_heap);
此处的 & 操作符用于获取变量 on_the_stack 的地址;这是因为, on_the_stack 的类型是 Point (即,是一个结构体值),所以我们需要取它的地址才能获取到它的引用。我们也把这个操作称为 借用 局部变量 on_the_stack ,因为我们正在创建一个别名:即,另一条能够访问到同一个数据的路径。
然而,对于 owned_box 呢,不需要显式地做什么操作。编译器会自动地将一个包装盒 box point 转换成一个引用 &point 。这是另一种借用形式;在这种情况下,这个被拥有的包装盒中的内容被借出。
当某个值被借出时,会限制妳能够对原始值所做的事。例如,如果某个变量的内容被借出,那么,妳不能将那个变量发送到另一个任务中,妳也不能做那些可能引起借出的值被释放或者被改变类型的操作。妳可以通过直觉来理解这个规则:对于被借出的值,在妳继续随意使用它之前,必须等待它被返回(即,对应的引用超出了作用域)。
若想深入了解引用和生命周期,则阅读 引用及生命周期指南 。
11.1 冻结
当妳把某个对象的&指针借出时,就冻结了所指向的对象,并且开始阻止变化的发生——即使该对象被声明为 mut 也是一样。冻结( Freeze )对象的动作是在编译期强制静态生效的。对于非冻结(non-Freeze )的类型,有一个例子,就是 RefCell<T> 。
let mut x = 5;
{
let y = &x; // `x`被冻结。它不可以被修改或者重新赋值。
}
// `x`被解冻。
Rust使用一元的星号操作符(*)来访问某个包装盒或者指针的内容,与C 语言类似。
let owned = box 10;
let borrowed = &20;
let sum = *owned + *borrowed;
被解引用的可变的指针,可以出现在赋值语句的左侧。这种赋值语句会修改该指针指向的值。
let mut owned = box 10;
let mut value = 20;
let borrowed = &mut value;
*owned = *borrowed + 100;
*borrowed = *owned + 1000;
指针具有狠高的操作符优先级,但是比用来访问字段或方法的小数点操作符的优先级低。有些时候,这样一种优先级顺序会使得代码中充满了括号,狠烦人。
let start = box Point { x: 10.0, y: 20.0 };
let end = box Point { x: (*start).x + 100.0, y: (*start).y + 100.0 };
let rect = &Rectangle(*start, *end);
let area = (*rect).area();
为了避免弄出这么丑的代码,小数点操作符会对接收者(小数点左侧的那个值)进行 自动指针解引用 ,所以,在大部分情况下,不需要地显式地对接收者进行解引用。
let start = box Point { x: 10.0, y: 20.0 };
let end = box Point { x: start.x + 100.0, y: start.y + 100.0 };
let rect = &Rectangle(*start, *end);
let area = rect.area();
妳可以写出一个自动对任意数量的指针进行解引用的表达式。例如,如果妳愿意的话,甚至可以写出如下面这样丧心病狂的代码
let point = &box Point { x: 10.0, y: 20.0 };
println!("{:f}", point.x);
下标操作符([])也会自动解引用。
向量是一块连续的内存,其中包含0个或多个相同类型的值。Rust还支持对向量的引用类型,称作切片(slices),它是对于某块内存的一个视图,由一个指针和一个长度来表示。
字符串表示的是由 u8 组成的向量,并且确保其中包含着有效的UTF-8序列。
固定尺寸的向量,是一块未包装的内存,并且元素的长度也是它的类型中的一部分。一个固定尺寸的向量,拥有着它所包含的元素,所以,若该向量是可变的,则其中的元素也是可变的。不存在固定尺寸的字符串。
// 一个固定尺寸的向量
let numbers = [1i, 2, 3];
let more_numbers = numbers;
// 固定尺寸的向量,其类型写作`[Type, ..length]`
let five_zeroes: [int, ..5] = [0, ..5];
唯一向量(unique vector)的尺寸是动态变化的,并且还有一个析构函数,用于清理之前在堆上分配的内存。唯一向量拥有着它所包含的元素,所以,若该向量是可变的,则其中的元素也是可变的。
use std::string::String;
// 一个尺寸可变的向量(唯一向量)
let mut numbers = vec![1i, 2, 3];
numbers.push(4);
numbers.push(5);
// 唯一向量的类型,写作`Vec<int>`
let more_numbers: Vec<int> = numbers.move_iter().map(|i| i+1).collect();
// 因为移动语义,原始的`numbers`值不再可用。
let mut string = String::from_str("fo");
string.push_char('o');
切片与固定尺寸的向量类似,但是长度并不是它的类型中的一部分。它们只是指向某块内存,并不具有那些元素的所有权。
// 一个切片
let xs = &[1, 2, 3];
// 切片的类型写作`&[int]`
let ys: &[int] = xs;
// 将其它向量类型转换成切片
let three = [1, 2, 3];
let zs: &[int] = three;
// 普通的字符串常量,是一个不可变的字符串切片
let string = "foobar";
// 字符串切片的类型写作`&str`
let view: &str = string.slice(0, 3);
就像存在着可变的引用一样,可变的切片也存在。但是,不存在可变的字符串切片。字符串是对统一码(Unicode)码位(code points)的多字节编码(UTF-8),所以,不能在无法改变长度的情况下随意地改变它们的内容。
let mut xs = [1, 2, 3];
let view = xs.mut_slice(0, 2);
view[0] = 5;
// 可变切片,其类型写作`&mut [T]`
let ys: &mut [int] = &mut [1, 2, 3];
方括号表示针对切片或固定尺寸向量的下标操作:
let crayons: [Crayon, ..3] = [BananaMania, Beaver, Bittersweet];
match crayons[0] {
Bittersweet => draw_scene(crayons[0]),
_ => ()
}
切片和固定尺寸的向量可使用模式匹配来解构:
let numbers: &[int] = &[1, 2, 3];
let score = match numbers {
[] => 0,
[a] => a * 10,
[a, b] => a * 6 + b * 4,
[a, b, c, ..rest] => a * 5 + b * 3 + c * 2 + rest.len() as int
};
向量和字符串都支持一些有用的 方法 ,它们都定义在 std::vec 、 std::slice 和 std::str 中。
利用所有权特性,可以清晰地描述树型数据结构,而引用又提供了不带所有权的指针。然而,我们经常会需要有更灵活的使用方式,而Rust也提供了一些方式,可让我们避开严格的单亲所有权特性。
标准库中提供了 std::rc::Rc 这个指针类型,用于表达对于某个以引用计数来维护的包装盒的 共享式所有权 。一旦所有的 Rc指针离开其作用域,该包装盒及它所包含的值就被销毁。
use std::rc::Rc;
// 通过带引用计数特性的包装盒所分配的一个固定尺寸的数组
let x = Rc::new([1i, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let y = x.clone(); // 又来了一位所有者
let z = x; // 将`x`移动到`z`,而没有创建一个新的所有者
assert!(*z == [1i, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
// 这个变量是可变的,但是此包装盒中的内容不是可变的
let mut a = Rc::new([10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
a = z;
通过std::gc::Gc 提供了一个带垃圾收集特性的指针,这种情况下,某个与具体任务相关的垃圾收集器会拥有该包装盒的所有权。它允许创建循环引用(cycles),而单独的 Gc指针没有析构函数。
use std::gc::GC;
// 通过一个带垃圾收集特性的包装盒分配的一个固定尺寸数组
let x = box(GC) [1i, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let y = x; // 不会引起移动语义,这与`Rc`不同
let z = x;
assert!(*z == [1i, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
在共享所有权的情况下,可变性无法被继承,因此,这些包装盒永远是不可变的。不过,可以通过像 std::cell::Cell 这样的类型来使用 动态的 可变性,这种情况下,冻结是通过动态检查来处理的,可能会在运行时失败。
Rc 和 Gc类型都是不可发送的,所以不能用来在任务之间共享内存。安全的不可变及可变的共享内存,是由 sync::arc 模块提供的。
命名函数,也就是我们到目前为止见过的那些函数,不可以引用到该函数体之外声明的局部变量:那些变量不在它们的作用范围内(有些时候,我们也称作,“捕获”它们所在范围内的变量)。例如,妳不能写以下代码:
let x = 3;
// `fun`无法引用到`x`
fn fun() -> () { println!("{}", x); }
闭包( closure )支持访问到它周围作用域内的变量;下面代码中,我们会创建2个 闭包 (匿名函数)。跟函数做一下,比较,注意看 || 是如何替换掉 () 的,以及它们是如何访问到 x 的:
let x = 3;
// `fun`是一个无效的定义
fn fun () -> () { println!("{}", x) } // 无法捕获周围的作用域中的变量
let closure = || -> () { println!("{}", x) }; // 可以捕获周围作用域中的变量
// `fun_arg`是一个无效的定义
fn fun_arg (arg: int) -> () { println!("{}", arg + x) } // 无法捕获
let closure_arg = |arg: int| -> () { println!("{}", arg + x) }; // 可以捕获
// ^
// 这里需要写上类型,因为代码中需要知道应当使用哪个`+`操作符。
// 在未来版本中,可能不再需要这样写。
fun(); // 仍然不起作用
closure(); // 输出:3
fun_arg(7); // 仍然不起作用
closure_arg(7); // 输出:10
闭包的写法:将参数列表放置在两个竖线中,接着是一个单个表达式。记住,一个代码块, { <expr1>; <expr2>; ... } ,也被认为是一个单个表达式:如果其中最后一条语句不带分号,则该代码块的值就取最后一条语句的值,否则访代码块的值就是 () ,即为单位值。
一般来说,对于函数定义,返回类型和所有的参数类型都必须显式指定。(之前在函数一节中提到过,在函数声明中,如果省略掉返回类型的话,相 当于显式地把返回类型定义为单位类型, () 。)
fn fun (x: int) { println!("{}", x) } // 这等价于写上`-> ()`
fn square(x: int) -> uint { (x * x) as uint } // 其它的返回类型,要显式写出
// Error: mismatched types: expected `()` but found `uint`
fn badfun(x: int) { (x * x) as uint }
fn main()
{
}
另一方面呢,编译器通过可以为闭包表达式推导出参数和返回类型;因此,它们通常会被省略,因为,阅读该代码的人和编译器都能够通过周围的上下文来推导出它们的类型。这就与函数定义相反了,在函数定义中,必须明确指定类型,而不能依靠类型推导。比较一下:
// `fun`,作为一个函数定义,无法推导出`x`的类型,所以必须提供类型
fn fun (x: int) { println!("{}", x) }
fn main()
{
let closure = |x | { println!("{}", x) }; // 推导出`x: int`,返回类型`()`
// 对于闭包,省略返回类型并不等价于写上`-> ()`
let add_3 = |y | { 3i + y }; // 推导出`y: int`,返回类型`int`。
fun(10); // 输出10
closure(20); // 输出20
closure(add_3(30)); // 输出33
fun("String"); // Error: mismatched types
// Error: mismatched types
// 通过类型推导,已经确定`closure`的类型是`|int| -> ()`
closure("String");
}
如果编译器确定需要我们的帮助的话,就必须将参数和返回类型写在闭包定义里,使用之前展示的格式来写就可以了。在下面的示例中,由于有多个类型都提供了 *操作符,所以 x 参数的参数类型必须显式提供。
fn main()
{
// 错误:必须知道`x`的类型,才能用于`x * x`
let square = |x | -> uint { (x * x) as uint };
}
在正确的版本中,参数类型是显式注明的,而返回类型仍然可以推导出来。
let square_explicit = |x: int| -> uint { (x * x) as uint };
let square_infer = |x: int| { (x * x) as uint };
println!("{}", square_explicit(20)); // 400
println!("{}", square_infer(-20)); // 400
有多种形式的闭包,每种都有它自己的作用。最常见的一种,称作 栈闭包 ,其类型为 || ,可以直接访问到周围作用域里的局部变量。
let mut max = 0;
let f = |x: int| if x > max { max = x };
for x in [1, 2, 3].iter() {
f(*x);
}
栈闭包是非常高效的,因为,它们的环境是在调用栈上分配的,而且是使用指向所捕获的局部变量的指针来引用的。为了避免栈闭包的生存时间比它们所引用的局部变量还长,我们不让栈闭包成为语言中的一等元素(first-class)。就是说,它们只能出现在参数能够出现的位置;它们不能储存在数据结构中,也不能从函数中返回。尽管有着这些限制,栈闭包仍然在Rust 代码中到处被使用。
15.1 被拥有的闭包
被拥有的闭包,写作 proc ,其中储存的是可以安全地在进程(processes)之间发送的东西。它们会复制自己所捕获的值,同时也拥有了那些值:也就是说,其它的代码都无法访问那些变量了。被拥有的闭包,是用于并行代码中的,尤其是用于创建 任务 。
闭包可用来创建任务。对于这种模式的一个实际示例就是,使用 spawn 函数,该函数用于启动一个新任务。
use std::task::spawn;
fn main()
{
// proc即是将被启动的闭包。
spawn(proc() {
println!("I'm a new task")
});
} //fn main()
15.2 闭包兼容性
Rust中的闭包有一个方便的子类型(subtyping)属性:妳可以将任意类型的闭包(只要参数和返回类型是匹配的)传递给需要一个 || 类型参数的函数。因为,当妳正在写一个会调用其中的函数参数而不做其它事情的高阶函数时,妳几乎应当永远将那个参数的类型声明为 || 。那样的话,调用者可以向它传入任意类型的闭包。
fn call_twice(f: ||) { f(); f(); }
let closure = || { "I'm a closure, and it doesn't matter what type I am"; };
fn function() { "I'm a normal function"; }
call_twice(closure);
call_twice(function);
注意:语法和语义都会逐渐变化。目前,它们可能会不完整,尤其对于不可复制的类型更是如此。
方法与函数类似,唯一的不同就是,它们总是带有一个特殊参数,叫做 self ,其类型与该方法的接收者一致。 self参数类似于C++和其它狠多语言中的 this 。方法是使用小数点语法来调用的,例如 my_vec.len() 。
实现代码 ,即,使用 impl 关键字引出的代码,可以为大部分Rust 类型定义方法,包括结构体和枚举类型。例如,为 Shape 枚举类型定义一个 draw 方法。
struct Point {
x: f64,
y: f64
}
enum Shape {
Circle(Point, f64),
Rectangle(Point, Point)
}
impl Shape {
fn draw(&self) {
match *self {
Circle(p, f) => draw_circle(p, f),
Rectangle(p1, p2) => draw_rectangle(p1, p2)
}
}
}
let s = Circle(Point { x: 1.0, y: 2.0 }, 3.0);
s.draw();
这段代码为 Shape 定义了一个 实现 ,其中包含一个方法, draw 。在大部分方面, draw方法与任意其它函数类似,不同之处就是,其中有个 self 。
self 的类型与该方法的接收者相同,或是该类型的一个指针。作为一个参数,它可能被写作 self 、 &self 或 ~self 。调用者必须要有一个相兼容的指针类型,才能调用该方法。
impl Shape {
fn draw_reference(&self) { /* ... */ }
fn draw_owned(~self) { /* ... */ }
fn draw_value(self) { /* ... */ }
}
let s = Circle(Point { x: 1.0, y: 2.0 }, 3.0);
(&s).draw_reference();
(box s).draw_owned();
s.draw_value();
在方法的定义中,一般都使用引用型的self 类型,所以,编译器会尽力将被调用者转换成一个引用。
// 对于典型的函数参数来说,被拥有的指针会自动转换成引用
(box s).draw_reference();
// 与典型的函数参数不同,self值会被
// 自动地引用……
s.draw_reference();
// ……自动地解引用
(& &s).draw_reference();
// ……以及自动地解引用和借用
(&box s).draw_reference();
在实现代码中,还可以定义独立(有些时候被称作“静态”)的方法。不带 self 参数的,就是这种方法。这种方法就是定义构造函数的最佳方式。
impl Circle {
fn area(&self) -> f64 { /* ... */ }
fn new(area: f64) -> Circle { /* ... */ }
}
要调用这样一个方法的话,则在方法前面加上类型名和双冒号:
use std:: f64::consts::PI;
struct Circle { radius: f64 }
impl Circle {
fn new(area: f64) -> Circle { Circle { radius: (area / PI).sqrt() } }
}
fn main()
{
let c = Circle::new(42.5);
println!("{}",c.radius);
} //fn main()
直到现在,我们都是定义那种针对特定数据类型的函数。利用类型参数,我们可以定义那种其参数为通用类型的函数,这样就可以带着多种类型的参数来调用它们了。设想一个通用的 map 函数,它有两个参数,其中一个是函数 function ,另一个是一个向量 vector ,它返回一个新的向量,其中包含的是,对 vector 中的每个元素应用 function 之后的结果:
fn map<T, U>(vector: &[T], function: |v: &T| -> U) -> Vec<U> {
let mut accumulator = Vec::new();
for element in vector.iter() {
accumulator.push(function(element));
}
return accumulator;
}
以 <T, U> 这种方式定义了类型参数之后,这个函数可用于任意类型的向量,只要 function 的参数的类型与该向量的内容的类型相匹配就可以了。
在泛型函数中,类型参数(按照惯例是大写的)的名字代表着不透明类型(opaque types)。对于这些类型的实例,妳能做的就是将它们来回传递:妳无法对它们做任何操作,也无法对它们做模式匹配。注意,泛型类型的实例经常通过指针来传递。例如,对于 function() ,向它传递参数时,传递的是类型 T 的值的一个指针,而不是类型 T 的值本身。这样,就确保了该函数能够兼容尽可能多的数据类型,因为,某些类型在复制及传值时开销太大,或者根本就不允许复制和传值。
泛型的 type 、 struct 和 enum声明的模式是一样的:
type Set<T> = std::collections::HashMap<T, ()>;
struct Stack<T> {
elements: Vec<T>
}
enum Option<T> {
Some(T),
None
}
这些声明可被实例化为有效的类型,例如 Set<int> 、 Stack<int> 和 Option<int> 。
上面示例中的最后一个类型, Option ,经常在Rust 代码中出现。因为Rust中没有空指针(除非妳写非安全的代码),所以,我们需要另一种方式来写出以下函数:对于适当类型的参数,并非所有的可能的参数的组合都能够产生出确定的结果。通常的做法就是,让该函数返回 Option<T> ,而不是 T 。
fn radius(shape: Shape) -> Option<f64> {
match shape {
Circle(_, radius) => Some(radius),
Rectangle(..) => None
}
}
Rust编译器通过将泛型函数单态化( monomorphizing )来高效地编译泛型函数。单态化是对于某个简单创意的美称:在被调用时,为每个泛型函数生成一个单独的副本,该副本是专门针对当前的参数类型的,因此可以专门做针对性的优化。从这个方面来看,Rust的泛型与C++中的模板有着类似的性能特性。
17.1 特征
在泛型函数中——即,其参数是用类型参数表示的,例如,使用 T 来表示——我们能够针对类型 T 的参数所做的操作是非常有限的。毕竟,我们不知道类型 T 会被实例化为什么类型,所以,我们无法安全地修改或查询类型 T 的值。这就是特征( traits )要解决的问题。特征是Rust中用来编写多态代码的最有力工具。Java开发者会发现它们与Java中的接口类似,Haskeller开发者会觉得它们与类型类(type classes)类似。Rust的特征系统使得我们可以表达出绑定的多态性( bounded polymorphism ):通过限制一个类型参数能够引用的类型的范围,它们扩展了我们能够在该类型上安全地对参数所做的操作的数量。
现在,让我们来设想一下在Rust 中复制一个值的场景。 clone方法并不是对任何一个类型的值都有定义的。其中一个原因就是用户所定义的析构函数:复制一个拥有析构函数的类型的值,会导致析构函数多次运行。因此,拥有析构函数的类型的值,不可以被复制,除非我们显式为它们实现 clone 。
这就增加了处理泛型函数的复杂度。如果我们有一个函数,其中有类型参数 T ,那,我们可以在那个函数中复制类型 T 的值吗?在Rust中,无法做到,如果我们尝试着运行以下代码的话,编译器会报告错误。
// 以下代码无法编译
fn head_bad<T>(v: &[T]) -> T {
v[0] // error: copying a non-copyable value
}
fn main()
{
} //fn main()
但是,我们可以告诉编译器, head函数只能用于那些可被复制的类型。在Rust中,可复制的类型就是,那些, 实现了 Clone 特征的类型 。然后,我们可以通过调用 clone 方法来为我们要返回的值创建第二个副本:
// 这段代码可以编译通过
fn head<T: Clone>(v: &[T]) -> T {
v[0].clone()
}
绑定的类型参数 T: Clone 的意思是,对于任意的 T ,可以以类型 &[T] 的参数来调用 head ,嘦对于 T 存在着 Clone 特征的实现。在实例化一个泛型函数时,我们只会针对实现了正确特征的类型来实例化它,因此,对于一个其中元素的类型未实现 Clone 特征的向量来说,我们无法应用 head 函数。
尽管大部分特征可由用户代码来定义及实现,但是,有3个特征,是由编译器针对所有可应用的类型来自动继承及实现的,并且,这3个特征不可被覆盖(overridden):
• Send - 可发送的类型。除非该类型中包含着引用,否则该类型是要发送的。
•
• Share - 线程安全( threadsafe )的类型。这些类型,可以以一个 &T 指针的形式被多个线程安全地使用。 Mutex<T> 便是可共享( sharable )的类型的一个例子,它带有内部的可变数据。
•
• 'static – 不可借用的类型。这个类型中不包含任何符合以下条件的数据:其生命周期被绑定到某个特定的栈帧。这些类型中不包含任何的引用,或者,其中唯一所包含的引用具有 'static 形式的生命周期。(欲知更多关于命名生命周期和它们的用法的说明,则参考 引用和生命周期指南 。)
注意:在本语言的之前版本中,将这些内置的特征称作“种类”('kinds'),而且经常看到有些人还这样说。
另外, Drop特征用于定义析构函数。这个特征提供了一个方法,称作 drop ,当某个实现了该特征的类型的值被销毁(因为该值离开了作用域,或者垃圾收集器回收了它)时,便会自动调用该方法。
struct TimeBomb {
explosivity: uint
}
impl Drop for TimeBomb {
fn drop(&mut self) {
for _ in range(0, self.explosivity) {
println!("blam!");
}
}
}
不允许直接调用 drop 。只有由编译器插入的代码才允许调用它。
17.2 声明及实现特征
在最简单的情况下,一个特征就是由0个或多个方法签名( method signatures )组成的集合。例如,我们可以声明特征 Printable ,表示那些可以将自身信息输出到终端的东西,它只有一个方法签名:
trait Printable {
fn print(&self);
}
我们就说, Printable 这个特征, 提供 了一个 print 方法,并且指定了它的签名。这就意味着,对于任何一个实现了 Printable 特征的类型的参数,我们都可以调用它的 print 方法。
Rust中内置的 Send 和 Share类型,就是那种不提供任何方法的特征的例子。
可使用实现( impls )来针对特定的类型实现某些特征。对于特定特征的一个实现(impl),实现了该特征提供的某些方法。例如,下面代码中,对于 int 和 String 针对 Printable 的实现中,具体地实现了 print 方法。
impl Printable for int {
fn print(&self) { println!("{}", *self) }
}
impl Printable for String {
fn print(&self) { println!("{}", *self) }
}
在实现代码中针对某个特征所定义的方法,可以像其它方法一样调用,使用小数点语法就可以,例如 1.print() 。
17.3 特征定义中提供的默认方法实现
有些时候,对于某个特征提供的某个方法,在实现该特征的大部分或所有类型中,其实现都是相同的。例如,我们希望 bool 和 f32 都可以被输出到终端,并且,我们希望这两个类型 print 中的实现与 int 的实现相同:
impl Printable for f32 {
fn print(&self) { println!("{}", *self) }
}
impl Printable for bool {
fn print(&self) { println!("{}", *self) }
}
可以这样写,但是,现在我们已经在三处重复写了 print 的相同的定义。我们不需要那样做,我们只需要简单地将 print 的定义包含在特征定义中,而不是仅仅给出方法签名。就是说,我们可以这样写:
extern crate debug;
trait Printable {
// 默认的方法实现
fn print(&self) { println!("{:?}", *self) }
}
impl Printable for int {}
impl Printable for String {
fn print(&self) { println!("{}", *self) }
}
impl Printable for bool {}
impl Printable for f32 {}
这段代码中,对于 int 、 bool 和 f32 针对 Printable 的实现代码里,不需要提供对于 print 的实现。因为,在省略某个特定实现的情况下,Rust会直接使用特征定义中提供的 默认方法 。取决于具体的特征,默认方法可以省掉大量的本来会在实现代码中写下的重复代码。当然,单个的实现代码中仍然可以覆盖掉 print 的默认方法,以上代码中对于 String 的实现中就是这么干的。
17.4 带类型参数的特征
特征可使用类型变量来参数化。例如,一个用来表达通用序列式类型的特征,可以这样写:
trait Seq<T> {
fn length(&self) -> uint;
}
impl<T> Seq<T> for Vec<T> {
fn length(&self) -> uint { self.len() }
}
在实现代码中,必须要先显式地声明它所要绑定的类型参数 T ,之后才能使用它来指定自己的特征类型。Rust要求写上这样的声明,因为, impl 也可指定针对其它类型的实现,例如 Seq<int> 。特征类型(位于impl 和 for之间)用来引用 一个类型,而不是定义它。
由特征所绑定的类型参数,处于每个方法声明的作用域中。所以,在特征或者实现中,重新将类型参数 T声明为 length 的一个显式类型参数的话,会导致编译错误。
在特征的定义中, Self 是一个特殊类型,妳可以把它看作一个类型参数。在为该特征针对任意指定类型 T 所做的实现中,会将 Self 类型参数替换为 T 。以下代码中,这个特征,描述的是支持一个相等性操作的那些类型:
// 在特征中,`self`指代的是self参数。
// `Self`指代的是实现该特征的那个类型。
trait PartialEq {
fn equals(&self, other: &Self) -> bool;
}
// 在实现中,`self`仅仅指代接收者的值
impl PartialEq for int {
fn equals(&self, other: &int) -> bool { *other == *self }
}
注意,在特征的定义中, equals 有第二个参数,其类型为 Self 。反过来,在 impl 中, equals 的第二个参数是 int 类型,仅仅将 self作为接收者的名字使用。
就像在类型的实现中看到的那样,特征也可以定义独立(静态)方法。在调用这些方法时,要将特征名字和双冒号放在方法名前面。编译器利用类 型推导来决定该使用哪个具体的实现。
use std:: f64::consts::PI;
trait Shape { fn new(area: f64) -> Self; }
struct Circle { radius: f64 }
struct Square { length: f64 }
impl Shape for Circle {
fn new(area: f64) -> Circle { Circle { radius: (area / PI).sqrt() } }
}
impl Shape for Square {
fn new(area: f64) -> Square { Square { length: area.sqrt() } }
}
let area = 42.5;
let c: Circle = Shape::new(area);
let s: Square = Shape::new(area);
17.5 绑定的类型参数及静态方法调度
特征给我们提供了一种强大的语言,可以用来为类型定义断言(predicates),或者说是类型能够拥有的抽象属性。我们可以使用这种语言来定义类型参数的 绑定 ,这样,我们就可以操作泛型类型了。
fn print_all<T: Printable>(printable_things: Vec<T>) {
for thing in printable_things.iter() {
thing.print();
}
}
将T声明为满足Printable特征(as we earlier did with Clone),就使得,可以在函数内部对类型 T 的值调用该特征中的那些方法。同时,如果尝试对一个其中的元素类型未实现 Printable 特征的向量调用 print_all 的话,会导致编译错误。
类型参数可拥有多个绑定,将它们用 +分隔就可以,在以下这个版本的 print_all 中就是这样写的,它会复制元素。
fn print_all<T: Printable + Clone>(printable_things: Vec<T>) {
let mut i = 0;
while i < printable_things.len() {
let copy_of_thing = printable_things.get(i).clone();
copy_of_thing.print();
i += 1;
}
}
对于绑定类型参数的方法调用,是静态调度的,这样就不会比普通的函数调用有更多的开销,所以是优先用来以多态方式使用特征的方式。
对于特征的使用,类型于Haskell的类型类。
17.6 特征对象和动态方法调度
上面的代码中,允许我们定义一些函数,这些函数会多态地在满足指定特征的单个未知类型的值上做操作。然而,研究一下以下这个函数:
trait Drawable { fn draw(&self); }
fn draw_all<T: Drawable>(shapes: Vec<T>) {
for shape in shapes.iter() { shape.draw(); }
}
妳可以在一个由圆组成的向量上调用它,或者在一个由矩形组成的向量上调用它,(假设那两个类型都适当地定义了 Drawable 特征),但是不能在一个同时包含圆形和矩形的向量上调用它。当我们需要这种行为时,可以使用特征名来作为类型,此时我们称它为对象。
fn draw_all(shapes: &[Box<Drawable>]) {
for shape in shapes.iter() { shape.draw(); }
}
在这个示例中,没有类型参数。取而代之的是, Box<Drawable>类型,它表示任意一个实现了 Drawable 特征的被拥有的包装盒值。要构造这样一个值的话,就使用 as操作符来将对应的值转换成一个对象:
impl Drawable for Circle { fn draw(&self) { /* ... */ } }
impl Drawable for Rectangle { fn draw(&self) { /* ... */ } }
let c: Box<Circle> = box new_circle();
let r: Box<Rectangle> = box new_rectangle();
draw_all([c as Box<Drawable>, r as Box<Drawable>]);
我们省略了 new_circle 和 new_rectangle 的代码;可以想像成它们仅仅返回默认尺寸的 Circle 和 Rectangle 。注意,就像字符串和向量一样,对象的尺寸是不固定的,因而只能通过某种指针类型来引用。使用其它的指针类型也可以。只有与之相兼容的指针,才能转换成特征,因此,举个例子, &Circle 不能被转换成 Box<Drawable> 。
// 一个被拥有的对象
let owny: Box<Drawable> = box new_circle() as Box<Drawable>;
// 一个被借用的对象
let stacky: &Drawable = &new_circle() as &Drawable;
对于特征类型的方法调用,是 动态调度 的。由于编译器无法在编译期得知具体该调用哪个函数,所以,它使用一个查找表(也被称作虚表(vtable)或字典)来在运行时选择要调用的方法。
对于特征的这种用法,类似于Java中的接口。
还有一些内置的绑定,例如 Send 和 Share ,它们是类型的组件的属性。按照设计,特征对象是不知道它们的内容的准确类型的,因此,编译器无法推导出它们的属性。
然而,妳可以指示编译器,某个特征对象的内容必须满足某个特定的绑定,具体做法就是在末尾写上一个冒号( : )。以下是一些有效的类型:
trait Foo {}
trait Bar<T> {}
fn sendable_foo(f: Box<Foo + Send>) { /* ... */ }
fn shareable_bar<T: Share>(b: &Bar<T> + Share) { /* ... */ }
如果未指定冒号的话(例如Box<Foo> 这个类型),那么,会推导出这个值不满足任何绑定。如果在使用过程中要用上绑定的话,那么,必须手动添加。
内置类型的绑定,也可以以相同的方式指定给闭包类型(例如,这样写 fn:Send()),而其默认行为也与具有相同存储类的特征相同。
17.7 特征的继承
我们可以写一个 继承 了其它特征的特征声明,被继承的特征叫做超特征( supertraits )。那些实现某个特征的类型,也必须实现它的超特征。例如,我们可以定义一个 Circle特征,它继承了 Shape特征。
trait Shape { fn area(&self) -> f64; }
trait Circle : Shape { fn radius(&self) -> f64; }
现在,对于一个类型,只有在我们也实现 Shape 的情况下,我们才能实现 Circle 。
use std:: f64::consts::PI;
struct CircleStruct { center: Point, radius: f64 }
impl Circle for CircleStruct {
fn radius(&self) -> f64 { (self.area() / PI).sqrt() }
}
impl Shape for CircleStruct {
fn area(&self) -> f64 { PI * square(self.radius) }
}
注意, Circle 中的那些方法可以调用 Shape 中的方法,在我们的 radius实现中就调用了 area方法。这是一种丧心病狂的计算圆形的半径的方式(因为我们可以直接返回 radius字段的值),但是,妳知道可以这样写就行了。
在类型参数化的函数中,可以对那些绑定到子特征的类型参数的值调用超特征的方法。看看之前那个关于 trait Circle : Shape 的示例:
fn radius_times_area<T: Circle>(c: T) -> f64 {
// `c`既是一个Circle也是一个Shape
c.radius() * c.area()
}
同样地,超特征的方法,也可以通过特征对象来调用。
use std:: f64::consts::PI;
let concrete = box CircleStruct{center:Point{x:3.0,y:4.0},radius:5.0};
let mycircle: Box<Circle> = concrete as Box<Circle>;
let nonsense = mycircle.radius() * mycircle.area();
注意:特征的继承目前还无法与对象配合使用
17.8 继承对特征的实现
在 std 和 extra 中的一小部分特征中,会有一些能够被自动继承的实现。这些实例是如此指定的:将 deriving 属性放置在某个数据类型声明中。例如,以下代码表示, Circle实现了 PartialEq ,因而可以用在相等性操作符中,而 ABC 类型的一个值可以被随机生成并且可以被转换成字符串:
extern crate rand;
#[deriving(PartialEq)]
struct Circle { radius: f64 }
#[deriving(Rand, Show)]
enum ABC { A, B, C }
fn main() {
// 使用Show这个特征来输出"A, B, C."
println!("{}, {}, {}", A, B, C);
}
可继承的特征的完整列表: PartialEq 、 Eq 、 PartialOrd 、 Ord 、 Encodable 、 Decodable 、 Clone 、 Hash 、 Rand 、 Default 、 Zero 、 FromPrimitive 和 Show 。
Rust的模块系统是非常强大的,因而也非常复杂。不管怎样,这一章节中,我们会尝试解释其中的每个重要方面。
18.1 箱子(Crates)
在介绍模块系统之前,我们先定义一下用来容纳它的东西:
假设妳写了一个程序或库,编译了它,然后得到了对应的二进制文件。在Rust中,编译器需要直接编译以构造出那个二进制文件的所有源代码的内容,被统称为一个“箱子”('crate')。
例如,对于一个简单的世界妳好(hello world)程序,妳的箱子中就只包含以下代码:
// `main.rs`
fn main() {
println!("Hello world!");
}
箱子也是Rust 中单独编译的基本单元:rustc每次都会编译一个单独的箱子,从那个箱子中产生出一个库或一个可执行程序。
注意,仅仅在妳的代码中使用某个已编译的库,不会让它成为妳的箱子的一部分。
18.2 模块层次关系
对于每个箱子,其中的所有代码都被组织到一个由模块组成的层次结构中,并且其顶层是单个根模块。这个根模块被称作“箱根”( 'crate root')。
在一个箱子中,所有位于箱根之下的模块都是以 mod 关键字来定义的:
// 这是箱根
mod farm {
// 这是定义于箱根中的模块'farm'的代码体。
fn chicken() { println!("cluck cluck"); }
fn cow() { println!("mooo"); }
mod barn {
// 模块'barn'的代码体
fn hay() { println!("..."); }
}
}
fn main() {
println!("Hello farm!");
}
妳可以看到,现在,妳的模块层次关系有三层深了:其中包含着箱根,它又包含着妳的 main()函数,以及模块 farm 。模块 farm 又包含着两个函数以及第三个模块 barn ,其中包含着一个函数 hay 。
18.3 路径和可见性
我们已经定义了一个漂亮的模块层次关系。但是,如何在我们的 main 函数中访问到其中的那些东西?一种方式就是,完整地引用它:
mod farm {
fn chicken() { println!("cluck cluck"); }
// ...
}
fn main() {
println!("Hello chicken!");
::farm::chicken(); // 无法编译通过,见下文说明
}
这里的 ::farm::chicken结构,我们称之为“路径”('path')。
因为它是以 ::开头的,所以它是一个“全局路径”('global path'),它以箱根为基准,采用模块层次关系中的完整路径来标识一个元素。
如果路径是以一个常规标识符开头的,例如 farm::chicken ,则它是一个“局部路径”('local path')。我们日后再说这个。
此时,如果妳尝试编译这段示例代码的话,会产生一个错误 function 'chicken' is private 。那是因为,默认情况下,元素(fn 、 struct 、 static 、 mod 、...)是私有的。
要想让它们变成在包含它们的模块之外也可访问的,则,妳需要使用 pub 关键字来将它们变成 公有 的:
mod farm {
pub fn chicken() { println!("cluck cluck"); }
pub fn cow() { println!("mooo"); }
// ...
}
fn main() {
println!("Hello chicken!");
::farm::chicken(); // 现在可以编译通过了
}
Rust 中的可见性限制只存在于模块的边界处。这与大部分的面向对象语言有狠大的不同,后者会对对象本身的可见性也做限制。这并不是说Rust不支持封装:结构体的字段和方法都可以是私有的。但是这种封装是在模块级别的,而不是结构体级别的。
默认情况下,字段是 私有 的,可使用 pub 关键字让它们变成 公有 的:
mod farm {
pub struct Farm {
chickens: Vec<Chicken>,
pub farmer: Human
}
impl Farm {
fn feed_chickens(&self) { /* ... */ }
pub fn add_chicken(&self, c: Chicken) { /* ... */ }
}
pub fn feed_animals(farm: &Farm) {
farm.feed_chickens();
}
}
fn main() {
let f = make_me_a_farm();
f.add_chicken(make_me_a_chicken());
farm::feed_animals(&f);
f.farmer.rest();
// 以下代码无法编译通过,因为以下两个方法都是私有的:
// `f.feed_chickens();`
// `let chicken_counter = f.chickens.len();`
}
可在Rust 手册中找到关于可见性规则的详情和规范。
18.4 文件和模块
Rust 的模块系统中有一个狠重要的概念:源代码文件跟模块不是一回事。妳定义一个模块层次结构,将妳所有的定义填充于其中,定义好可见性,可能还会定义一个 fn main() ,然后就完事了。
在编译时,唯一一个要注意其位置的文件就是,包含着妳的箱根代码体的那个文件,为什么要注意它的位置呢?这也仅仅是因为,妳需要将它传递给 rustc ,以编译妳的箱子。
原则上来说,妳只需要这样做:妳可以将任意一个Rust程序写在一个巨大的源文件里,其中包含着妳的箱根,其它的代码都位于 mod ... { ... } 声明里。
然而,实际开发过程中,妳通常会将源代码分开放置在多个源文件中,以便于管理。Rust允许妳将任何模块的代码体放置在它自己的源文件里。如果妳声明一个不带代码体的模块,例如 mod foo; ,那么,编译器会在某些目录(通常是与包含着 mod foo; 声明的文件相同的目录)中寻找foo.rs和foo/mod.rs文件。如果它找到了其中一个,那么,它会使用那个文件中的内容作为该模块的代码体。如果同时找到了两个文件,那么,会导致编译错误。
要想将 mod farm 的内容移动到它自己的文件中,妳可以这样写:
// `main.rs` - 包含着箱根的代码体
mod farm; // 编译器会寻找`farm.rs`和`farm/mod.rs`文件
fn main() {
println!("Hello farm!");
::farm::cow();
}
// `farm.rs` - 包含着箱根中声明的模块'farm'的代码体
pub fn chicken() { println!("cluck cluck"); }
pub fn cow() { println!("mooo"); }
pub mod barn {
pub fn hay() { println!("..."); }
}
简单来说, mod foo;实际上就是 mod foo { /* <...>/foo.rs 或 <...>/foo/mod.rs 的内容 */ } 的语法糖。
这也意味着,在妳的箱子层次中,某处放置着两个或多个相同内容的 mod foo;声明的话,是一件坏事,这就跟把某个模块的代码复制粘贴到多处的道理一样。这会引起冗余及互不兼容的定义。
rustc 在解析这些模块声明时,它会从包含着 mod foo 声明的文件的上级目录开始寻找。例如,某个文件中的内容是这样的:
// `src/main.rs`
mod plants;
mod animals {
mod fish;
mod mammals {
mod humans;
}
}
则,编译器会按照顺序寻找以下文件:
src/plants.rs
src/plants/mod.rs
src/animals/fish.rs
src/animals/fish/mod.rs
src/animals/mammals/humans.rs
src/animals/mammals/humans/mod.rs
记住,相同的模块层次关系仍然会导致在不同的路径中寻找,这取决于妳如何将模块的代码体分割开来,以及妳将各个模块的代码体放置在哪个文件中。例如,假设妳将 animals 模块也放置在它自己的文件中:
// `src/main.rs`
mod plants;
mod animals;
// `src/animals.rs` 或`src/animals/mod.rs`
mod fish;
mod mammals {
mod humans;
}
...那么,对于 mod animals 的子模块的源文件呢,可能与animals 源文件位于同一个目录中,也可能在它的目录的子目录中。如果animals源文件是 src/animals.rs ,那么, rustc 会查找:
src/animals.rs
src/fish.rs
src/fish/mod.rs
src/mammals/humans.rs
src/mammals/humans/mod.rs
如果animals源文件是 src/animals/mod.rs ,那么, rustc 会查找:
src/animals/mod.rs
src/animals/fish.rs
src/animals/fish/mod.rs
src/animals/mammals/humans.rs
src/animals/mammals/humans/mod.rs
利用这些规则,妳可以编写一些简单的模块,只包含单个源文件,位于相同的目录中,也可以编写巨大的模块,使用子目录来组织子模块的源文件。
如果妳想明确指定 rustc应该到哪里去寻找某个模块的源文件的话,则使用 path 这个编译器指令。例如,要想从某个特殊文件中载入某个名为 classified 的模块的话:
#[path="../../area51/alien.rs"]
mod classified;
18.5 将名字导入到局部作用域中
如果总是以全局路径来引用其它模块中的定义的话,妳会老得狠快,所以Rust提供了一种手段,让妳可以将它们导入到妳的模块的局部作用域中: use语句。
用法:在模块代码体的开头、 fn代码体的开头、或别的可以写这种语句的代码块中,写上关键字 use ,后面跟一个指向某个元素的不带 :: 前缀的 全局路径 。例如,这名代码会将 cow导入到局部作用域中:
use farm::cow;
妳提供给 use 的路径是按照全局路径来写的,就是说,它是相对于箱根的,无论该模块层次关系有多深,也无论该模块的代码体是否被写在它自己的文件中。(记住:文件是不相关的。)
这与其它语言不同,其它语言中,通常只会有一个单个的导入语句结构,该结构组合了 mod foo; 和 use 的语义,并且,倾向于以相对于当前源文件的路径或者是绝对文件路径来工作的——想一想Ruby的 require 或C/C++的 #include 。
不过呢,我们仍然可以以相对于包含着 use 语句的模块的路径来导入其它模块中的东西:在路径的前面加上 super:: 会导致从上级模块开始寻找,而加上 self::前缀的话会导致从当前模块开始寻找:
use super::some_parent_item;
use self::some_child_module::some_item;
同样地——与模块相关,而与文件无关。
导入的东西也会被局部定义所屏蔽:对于妳在一个模块/代码块中提到了每个名字, rust首先会检查那些在局部定义的元素,只有在局部定义中未找到的情况下,才会检查妳使用对应的 use 语句导入到局部作用域中的那些元素。
use farm::cow;
// ...
fn cow() { println!("Mooo!") }
fn main() {
cow() // 解析为局部定义的`cow()`函数
}
为了使这个行为更加确定,我们定下了一条规则, use语句必须写在任何声明语句之前,就像上例所示。这完全是一个人为的规则,因为,人们总是以为它们会按照顺序来互相屏蔽,实际上根本不是这么回事,实际上,在rust中,所有东西都是互相递归地定义的,与顺序无关。
这个规则导致的一个奇特后果就是, use语句出现在 mod 声明语句之前,即便它们引用的正是该声明中定义的东西,也是如此:
use farm::cow;
mod farm {
pub fn cow() { println!("Moooooo?") }
}
fn main() { cow() }
以下是我们使用 use 语句进行导入的 farm 示例代码:
use farm::chicken;
use farm::cow;
use farm::barn;
mod farm {
pub fn chicken() { println!("cluck cluck"); }
pub fn cow() { println!("mooo"); }
pub mod barn {
pub fn hay() { println!("..."); }
}
}
fn main() {
println!("Hello farm!");
// 现在可以直接引用那些名字了:
chicken();
cow();
barn::hay();
}
以下是多个文件情况下的示例:
// `a.rs` - 箱根
use b::foo;
use b::c::bar;
mod b;
fn main() {
foo();
bar();
}
// `b/mod.rs`
pub mod c;
pub fn foo() { println!("Foo!"; }
// `b/c.rs`
pub fn bar() { println!("Bar!"); }
还有两种简写形式,用于一次导入多个名字:
1. 在一个 use 语句的路径末尾,显式地写上多个名字:
use farm::{chicken, cow};
2.使用通配符,导入某个模块中的所有东西:
use farm::*;
注意:目前,编译器的这个特性是由 #![feature(globs)] 指令来开关的。还可以在手册中找到更多类似的指令。
然而,这还没完。当妳向局部作用域中导入一个元素时,还可以重命名它:
use egg_layer = farm::chicken;
// ...
fn main() {
egg_layer();
}
一般来说, use 是创建了一个局部别名:一个替代路径,以及一个可能不同的名字,用于访问到同一个元素,并且还不需要接触到原始对象,并且,二者还是可互相替换的。
18.6 重新导出名字
还可以重新导出妳的模块中的一些元素,使得它们能够在妳的模块名下被访问到。
对于这种用法,妳需要写 pub use :
mod farm {
pub use self::barn::hay;
pub fn chicken() { println!("cluck cluck"); }
pub fn cow() { println!("mooo"); }
mod barn {
pub fn hay() { println!("..."); }
}
}
fn main() {
farm::chicken();
farm::cow();
farm::hay();
}
就跟一般的 use语句一样,所导出的名字只是代表着对于某个相同事物的别名,并且也可以被重命名。
以上示例也展示了 pub use 的作用:嵌套的 barn模块是私有的,但是, pub use语句使得模块farm 的用户能够访问到 barn 中的某个函数,还不需要知道 barn 的存在。
换句话说,妳可以使用这个东西来将一个公有接口与它的内部实现分离开来。
18.7 使用库
到目前为止,我们还只说到了怎么定义及组织妳自己的箱子。
然而,大部分程序都希望使用一些已有的库,而不是什么东西都由自己从头开始写。
在Rust中,我们需要有一种方法来引用到其它的箱子。
对于这一点,Rust给妳提供了 extern crate声明:
extern crate num;
// `num`由Rust提供 (类似于`extra`;下文详述)。
fn main() {
// 有理数'1/2':
let one_half = ::num::rational::Ratio::new(1i, 2);
}
extern crate foo; 这个语句,会使得 rustc搜索foo 这个箱子,如果它找到了相匹配的二进制文件,就会允许妳在自己的箱子中使用它。
它对妳的模块层次关系所产生的效果,相当于 mod 和 use 的组合:
•类似于 mod ,它会导致 rustc生成以下代码:要将该二进制文件与库 foo 链接起来。
•类似于 use ,所以引用了同一个库的 extern crate语句都是可以交换的,因为,每一个语句都仅仅是代表了一个指向某个外部模块(妳所链接的那个库的箱根)的别名。
还记得吗? use语句必须位于局部声明之前,因为,后者会屏蔽前者?嗯,在这个方面, extern crate语句也有自己的规则: use 和局部声明都可以屏蔽它们,所以,规则就是, extern crate必须位于use 和局部声明之前。
于是就会产生这样的代码:
extern crate num;
use farm::dog;
use num::rational::Ratio;
mod farm {
pub fn dog() { println!("woof"); }
}
fn main() {
farm::dog();
let a_third = Ratio::new(1i, 3);
}
有点怪异,但是,这是屏蔽规则的结果,因为这种屏蔽规则最符合人们的认知。
18.8 箱子的元数据和设置
对于每个箱子,妳都可以定义一些元数据项目,例如链接名字、版本号或作者。妳还可以调整一些在整个箱子范围内有效的选项。这两种机制的具体做法都是在箱根处定义一些属性。
比如说,Rust使用链接元数据来唯一地标识箱子,其中就包含了链接名字和版本号。它还会依据链接元数据来对二进制文件的文件名和其中的符号做散列,这样,妳就可以在一个箱子中使用同一个库的两个不同版本,而不会起冲突。
因此,如果妳打算将妳的箱子编译为一个库的话,那么,妳需要标注对应的信息:
// `lib.rs`
#![crate_id = "farm#2.5" ]
// ...
妳还可以在 extern crate 语句中指定箱子的编号信息。例如,以下3个 extern crate语句都会接受并且选中上面定义的那个箱子:
extern crate farm;
extern crate farm = "farm#2.5";
extern crate my_farm = "farm";
其它的箱子设置及元数据包括:启用/禁用特定的错误或警告;或者显式地设置箱子的类型(库或可执行程序):
// `lib.rs`
// ...
// 这个箱子是一个库(默认值是"bin")
#![crate_id = "farm#2.5" ]
#![crate_type = "lib" ]
// 启用某个警告
#[warn(non_camel_case_types)]
18.9 一个最小化的示例
现在我们弄点真正可以编译的东西。
我们定义两个箱子,并且其中一个箱子被另一个箱子当作库来使用。
// `world.rs`
#![crate_id = "world#0.42" ]
pub fn explore() -> &'static str { "world" }
// `main.rs`
extern crate world;
fn main() { println!("hello {}", world::explore()); }
现在,使用以下命令来编译运行(必要的情况下按照妳的平台做些调整):
$ rustc --crate-type=lib world.rs # 编译libworld-<HASH>-0.42.rlib
$ rustc main.rs -L . # 编译main
$ ./main
"hello world"
注意,所产生的库文件的文件名中会包含版本号,另外还有一串奇怪的由字母数字组成的字符串。之前已经解释过,这两个东西都是Rust 的库文件版本控制模式中的一部分。这里的字母数字字符串是一个散列,代表着这个箱子的编号。
18.10 标准库和前奏(prelude)
在阅读本教程中的那些示例时,妳可能曾经疑惑过,那些神奇的预定义元素都是哪里来的,比如说 range 。
事实上,它们没有什么神奇之处:它们都是定义在 std 库中的,这是Rust 自带的一个箱子。
唯一一个可以称得上神奇的地方就是, rustc 会自动向妳的箱根中插入这一行代码:
extern crate std;
另外还向每个模块代码体中插入以下这行代码:
use std::prelude::*;
prelude 模块的作用就是,重新导出 std 中的那些常用定义。
这样,妳就可以直接使用常用的类型和函数,例如 Option<T> 或 range ,而无需导入它们。如果妳需要使用 std 中某个不存在于 prelude 中的东西的话,妳只需要使用 use 语句导入它就行了。
例如,它重新导出了 range ,这个东西是定义于 std::iter::range 中的:
use iter_range = std::iter::range;
fn main() {
// `range`默认就已经导入了
for _ in range(0u, 10) {}
// 这并不会影响妳以另一个名字来导入它
for _ in iter_range(0u, 10) {}
// 妳也可以完全不理会自动导入的名字。
for _ in ::std::iter::range(0u, 10) {}
}
必要的情况下,可使用一个属性来禁用这种自动插入代码的功能:
// 在箱根处写以下代码:
#![no_std]
// 在任意模块中:
#![no_implicit_prelude]
参考应用编程接口文档 ,以了解详情。
妳已经学会了基础内容,现在可以研究一些单独主题了。
在 维基 上还有一些深入的文档,不过呢,它们甚至可能比本文档还要旧。
Your opinionsHxLauncher: Launch Android applications by voice commands