第7章 程序项
句法:
Item:
OuterAttribute*
VisItem
| MacroItemVisItem:
Visibility?
(
Module
| ExternCrate
| UseDeclaration
| Function
| TypeAlias
| Struct
| Enumeration
| Union
| ConstantItem
| StaticItem
| Trait
| Implementation
| ExternBlock
)MacroItem:
MacroInvocationSemi
| MacroRulesDefinition
*程序项*是 crate 的组成单元。程序项由一套嵌套的模块被组织在一个 crate 内。每个 crate 都有一个“最外层”的匿名模块;crate 中所有的程序项都在其 crate 的模块树中自己的路径。
程序项在编译时就完全确定下来了,通常在执行期间保持结构稳定,并可以驻留在只读内存中。
有以下几类程序项:
有些程序项会形成子(数据)项声明的隐式作用域。换句话说,在一个函数或模块中,程序项的声明可以(在许多情况下)与语句、控制块、以及类似的能构成程序项主体的部件混合在一起。这些在作用域内的程序项的意义与在作用域外声明的程序项的意义相同(它仍然是静态项),只是该程序项在模块的命名空间中的路径名由封闭它的程序项的名称限定,或该程序项也可能是封闭它的程序项的私有程序项(比如函数的情况)。语法规范指定了子项声明可能出现的合法位置。
模块
句法:
Module :
unsafe?modIDENTIFIER;
|unsafe?modIDENTIFIER{
InnerAttribute*
Item*
}
模块是零个或多个程序项的容器。
模块项是一个用花括号括起来的,有名称的,并以关键字 mod 作为前缀的模块。模块项将一个新的具名模块引入到组成 crate 的模块树中。模块可以任意嵌套。
模块的一个例子:
#![allow(unused)] fn main() { mod math { type Complex = (f64, f64); fn sin(f: f64) -> f64 { /* ... */ unimplemented!(); } fn cos(f: f64) -> f64 { /* ... */ unimplemented!(); } fn tan(f: f64) -> f64 { /* ... */ unimplemented!(); } } }
模块和类型共享相同的命名空间。禁止在同一个作用域中声明与此作用域下模块同名的具名类型(named type):也就是说,类型定义、trait、结构体、枚举、联合体、类型参数或 crate 不能在其作用域中屏蔽此作用域中也生效的模块名称,反之亦然。使用 use 引入到当前作用域的程序项也受这个限制。
在句法上,关键字 unsafe 允许出现在关键字 mod 之前,但是在语义层面却会被弃用。这种设计允许宏在将关键字 unsafe 从 token流中移除之前利用此句法来使用此关键字。
Module Source Filenames
模块的源文件名
没有代码体的模块是从外部文件加载的。当模块没有 path 属性限制时,文件的路径和逻辑上的模块路径互为镜像。祖先模块的路径组件(path component)是此模块文件的目录,而模块的内容存在一个以该模块名为文件名,以 .rs 为扩展文件名的文件中。例如,下面的示例可以反映这种模块结构和文件系统结构相互映射的关系:
| 模块路径 | 文件系统路径 | 文件内容 |
|---|---|---|
crate | lib.rs | mod util; |
crate::util | util.rs | mod config; |
crate::util::config | util/config.rs |
当一个目录下有一个名为 mod.rs 的源文件时,模块的文件名也可以和这个目录互相映射。上面的例子也可以用一个承载同一源码内容的名为 util/mod.rs 的实体文件来表达模块路径 crate::util。注意不允许 util.rs 和 util/mod.rs 同时存在。
注意:在
rustc1.30 版本之前,使用文件mod.rs是加载嵌套子模块的方法。现在鼓励使用新的命名约定,因为它更一致,并且可以避免在项目中搞出许多名为mod.rs的文件。
The path attribute
path属性
用于加载外部文件模块的目录和文件可以受 path属性的影响。(或者说可以联合使用 path属性来重新指定那种没有代码体的模块声明的加载对象的文件路径。)
对于不在内联模块(inline module)块内的模块上的 path属性,此属性引入的文件的路径为相对于当前源文件所在的目录。例如,下面的代码片段将使用基于其所在位置的路径:
#[path = "foo.rs"]
mod c;
| Source File | c's File Location | c's Module Path |
|---|---|---|
src/a/b.rs | src/a/foo.rs | crate::a::b::c |
src/a/mod.rs | src/a/foo.rs | crate::a::c |
对于处在内联模块块内的 path属性,此属性引入的文件的路径取决于 path属性所在的源文件的类型。(先对源文件进行分类,)“mod-rs”源文件是根模块(比如是 lib.rs 或 main.rs)和文件名为 mod.rs 的模块,“非mod-rs”源文件是所有其他模块文件。(那)在 mod-rs 文件中,内联模块块内的 path 属性(引入的文件的)路径是相对于 mod-rs 文件的目录(该目录包括作为目录的内联模块组件名)。对于非mod-rs 文件,除了路径以此模块名为目录前段外,其他是一样的。例如,下面的代码片段将使用基于其所在位置的路径:
mod inline {
#[path = "other.rs"]
mod inner;
}
| Source File | inner's File Location | inner's Module Path |
|---|---|---|
src/a/b.rs | src/a/b/inline/other.rs | crate::a::b::inline::inner |
src/a/mod.rs | src/a/inline/other.rs | crate::a::inline::inner |
在内联模块和其内嵌模块上混合应用上述 path属性规则的一个例子(mod-rs 和非mod-rs 文件都适用):
#[path = "thread_files"]
mod thread { // 译者注:有模块要内联进来的内联模块
// 从相对于当前源文件的目录下的 `thread_files/tls.rs` 文件里载 `local_data` 模块。
#[path = "tls.rs"]
mod local_data; // 译者注:内嵌模块
}
Attributes on Modules
模块上的属性
模块和所有程序项一样能接受外部属性。它们也能接受内部属性:可以在带有代码体的模块的 { 之后,也可以在模块源文件的开头(但须在可选的 BOM 和 shebang 之后)。
在模块中有意义的内置属性是 cfg、deprecated、doc、lint检查类属性、path 和 no_implicit_prelude。模块也能接受宏属性。
外部crate声明
句法:
ExternCrate :
externcrateCrateRef AsClause?;CrateRef :
IDENTIFIER |selfAsClause :
as( IDENTIFIER |_)
外部crate(extern crate)声明指定了对外部 crate 的依赖关系。(这种声明让)外部的 crate 作为外部crate(extern crate)声明中提供的标识符被绑定到当前声明的作用域中。此外,如果 extern crate 出现在 crate的根模块中,那么此 crate名称也会被添加到外部预导入包中,以便使其自动出现在所有模块的作用域中。as子句可用于将导入的 crate 绑定到不同的名称上。
外部crate 在编译时被解析为一个特定的 soname1, 并且一个到此 soname 的运行时链接会传递给链接器,以便在运行时加载此 soname。soname 在编译时解析,方法是扫描编译器的库文件路径,匹配外部crate 的 crateid。因为crateid 是在编译时通过可选的 crateid属性声明的,所以如果外部 crate 没有提供 crateid, 则默认拿该外部crate 的 name属性值来和外部crate(extern crate)声明中的[标识符]绑定。
导入 self crate 会创建到当前 crate 的绑定。在这种情况下,必须使用 as子句指定要绑定到的名称。
三种外部crate(extern crate)声明的示例:
extern crate pcre;
extern crate std; // 等同于: extern crate std as std;
extern crate std as ruststd; // 使用其他名字去链接 'std'
当给 Rust crate 命名时,不允许使用连字符(-)。然而 Cargo 包却可以使用它们。在这种情况下,当 Cargo.toml 文件中没有指定 crate 名称时, Cargo 将透明地将 - 替换为 _ 以供 Rust 源文件内的外部crate(extern crate)声明引用 (详见 RFC 940)。
这有一个示例:
// 导入 Cargo 包 hello-world
extern crate hello_world; // 连字符被替换为下划线
Extern Prelude
外部预导入包
本节内容已经移入预导入包 — 外部预导入包中了。
Underscore Imports
下划线导入
外部的 crate依赖可以通过使用带有下划线形如 extern crate foo as _ 的形式来声明,而无需将其名称绑定到当前作用域内。这种声明方式对于只需要 crate 被链接进来,但 crate 从不会被当前代码引用的情况可能很有用,并且还可以避免未使用的 lint 提醒。
下划线导入不会影响 macro_use属性的正常使用,这情况下使用 macro_use属性,宏名称仍会正常导入到 macro_use预导入包中。
The no_link attribute
no_link属性
可以在外部项(extern crate item)上指定使用 no_link属性,以防止此 crate 被链接到编译输出中。这通常用于加载一个 crate 而只访问它的宏。
译者注:这里的 soname 是 linux系统里的动态库文件的 soname。
Use声明
句法:
UseDeclaration :
useUseTree;UseTree :
(SimplePath?::)?*
| (SimplePath?::)?{(UseTree (,UseTree )*,?)?}
| SimplePath (as( IDENTIFIER |_) )?
use声明用来创建一个或多个与程序项路径同义的本地名称绑定。通常使用 use声明来缩短引用模块所需的路径。这些声明可以出现在模块和块中,但通常在作用域顶部。
use声明支持多种便捷方法:
- 使用带有花括号的 glob-like(
::) 句法use a::b::{c, d, e::f, g::h::i};来同时绑定一个系列有共同前缀的路径。 - 使用关键字
self,例如use a::b::{self, c, d::e};,来同时绑定一系列有共同前缀和共同父模块的路径。 - 使用句法
use p::q::r as x;将编译目标名称重新绑定为新的本地名称。这种也可以和上面两种方法一起使用:use a::b::{self as ab, c as abc}。 - 使用星号通配符句法
use a::b::*;来绑定与给定前缀匹配的所有路径。 - 将前面的方法嵌套重复使用,例如
use a::b::{self as ab, c, d::{*, e::f}};。
use声明的一个示例:
use std::option::Option::{Some, None}; use std::collections::hash_map::{self, HashMap}; fn foo<T>(_: T){} fn bar(map1: HashMap<String, usize>, map2: hash_map::HashMap<String, usize>){} fn main() { // 等价于 'foo(vec![std::option::Option::Some(1.0f64), std::option::Option::None]);' foo(vec![Some(1.0f64), None]); // `hash_map` 和 `HashMap` 在当前作用域内都有效. let map1 = HashMap::new(); let map2 = hash_map::HashMap::new(); bar(map1, map2); }
use可见性
与其他程序项一样,默认情况下,use声明对包含它的模块来说是私有的。同样的,如果使用关键字 pub 进行限定,use声明也可以是公有的。use声明可用于*重导出(re-expor)*名称。因此,公有的 use声明可以将某些公有名称重定向到不同的目标定义中:甚至是位于不同模块内具有私有可见性的规范路径定义中。如果这样的重定向序列形成一个循环或不能明确地解析,则会导致编译期错误。
重导出的一个示例:
mod quux { pub use self::foo::{bar, baz}; pub mod foo { pub fn bar() {} pub fn baz() {} } } fn main() { quux::bar(); quux::baz(); }
在本例中,模块 quux 重导出了在模块 foo 中定义的两个公共名称。
use 路径
注意:本章节内容还不完整。
一些正常和不正常的使用 use程序项的例子:
#![allow(unused_imports)] use std::path::{self, Path, PathBuf}; // good: std 是一个 crate 名称 use crate::foo::baz::foobaz; // good: foo 在当前 crate 的第一层 mod foo { pub mod example { pub mod iter {} } use crate::foo::example::iter; // good: foo 在当前 crate 的第一层 // use example::iter; // 在 2015 版里不行,2015 版里相对路径必须以 `self` 开头; 2018 版这样写没问题 use self::baz::foobaz; // good: `self` 指的是 'foo' 模块 use crate::foo::bar::foobar; // good: foo 在当前 crate 的第一层 pub mod bar { pub fn foobar() { } } pub mod baz { use super::bar::foobar; // good: `super` 指的是 'foo' 模块 pub fn foobaz() { } } } fn main() {}
:
版本差异: 在 2015 版中,
use路径也允许访问 crate 根模块中的程序项。继续使用上面的例子,那以下use路径的用法在 2015 版中有效,在 2018 版中就无效了:mod foo { pub mod example { pub mod iter {} } pub mod baz { pub fn foobaz() {} } } use foo::example::iter; use ::foo::baz::foobaz; fn main() {}2015 版不允许用 use声明来引用外部预导入包里的 crate。因此,在2015 版中仍然需要使用
extern crate声明,以便在 use声明中去引用外部 crate。从 2018 版开始,use声明可以像extern crate一样指定外部 crate 依赖关系。在 2018 版中,如果本地程序项与外部的 crate 名称相同,那么使用该 crate 名称需要一个前导的
::来明确地选择该 crate 名称。这种做法是为了与未来可能发生的更改保持兼容。// use std::fs; // 错误, 这样有歧义. use ::std::fs; // 从`std` crate 里导入, 不是下面这个 mod. use self::std::fs as self_fs; // 从下面这个 mod 导入. mod std { pub mod fs {} } fn main() {}
下划线导入
通过使用形如为 use path as _ 的带下划线的 use声明,可以在不绑定名称的情况下导入程序项。这对于导入一个 trait 特别有用,这样就可以在不导入 trait 的 symbol 的情况下使用这个 trait 的方法,例如,如果 trait 的 symbol 可能与另一个 symbol 冲突。再一个例子是链接外部的 crate 而不导入其名称。
使用星号全局导入(Asterisk glob imports,即 ::*)句法将以 _ 的形式导入能匹配到的所有程序项,但这些程序项在当前作用域中将处于不可命名的状态。
mod foo { pub trait Zoo { fn zoo(&self) {} } impl<T> Zoo for T {} } use self::foo::Zoo as _; struct Zoo; // 下划线形式的导入就是为了避免和这个程序项在名字上起冲突 fn main() { let z = Zoo; z.zoo(); }
在宏扩展之后会创建一些惟一的、不可命名的 symbols,这样宏就可以安全地扩展出(emit)对 _ 导入的多个引用。例如,下面代码不应该产生错误:
#![allow(unused)] fn main() { macro_rules! m { ($item: item) => { $item $item } } m!(use std as _;); // 这会扩展出: // use std as _; // use std as _; }
函数
句法
Function :
FunctionQualifiersfnIDENTIFIER GenericParams?
(FunctionParameters?)
FunctionReturnType? WhereClause?
( BlockExpression |;)FunctionQualifiers :
const?async1?unsafe? (externAbi?)?Abi :
STRING_LITERAL | RAW_STRING_LITERALFunctionParameters :
SelfParam,?
| (SelfParam,)? FunctionParam (,FunctionParam)*,?SelfParam :
OuterAttribute* ( ShorthandSelf | TypedSelf )ShorthandSelf :
(&|&Lifetime)?mut?selfTypedSelf :
mut?self:TypeFunctionParam :
OuterAttribute* ( FunctionParamPattern |...| Type 2 )FunctionParamPattern :
PatternNoTopAlt:( Type |...)FunctionReturnType :
->Type1限定符
async不能在 2015版中使用。2在2015版中,只有类型的函数参数只允许出现在trait项的关联函数中。
函数由一个块以及一个名称和一组参数组成。除了名称,其他的都是可选的。函数使用关键字 fn 声明。函数可以声明一组输入变量作为参数,调用者通过它向函数传递参数,函数完成后,它再将带有输出类型的结果值返回给调用者。
当一个函数被引用时,该函数会产生一个相应的零尺寸函数项类型(function item type)的一等(first-class)值,调用该值就相当于直接调用该函数。
举个简单的定义函数的例子:
#![allow(unused)] fn main() { fn answer_to_life_the_universe_and_everything() -> i32 { return 42; } }
函数参数
和 let绑定一样,函数参数是不可反驳型模式,所以任何在 let绑定中有效的模式都可以有效应用在函数参数上:
#![allow(unused)] fn main() { fn first((value, _): (i32, i32)) -> i32 { value } }
如果第一个参数是一个SelfParam类型的参数,这表明该函数是一个方法。带 self参数的函数只能在 trait 或实现中作为关联函数出现。
带有 ...token 的参数表示此函数是一个可变参数函数,... 只能作为外部块里的函数的最后一个参数使用。可变参数可以有一个可选标识符,例如 args: ...。
函数体
函数的块在概念上被包装进在一个块中,该块绑定该函数的参数模式,然后返回(return)该函数的块的值。这意味着如果轮到块的*尾部表达式(tail expression)*被求值计算了,该块将结束,求得的值将被返回给调用者。通常,程序执行时如果流碰到函数体中的显式返回表达式(return expression),就会截断那个隐式的最终表达式的执行。
例如,上面例子里函数的行为就像下面被改写的这样:
// argument_0 是调用者真正传过来的第一个参数
let (value, _) = argument_0;
return {
value
};
没有主体块的函数以分号结束。这种形式只能出现在 trait 或外部块中。
泛型函数
泛型函数允许在其签名中出现一个或多个参数化类型(parameterized types)。每个类型参数必须在函数名后面的尖括号封闭逗号分隔的列表中显式声明。
#![allow(unused)] fn main() { // foo 是建立在 A 和 B 基础上的泛型函数 fn foo<A, B>(x: A, y: B) { } }
在函数签名上和函数体内部,类型参数的名称可以被用作类型名。可以为类型参数指定 trait约束,以允许对该类型的值调用这些 trait 的方法。这种约束是使用 where句法指定的:
#![allow(unused)] fn main() { use std::fmt::Debug; fn foo<T>(x: T) where T: Debug { } }
当使用泛型函数时,它的(类型参数的)类型会基于调用的上下文被实例化。例如,这里调用 foo 函数:
#![allow(unused)] fn main() { use std::fmt::Debug; fn foo<T>(x: &[T]) where T: Debug { // 省略细节 } foo(&[1, 2]); }
将用 i32 实例化类型参数 T。
类型参数也可以在函数名之后的末段路径组件(trailing path component,即 ::<type>)中显式地提供。如果没有足够的上下文来确定类型参数,那么这可能是必要的。例如:mem::size_of::<u32>() == 4。
外部函数限定符
外部函数限定符(extern)可以提供那些能通过特定 ABI 才能调用的函数的定义:
extern "ABI" fn foo() { /* ... */ }
这些通常与提供了函数声明的外部块程序项一起使用,这样就可以调用此函数而不必同时提供此函数的定义:
extern "ABI" {
fn foo(); /* no body */
}
unsafe { foo() }
当函数的 FunctionQualifiers 句法规则中的 "extern" Abi?* 选项被省略时,会默认使用 "Rust" 类型的 ABI。例如:
#![allow(unused)] fn main() { fn foo() {} }
等价于:
#![allow(unused)] fn main() { extern "Rust" fn foo() {} }
使用 "Rust" 之外的 ABI 可以让 Rust 中声明的函数被其他编程语言调用。比如下面声明了一个可以从 C 中调用的函数:
#![allow(unused)] fn main() { // 使用 "C" ABI 声明一个函数 extern "C" fn new_i32() -> i32 { 0 } // 使用 "stdcall" ABI声明一个函数 #[cfg(target_arch = "x86_64")] extern "stdcall" fn new_i32_stdcall() -> i32 { 0 } }
与外部块一样,当使用关键字 extern 而省略 "ABI 时,ABI 默认使用的是 "C"。也就是说这个:
#![allow(unused)] fn main() { extern fn new_i32() -> i32 { 0 } let fptr: extern fn() -> i32 = new_i32; }
等价于:
#![allow(unused)] fn main() { extern "C" fn new_i32() -> i32 { 0 } let fptr: extern "C" fn() -> i32 = new_i32; }
非 "Rust" 的 ABI 函数不支持与 Rust 函数完全相同的展开(unwind)方式。因此展开进程过这类 ABI 函数的尾部时就会导致该进程被终止。(译者注:展开是逆向的。)
注意:
rustc背后的 LLVM 是通过执行一个非法指令来实现中止进程的功能的。
常量函数
使用关键字 const 限定的函数是常量(const)函数,元组结构体构造函数和元组变体构造函数也是如此。可以在常量上下文中调用常量函数。
常量函数不允许是 async类型的,并且不能使用 extern函数限定符。
异步函数
函数可以被限定为异步的,这还可以与 unsafe 限定符结合在一起使用:
#![allow(unused)] fn main() { async fn regular_example() { } async unsafe fn unsafe_example() { } }
异步函数在调用时不起作用:相反,它们将参数捕获进一个 future。当该函数被轮询(polled)时,该 future 将执行该函数的函数体。
一个异步函数大致相当于返回一个以 async move块为代码体的 impl Future 的函数:
#![allow(unused)] fn main() { // 源代码 async fn example(x: &str) -> usize { x.len() } }
大致等价于:
#![allow(unused)] fn main() { use std::future::Future; // 脱糖后的 fn example<'a>(x: &'a str) -> impl Future<Output = usize> + 'a { async move { x.len() } } }
实际的脱糖(desugaring)过程相当复杂:
- 脱糖过程中的返回类型被假定捕获了
async fn声明中的所有的生存期参数(包括省略的)。这可以在上面的脱糖示例中看到:(我们)显式地给它补上了一个生存期参数'a,因此捕捉到'a。 - 代码体中的 [
async move块][async blocks]捕获所有函数参数,包括未使用或绑定到_模式的参数。参数销毁方面,除了销毁动作需要在返回的 future 完全执行(fully awaited)完成后才会发生外,可以确保异步函数参数的销毁顺序与函数非异步时的顺序相同。
有关异步效果的详细信息,请参见 async块。
版本差异: 异步函数只能从 Rust 2018 版才开始可用。
async 和 unsafe 的联合使用
声明一个既异步又非安全(unsafe)的函数是合法的。调用这样的函数是非安全的,并且(像任何异步函数一样)会返回一个 future。这个 future 只是一个普通的 future,因此“await”它不需要一个 unsafe 的上下文:
#![allow(unused)] fn main() { // 等待这个返回的 future 相当于解引用 `x`。 // // 安全条件: 在返回的 future 执行完成前,`x` 必须能被安全解引用 async unsafe fn unsafe_example(x: *const i32) -> i32 { *x } async fn safe_example() { // 起初调用上面这个函数时需要一个非安全(`unsafe`)块: let p = 22; let future = unsafe { unsafe_example(&p) }; // 但是这里非安全(`unsafe`)块就没必要了,这里能正常读到 `p`: let q = future.await; } }
请注意,此行为是对返回 impl Future 的函数进行脱糖处理的结果——在本例中,我们脱糖处理生成的函数是一个非安全(unsafe)函数,这个非安全(unsafe)函数返回值与原始定义的函数的返回值仍保持一致。
非安全限定在异步函数上的使用方式与它在其他函数上的使用方式完全相同:只是它表示该函数会要求它的调用者提供一些额外的义务/条件来确保该函数的健全性(soundness)。与任何其他非安全函数一样,这些条件可能会超出初始调用本身——例如,在上面的代码片段中, 函数 unsafe_example 将指针 x 作为参数,然后(在执行 await 时)解引用了对该指针的引用。这意味着在 future 完成执行之前,x 必须是有效的,调用者有责任确保这一点。
函数上的属性
在函数上允许使用外部属性,也允许在函数体中的 { 后面直接放置内部属性。
下面这个例子显示了一个函数的内部属性。该函数的文档中只会出现单词“Example”。
#![allow(unused)] fn main() { fn documented() { #![doc = "Example"] } }
注意:除了 lint检查类属性,函数上一般惯用的还是外部属性。
在函数上有意义的属性是 cfg、cfg_attr、deprecated、doc、export_name、link_section、no_mangle、lint检查类属性、must_use、过程宏属性、测试类属性和优化提示类属性。函数也可以接受属性宏。
函数参数上的属性
函数参数允许使用外部属性,允许的内置属性仅限于 cfg、cfg_attr、allow、warn、deny 和 forbid。
#![allow(unused)] fn main() { fn len( #[cfg(windows)] slice: &[u16], #[cfg(not(windows))] slice: &[u8], ) -> usize { slice.len() } }
应用于程序项的过程宏属性所使用的惰性辅助属性也是允许的,但是要注意不要在最终(输出)的 TokenStream 中包含这些惰性属性。
例如,下面的代码定义了一个未在任何地方正式定义的惰性属性 some_inert_attribute,而 some_proc_macro_attribute 过程宏负责检测它的存在,并从输出 token流 TokenStream 中删除它。
#[some_proc_macro_attribute]
fn foo_oof(#[some_inert_attribute] arg: u8) {
}
类型别名
句法
TypeAlias :
typeIDENTIFIER GenericParams? WhereClause? (=Type )?;761ad774fcb300f2b506fed7b4dbe753cda88d80 类型别名为现有的类型定义一个新名称。类型别名用关键字type声明。每个值都是一个唯一的特定的类型,但是可以实现几个不同的 trait,或者兼容几个不同的类型约束。
例如,下面将类型 Point 定义为类型 (u8, u8) 的同义词/别名:
#![allow(unused)] fn main() { type Point = (u8, u8); let p: Point = (41, 68); }
761ad774fcb300f2b506fed7b4dbe753cda88d80 元组结构体或单元结构体的类型别名不能用于充当该类型的构造函数: 761ad774fcb300f2b506fed7b4dbe753cda88d80
没有使用 Type 来定义的类型别名只能作为 trait 中的关联类型出现。
结构体
句法
Struct :
StructStruct
| TupleStructStructStruct :
structIDENTIFIER GenericParams? WhereClause? ({StructFields?}|;)TupleStruct :
structIDENTIFIER GenericParams?(TupleFields?)WhereClause?;StructFields :
StructField (,StructField)*,?StructField :
OuterAttribute*
Visibility?
IDENTIFIER:TypeTupleFields :
TupleField (,TupleField)*,?TupleField :
OuterAttribute*
Visibility?
Type
结构体是一个使用关键字 struct 定义的标称型(nominal)结构体类型。
结构体(struct)程序项的一个示例和它的使用方法:
#![allow(unused)] fn main() { struct Point {x: i32, y: i32} let p = Point {x: 10, y: 11}; let px: i32 = p.x; }
元组结构体是一个标称型(nominal)元组类型,也是用关键字 struct 定义的。例如:
#![allow(unused)] fn main() { struct Point(i32, i32); let p = Point(10, 11); let px: i32 = match p { Point(x, _) => x }; }
*单元结构体(unit-like struct)*是没有任何字段的结构体,它的定义完全不包含字段(fields)列表。这样的结构体隐式定义了其类型的同名常量(值)。例如:
#![allow(unused)] fn main() { struct Cookie; let c = [Cookie, Cookie {}, Cookie, Cookie {}]; }
等价于
#![allow(unused)] fn main() { struct Cookie {} const Cookie: Cookie = Cookie {}; let c = [Cookie, Cookie {}, Cookie, Cookie {}]; }
结构体的精确内存布局还没有规范下来。目前可以使用 repr属性来指定特定的布局。
枚举
句法
Enumeration :
enumIDENTIFIER GenericParams? WhereClause?{EnumItems?}EnumItems :
EnumItem (,EnumItem )*,?EnumItem :
OuterAttribute* Visibility?
IDENTIFIER ( EnumItemTuple | EnumItemStruct | EnumItemDiscriminant )?EnumItemTuple :
(TupleFields?)EnumItemStruct :
{StructFields?}EnumItemDiscriminant :
=Expression
枚举,英文为 enumeration,常见其简写形式 enum,它同时定义了一个标称型(nominal)枚举类型和一组构造器,这可用于创建或使用模式来匹配相应枚举类型的值。
枚举使用关键字 enum 来声明。
enum 程序项的一个示例和它的使用方法:
#![allow(unused)] fn main() { enum Animal { Dog, Cat, } let mut a: Animal = Animal::Dog; a = Animal::Cat; }
枚举构造器可以带有具名字段或未具名字段:
#![allow(unused)] fn main() { enum Animal { Dog(String, f64), Cat { name: String, weight: f64 }, } let mut a: Animal = Animal::Dog("Cocoa".to_string(), 37.2); a = Animal::Cat { name: "Spotty".to_string(), weight: 2.7 }; }
在这个例子中,Cat 是一个类结构体枚举变体(struct-like enum variant),而 Dog 则被简单地称为枚举变体。每个枚举实例都有一个判别值/判别式(discriminant),它是一个与此枚举实例关联的整数,用来确定它持有哪个变体。可以通过 mem::discriminant 函数来获得对这个判别值的不透明引用。
为无字段枚举自定义判别值
如果枚举的任何变体都没有附加字段,则可以直接设置和访问判别值。
可以使用操作符 as 通过数值转换将这些枚举类型转换为整型。枚举可以可选地指定每个判别值的具体值,方法是在变体名后面追加 = 和常量表达式。如果声明中的第一个变体未指定,则将其判别值设置为零。对于其他未指定的判别值,它比照前一个变体的判别值按 1 递增。
#![allow(unused)] fn main() { enum Foo { Bar, // 0 Baz = 123, // 123 Quux, // 124 } let baz_discriminant = Foo::Baz as u32; assert_eq!(baz_discriminant, 123); }
尽管编译器被允许在实际的内存布局中使用较小的类型,但在默认表形(default representation)下,指定的判别值会被解释为一个 isize 值。也可以使用[原语表形(primitive representation)]或C表形来更改成大小可接受的值。
同一枚举中,两个变体使用相同的判别值是错误的。
#![allow(unused)] fn main() { enum SharedDiscriminantError { SharedA = 1, SharedB = 1 } enum SharedDiscriminantError2 { Zero, // 0 One, // 1 OneToo = 1 // 1 (和前值冲突!) } }
当前一个变体的判别值是当前表形允许的的最大值时,再使用默认判别值就也是错误的。
#![allow(unused)] fn main() { #[repr(u8)] enum OverflowingDiscriminantError { Max = 255, MaxPlusOne // 应该是256,但枚举溢出了 } #[repr(u8)] enum OverflowingDiscriminantError2 { MaxMinusOne = 254, // 254 Max, // 255 MaxPlusOne // 应该是256,但枚举溢出了。 } }
无变体枚举
没有变体的枚举称为零变体枚举/无变体枚举。因为它们没有有效的值,所以不能被实例化。
#![allow(unused)] fn main() { enum ZeroVariants {} }
零变体枚举与 never类型等效,但它不能被强转为其他类型。
#![allow(unused)] fn main() { enum ZeroVariants {} let x: ZeroVariants = panic!(); let y: u32 = x; // 类型不匹配错误 }
变体的可见性
依照句法规则,枚举变体是允许有自己的[可见性(visibility)][Visibility]限定/注解(annotation)的,但当枚举被(句法分析程序)验证(validate)通过后,可见性注解又被弃用。因此,在源码解析层面,允许跨不同的上下文对其中不同类型的程序项使用统一的句法规则进行解析。
#![allow(unused)] fn main() { macro_rules! mac_variant { ($vis:vis $name:ident) => { enum $name { $vis Unit, $vis Tuple(u8, u16), $vis Struct { f: u8 }, } } } // 允许空 `vis`. mac_variant! { E } // 这种也行,因为这段代码在被验证通过前会被移除。 #[cfg(FALSE)] enum E { pub U, pub(crate) T(u8), pub(super) T { f: String } } }
联合体
句法
Union :
unionIDENTIFIER GenericParams? WhereClause?{StructFields}
除了用 union 代替 struct外,联合体声明使用和结构体声明相同的句法。
#![allow(unused)] fn main() { #[repr(C)] union MyUnion { f1: u32, f2: f32, } }
联合体的关键特性是联合体的所有字段共享同一段存储。因此,对联合体的一个字段的写操作会覆盖其他字段,而联合体的尺寸由其尺寸最大的字段的尺寸所决定。
联合体的初始化
可以使用与结构体类型相同的句法创建联合体类型的值,但必须只能指定一个字段:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; }
上面的表达式创建了一个类型为 MyUnion 的值,并使用字段 f1 初始化了其存储。可以使用与结构体字段相同的句法访问联合体:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; let f = unsafe { u.f1 }; }
读写联合体字段
联合体没有“活跃字段(active field)”的概念。相反,每次访问联合体只是用访问所指定的字段的类型解释此联合体的存储。读取联合体的字段就是以当前读取字段的类型来解读此联合体的存储位。字段(间)可以有非零的偏移量存在(使用 C表型的除外);在这种情况下,读取将从字段的相对偏移量的 bit 开始。程序员有责任确保此数据在当前字段类型下有效。否则会导致未定义行为(undefined behavior)。例如,在 bool类型的字段下读取到数值 3 是未定义行为。实际上,对一个 C表型的联合体进行写操作,然后再从中读取,就好比从用于写入的类型到用于读取的类型的 transmute 操作。
因此,所有的联合体字段的读取必须放在非安全(unsafe)块里:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; unsafe { let f = u.f1; } }
对实现了 Copy trait 或 [ManuallyDrop][ManuallyDrop] trait 的联合体字段的写操作不需要事先的析构读操作,也因此这些写操作不必放在非安全(unsafe)块中。1
#![allow(unused)] fn main() { use std::mem::ManuallyDrop; union MyUnion { f1: u32, f2: ManuallyDrop<String> } let mut u = MyUnion { f1: 1 }; // 这些都不是必须要放在 `unsafe` 里的 u.f1 = 2; u.f2 = ManuallyDrop::new(String::from("example")); }
通常,那些用到联合体的程序代码会先在非安全的联合体字段访问操作上提供一层安全包装,然后再使用。
联合体和 Drop
当一个联合体被销毁时,它无法知道需要销毁它的哪些字段。因此,所有联合体的字段都必须实现 Copy trait 或被包装进 ManuallyDrop<_>。这确保了联合体在超出作用域时不需要销毁任何内容。
与结构体和枚举一样,联合体也可以通过 impl Drop 手动定义被销毁时的具体动作。
联合体上的模式匹配
访问联合体字段的另一种方法是使用模式匹配。联合体字段上的模式匹配与结构体上的模式匹配使用相同的句法,只是这种模式只能一次指定一个字段。因为模式匹配就像使用特定字段来读取联合体,所以它也必须被放在非安全(unsafe)块中。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } fn f(u: MyUnion) { unsafe { match u { MyUnion { f1: 10 } => { println!("ten"); } MyUnion { f2 } => { println!("{}", f2); } } } } }
模式匹配可以将联合体作为更大的数据结构的一个字段进行匹配。特别是,当使用 Rust 联合体通过 FFI 实现 C标签联合体(C tagged union)时,这允许同时在标签和相应字段上进行匹配:
#![allow(unused)] fn main() { #[repr(u32)] enum Tag { I, F } #[repr(C)] union U { i: i32, f: f32, } #[repr(C)] struct Value { tag: Tag, u: U, } fn is_zero(v: Value) -> bool { unsafe { match v { Value { tag: Tag::I, u: U { i: 0 } } => true, Value { tag: Tag::F, u: U { f: num } } if num == 0.0 => true, _ => false, } } } }
引用联合体字段
由于联合体字段共享存储,因此拥有对联合体一个字段的写访问权就同时拥有了对其所有其他字段的写访问权。因为这一事实,引用的借用检查规则必须调整。因此,如果联合体的一个字段是被出借,那么在相同的生存期内它的所有其他字段也都处于出借状态。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } // 错误: 不能同时对 `u` (通过 `u.f2`)拥有多余一次的可变借用 fn test() { let mut u = MyUnion { f1: 1 }; unsafe { let b1 = &mut u.f1; // ---- 首次可变借用发生在这里 (通过 `u.f1`) let b2 = &mut u.f2; // ^^^^ 二次可变借用发生在这里 (通过 `u.f2`) *b1 = 5; } // - 首次借用在这里结束 assert_eq!(unsafe { u.f1 }, 5); } }
如您所见,在许多方面(除了布局、安全性和所有权),联合体的行为与结构体完全相同,这很大程度上是因为联合体继承使用了结构体的句法的结果。对于 Rust 语言未明确提及的许多方面(比如隐私性(privacy)、名称解析、类型推断、泛型、trait实现、固有实现、一致性、模式检查等等)也是如此。
这句译者简单理解就是对已经初始化的变量再去覆写的时候要先去读一下这个变量代表的地址上的值的状态,如果有值,并且允许覆写,那 Rust 为防止内存泄漏就先执行那变量的析构行为(drop()),清空那个地址上的关联堆数据,再写入。我们这里对联合体的预设条件是此联合体值有 Copy特性,有 Copy特性了,对值的直接覆写不会造成内存泄漏,就不必调用析构行为,也不需要事先的非安全读操作了。对于这个问题nomicon的“未初始化内存”章有讲述,博主CrLF0710的两篇“学一点 Rust 内存模型会发生什么呢?”里也都有精彩讲解。
常量项
句法
ConstantItem :
const( IDENTIFIER |_):Type (=Expression )?;
常量项是一个可选的具名 常量值,它与程序中的具体内存位置没有关联。无论常量在哪里使用,它们本质上都是内联的,这意味着当它们被使用时,都是直接被拷贝到相关的上下文中来使用的。这包括使用非拷贝(non-Copy)类型的值和来自外部的 crate 的常量。对相同常量的引用不保证它们引用的是相同的内存地址。
常量必须显式指定数据类型。类型必须具有 'static生存期:程序初始化器(initializer)中的任何引用都必须具有 'static生存期。
常量可以引用其他常量的地址,在这种情况下,如果适用,该地址将具有省略的生存期,否则(在大多数情况下)默认为 'static生存期。(请参阅静态生存期省略。)但是,编译器仍有权多次调整转移该常量,因此引用的地址可能并不固定。
#![allow(unused)] fn main() { const BIT1: u32 = 1 << 0; const BIT2: u32 = 1 << 1; const BITS: [u32; 2] = [BIT1, BIT2]; const STRING: &'static str = "bitstring"; struct BitsNStrings<'a> { mybits: [u32; 2], mystring: &'a str, } const BITS_N_STRINGS: BitsNStrings<'static> = BitsNStrings { mybits: BITS, mystring: STRING, }; }
常量表达式只能在trait定义中省略。
Constants with Destructors
常量与析构函数
常量可以包含析构函数。析构函数在值超出作用域时运行。1
#![allow(unused)] fn main() { struct TypeWithDestructor(i32); impl Drop for TypeWithDestructor { fn drop(&mut self) { println!("Dropped. Held {}.", self.0); } } const ZERO_WITH_DESTRUCTOR: TypeWithDestructor = TypeWithDestructor(0); fn create_and_drop_zero_with_destructor() { let x = ZERO_WITH_DESTRUCTOR; // x 在函数的结尾处通过调用 drop 方法被销毁。 // 打印出 "Dropped. Held 0.". } }
Unnamed constant
未命名常量
不同于关联常量(associated constant),自由常量(free constant)可以使用下划线来命名。例如:
#![allow(unused)] fn main() { const _: () = { struct _SameNameTwice; }; // OK 尽管名称和上面的一样: const _: () = { struct _SameNameTwice; }; }
与下划线导入一样,宏可以多次安全地在同一作用域中扩展出相同的未具名常量。例如,以下内容不应该产生错误:
#![allow(unused)] fn main() { macro_rules! m { ($item: item) => { $item $item } } m!(const _: () = ();); // 这会展开出: // const _: () = (); // const _: () = (); }
在程序退出前,析构销毁的只是其中的一份拷贝;这句还有另一层含义是常量在整个程序结束时会调用析构函数。
静态项
句法
StaticItem :
staticmut? IDENTIFIER:Type (=Expression )?;
静态项类似于常量项,除了它在程序中表示一个精确的内存位置。所有对静态项的引用都指向相同的内存位置。静态项拥有 'static 生存期,它比 Rust 程序中的所有其他生存期都要长。静态项不会在程序结束时调用析构动作 drop。
静态初始化器是在编译时求值的常量表达式。静态初始化器可以引用其他静态项。
包含非内部可变类型的非 mut 静态项可以放在只读内存中。
所有访问静态项的操作都是安全的,但对静态项有一些限制:
- 静态项的数据类型必须有
Synctrait约束,这样才可以让线程安全地访问。 - 常量项不能引用静态项。
必须为自由静态项提供初始化表达式,但在外部块中静态项必须省略初始化表达式。
可变静态项
如果静态项是用关键字 mut 声明的,则它允许被程序修改。Rust 的目标之一是使尽可能的避免出现并发 bug,那允许修改可变静态项显然是竞态(race conditions)或其他 bug 的一个重要来源。因此,读取或写入可变静态项变量时需要引入非安全(unsafe)块。应注意确保对可变静态项的修改相对于运行在同一进程中的其他线程来说是安全的。
尽管有这些缺点,可变静态项仍然非常有用。它们可以与 C库一起使用,也可以在外部(extern)块中从 C库中来绑定它。
#![allow(unused)] fn main() { fn atomic_add(_: &mut u32, _: u32) -> u32 { 2 } static mut LEVELS: u32 = 0; // 这违反了不共享状态的思想,而且它在内部不能防止竞争,所以这个函数是非安全的(`unsafe`) unsafe fn bump_levels_unsafe1() -> u32 { let ret = LEVELS; LEVELS += 1; return ret; } // 这里我们假设有一个返回旧值的 atomic_add 函数,这个函数是“安全的(safe)”, // 但是返回值可能不是调用者所期望的,所以它仍然被标记为 `unsafe` unsafe fn bump_levels_unsafe2() -> u32 { return atomic_add(&mut LEVELS, 1); } }
除了可变静态项的类型不需要实现 Sync trait 之外,可变静态项与普通静态项具有相同的限制。
使用常量项或静态项
应该使用常量项还是应该使用静态项可能会令人困惑。一般来说,常量项应优先于静态项,除非以下情况之一成立:
- 存储大量数据
- 需要静态项的存储地址不变的特性。
- 需要内部可变性。
Trait
句法
Trait :
unsafe?traitIDENTIFIER GenericParams? (:TypeParamBounds? )? WhereClause?{
InnerAttribute*
AssociatedItem*
}
trait 描述类型可以实现的抽象接口。这类接口由三种关联程序项(associated items)组成,它们分别是:
所有 trait 都定义了一个隐式类型参数 Self ,它指向“实现此接口的类型”。trait 还可能包含额外的类型参数。这些类型参数,包括 Self 在内,都可能会跟正常类型参数一样受到其他 trait 的约束。
trait 需要具体的类型去实现,具体的实现方法是通过该类型的各种独立实现(implementations)来完成的。
trait函数可以通过使用分号代替函数体来省略函数体。这表明此 trait的实现必须去定义实现该函数。如果 trait函数定义了一个函数体,那么这个定义就会作为任何不覆盖它的实现的默认函数实现。类似地,关联常量可以省略等号和表达式,以指示相应的实现必须定义该常量值。关联类型不能定义类型,只能在实现中指定类型。
#![allow(unused)] fn main() { // 有定义和没有定义的相关联trait项的例子 trait Example { const CONST_NO_DEFAULT: i32; const CONST_WITH_DEFAULT: i32 = 99; type TypeNoDefault; fn method_without_default(&self); fn method_with_default(&self) {} } }
Trait bounds
trait约束
泛型程序项可以使用 trait 作为其类型参数的约束。
Generic Traits
泛型trait
可以为 trait 指定类型参数来使该 trait 成为泛型trait/泛型类型。这些类型参数出现在 trait 名称之后,使用与泛型函数相同的句法。
#![allow(unused)] fn main() { trait Seq<T> { fn len(&self) -> u32; fn elt_at(&self, n: u32) -> T; fn iter<F>(&self, f: F) where F: Fn(T); } }
Object Safety
对象安全条款
对象安全的 trait 可以是 trait对象的底层 trait。如果 trait 符合以下限定条件(在 RFC 255 中定义),则认为它是对象安全的(object safe):
- 所有的超类traitsupertraits 也必须也是对象安全的。
- 超类trait 中不能有
Sized。也就是说不能有Self: Sized约束。 - 它必须没有任何关联常量。
- 所有关联函数必须可以从 trait对象调度分派,或者是显式不可调度分派:
- 可调度分派函数要求:
- 显式不可调度分派函数要求:
- 有
where Self: Sized约束(即接受者的类型Self(例如:self) 有Sized约束)。
- 有
#![allow(unused)] fn main() { use std::rc::Rc; use std::sync::Arc; use std::pin::Pin; // 对象安全的 trait 方法。 trait TraitMethods { fn by_ref(self: &Self) {} fn by_ref_mut(self: &mut Self) {} fn by_box(self: Box<Self>) {} fn by_rc(self: Rc<Self>) {} fn by_arc(self: Arc<Self>) {} fn by_pin(self: Pin<&Self>) {} fn with_lifetime<'a>(self: &'a Self) {} fn nested_pin(self: Pin<Arc<Self>>) {} } struct S; impl TraitMethods for S {} let t: Box<dyn TraitMethods> = Box::new(S); }
#![allow(unused)] fn main() { // 此 trait 是对象安全的,但不能在 trait对象上分发(dispatch)使用这些方法。 trait NonDispatchable { // 非方法不能被分发。 fn foo() where Self: Sized {} // 在运行之前 Self 类型未知。 fn returns(&self) -> Self where Self: Sized; // `other` 可能是另一具体类型的接受者。 fn param(&self, other: Self) where Self: Sized {} // 泛型与虚函数指针表(Virtual Function Pointer Table, vtable)不兼容。 fn typed<T>(&self, x: T) where Self: Sized {} } struct S; impl NonDispatchable for S { fn returns(&self) -> Self where Self: Sized { S } } let obj: Box<dyn NonDispatchable> = Box::new(S); obj.returns(); // 错误: 不能调用带有 Self 返回类型的方法 obj.param(S); // 错误: 不能调用带有 Self 类型的参数的方法 obj.typed(1); // 错误: 不能调用带有泛型类型参数的方法 }
#![allow(unused)] fn main() { use std::rc::Rc; // 非对象安全的 trait trait NotObjectSafe { const CONST: i32 = 1; // 错误: 不能有关联常量 fn foo() {} // 错误: 关联函数没有 `Sized` 约束 fn returns(&self) -> Self; // 错误: `Self` 在返回类型中 fn typed<T>(&self, x: T) {} // 错误: 泛型类型参数 fn nested(self: Rc<Box<Self>>) {} // 错误: 嵌套接受者还未被完全支持。(译者注:有限支持见上面的补充规则。) } struct S; impl NotObjectSafe for S { fn returns(&self) -> Self { S } } let obj: Box<dyn NotObjectSafe> = Box::new(S); // 错误 }
#![allow(unused)] fn main() { // Self: Sized trait 非对象安全的。 trait TraitWithSize where Self: Sized {} struct S; impl TraitWithSize for S {} let obj: Box<dyn TraitWithSize> = Box::new(S); // 错误 }
#![allow(unused)] fn main() { // 如果有 `Self` 这样的泛型参数,那 trait 就是非对象安全的 trait Super<A> {} trait WithSelf: Super<Self> where Self: Sized {} struct S; impl<A> Super<A> for S {} impl WithSelf for S {} let obj: Box<dyn WithSelf> = Box::new(S); // 错误: 不能使用 `Self` 作为类型参数 }
Supertraits
超类trait
超类trait 是类型为了实现某特定 trait 而需要一并实现的 trait。此外,在任何地方,如果泛型或 trait对象被某个 trait约束,那这个泛型或 trait对象就可以访问这个超类trait 的关联程序项。
超类trait 是通过 trait 的 Self类型上的 trait约束来声明的,并且通过这种声明 trait约束的方式来传递这种超类trait 关系。一个 trait 不能是它自己的超类trait。
有超类trait 的 trait 称其为其超类trait 的子trait(subtrait)。
下面是一个声明 Shape 是 Circle 的超类trait 的例子。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle : Shape { fn radius(&self) -> f64; } }
下面是同一个示例,除了改成使用 where子句来等效实现。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle where Self: Shape { fn radius(&self) -> f64; } }
下面例子通过 Shape 的 area 函数为 radius 提供了一个默认实现:
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle where Self: Shape { fn radius(&self) -> f64 { // 因为 A = pi * r^2,所以通过代数推导得:r = sqrt(A / pi) (self.area() /std::f64::consts::PI).sqrt() } } }
下一个示例调用了一个泛型参数的超类trait 上的方法。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle : Shape { fn radius(&self) -> f64; } fn print_area_and_radius<C: Circle>(c: C) { // 这里我们调用 `Circle` 的超类trait 的 area 方法。 println!("Area: {}", c.area()); println!("Radius: {}", c.radius()); } }
类似地,这里是一个在 trait对象上调用超类trait 的方法的例子。
#![allow(unused)] fn main() { trait Shape { fn area(&self) -> f64; } trait Circle : Shape { fn radius(&self) -> f64; } struct UnitCircle; impl Shape for UnitCircle { fn area(&self) -> f64 { std::f64::consts::PI } } impl Circle for UnitCircle { fn radius(&self) -> f64 { 1.0 } } let circle = UnitCircle; let circle = Box::new(circle) as Box<dyn Circle>; let nonsense = circle.radius() * circle.area(); }
Unsafe traits
非安全trait
以关键字 unsafe 开头的 trait程序项表示实现该 trait 可能是非安全的。使用正确实现的非安全trait 是安全的。trait实现也必须以关键字 unsafe 开头。
Parameter patterns
参数模式
(trait 中)没有代码体的函数声明或方法声明(的参数模式)只允许使用标识符/IDENTIFIER模式 或 _ 通配符模式。当前 mut IDENTIFIER 还是允许的,但已被弃用,未来将成为一个硬编码错误(hard error)。
在 2015 版中,trait 的函数或方法的参数模式是可选的:
#![allow(unused)] fn main() { trait T { fn f(i32); // 不需要参数的标识符. } }
所有的参数模式被限制为下述之一:
- IDENTIFIER
mutIDENTIFIER_&IDENTIFIER&&IDENTIFIER
(跟普通函数一样,)从 2018 版开始,(trait 中)函数或方法的参数模式不再是可选的。同时,也跟普通函数一样,(trait 中)函数或方法只要有代码体,其参数模式可以是任何不可反驳型模式。但如果没有代码体,上面列出的限制仍然有效。
#![allow(unused)] fn main() { trait T { fn f1((a, b): (i32, i32)) {} fn f2(_: (i32, i32)); // 没有代码体不能使用元组模式。 } }
Item visibility
程序项的可见性
依照句法规定,trait程序项在语法上允许使用 Visibility句法的注释,但是当 trait 被(句法法分析程序)验证(validate)后,该可见性注释又被弃用。因此,在源码解析层面,可以在使用程序项的不同上下文中使用统一的语法对这些程序项进行解析。例如,空的 vis 宏匹配段选择器可以用于 trait程序项,而在其他允许使用非空可见性的情况下,也可使用这同一套宏规则。
macro_rules! create_method { ($vis:vis $name:ident) => { $vis fn $name(&self) {} }; } trait T1 { // 只允许空 `vis`。 create_method! { method_of_t1 } } struct S; impl S { // 这里允许使用非空可见性。 create_method! { pub method_of_s } } impl T1 for S {} fn main() { let s = S; s.method_of_t1(); s.method_of_s(); }
两点提醒:所有 trait 都定义了一个隐式类型参数 Self ,它指向“实现此接口的类型”;trait 的 Self 默认满足:Self: ?Sized
实现
句法
Implementation :
InherentImpl | TraitImplInherentImpl :
implGenericParams? Type WhereClause?{
InnerAttribute*
AssociatedItem*
}TraitImpl :
unsafe?implGenericParams?!? TypePathforType
WhereClause?
{
InnerAttribute*
AssociatedItem*
}
实现是将程序项与*实现类型(implementing type)*关联起来的程序项。实现使用关键字 impl 定义,它包含了属于当前实现的类型的实例的函数,或者包含了当前实现的类型本身的静态函数。
有两种类型的实现:
- 固有实现(inherent implementations)
- trait实现(trait implementations)
固有实现
固有实现被定义为一段由关键字 impl,泛型类型声明,指向标称类型(nominal type)的路径,一个 where子句和一对花括号括起来的一组*类型关联项(associable items)*组成的序列。
(这里)标称类型也被称作实现类型(implementing type);类型关联项(associable items)可理解为实现类型的各种关联程序项(associated items)。
固有实现将其包含的程序项与其的实现类型关联起来。固有实现可以包含关联函数(包括方法)和关联常量。固有实现不能包含关联类型别名。
关联程序项的路径是其实现类型的所有(形式的)路径中的任一种,然后再拼接上这个关联程序项的标识符来作为整个路径的末段路径组件(final path component)。
类型可以有多个固有实现。但作为原始类型定义的实现类型必须与这些固有实现处在同一个 crate 里。
pub mod color { // 译者添加的注释:这个结构体是 Color 的原始类型,是一个标称类型,也是后面两个实现的实现类型 pub struct Color(pub u8, pub u8, pub u8); // 译者添加的注释:这个实现是 Color 的固有实现 impl Color { // 译者添加的注释:类型关联项(associable items) pub const WHITE: Color = Color(255, 255, 255); } } mod values { use super::color::Color; // 译者添加的注释:这个实现也是 Color 的固有实现 impl Color { // 译者添加的注释:类型关联项(associable items) pub fn red() -> Color { Color(255, 0, 0) } } } pub use self::color::Color; fn main() { // 实现类型 和 固有实现 在同一个模块下。 color::Color::WHITE; // 固有实现和类型声明不在同一个模块下,此时对通过固有实现关联进的程序项的存取仍通过指向实现类型的路径 color::Color::red(); // 实现类型重导出后,使用这类快捷路径效果也一样。 Color::red(); // 这个不行, 因为 `values` 非公有。 // values::Color::red(); }
Trait Implementations
trait实现
trait实现的定义与固有实现类似,只是可选的泛型类型声明后须跟一个 trait,再后跟关键字 for,之后再跟一个指向标称类型的路径。
这里讨论的 trait 也被称为被实现trait(implemented trait)。实现类型去实现该被实现trait。
trait实现必须去定义被实现trait 声明里的所有非默认关联程序项,可以重新定义被实现trait 定义的默认关联程序项,但不能定义任何其他程序项。
关联程序项的完整路径为 < 后跟实现类型的路径,再后跟 as,然后是指向 trait 的路径,再后跟 >,这整体作为一个路径组件,然后再后接关联程序项自己的路径组件。
非安全(unsafe) trait 需要 trait实现以关键字 unsafe 开头。
#![allow(unused)] fn main() { #[derive(Copy, Clone)] struct Point {x: f64, y: f64}; type Surface = i32; struct BoundingBox {x: f64, y: f64, width: f64, height: f64}; trait Shape { fn draw(&self, Surface); fn bounding_box(&self) -> BoundingBox; } fn do_draw_circle(s: Surface, c: Circle) { } struct Circle { radius: f64, center: Point, } impl Copy for Circle {} impl Clone for Circle { fn clone(&self) -> Circle { *self } } impl Shape for Circle { fn draw(&self, s: Surface) { do_draw_circle(s, *self); } fn bounding_box(&self) -> BoundingBox { let r = self.radius; BoundingBox { x: self.center.x - r, y: self.center.y - r, width: 2.0 * r, height: 2.0 * r, } } } }
trait实现的一致性
一个 trait实现如果未通过孤儿规则(orphan rules)检查或有 trait实现重叠(implementations overlap)发生,则认为 trait实现不一致(incoherent)。
当两个实现各自的 trait接口集之间存在非空交集时即为这两个 trait实现 重叠了,(这种常情况下)这两个 trait实现可以用相同的类型来实例化。
孤儿规则
给定 impl<P1..=Pn> Trait<T1..=Tn> for T0,只有以下至少一种情况为真时,此 impl 才能成立:
-
Trait是一个本地 trait -
以下所有
(译者注:为理解上面两条规则,举几个例子、
impl<T> ForeignTrait<LocalType> for ForeignType<T>这样的实现也是被允许的,而impl<Vec<T>> ForeignTrait<LocalType> for ForeignType<T>和impl<T> ForeignTrait<Vec<T>> for ForeignType<T>不被允许。)
这里只有无覆盖类型参数的外观被限制(译者注:为方便理解“无覆盖类型参数”,译者提醒读者把它想象成上面译者举例中的 T)。注意,理解“无覆盖类型参数”时需要注意:为了保持一致性,基本类型虽然外观形式特殊,但仍不认为是有覆盖的,比如 Box<T> 中的 T 就不认为是有覆盖的,Box<LocalType> 这样的就被认为是本地的。
泛型实现
实现可以带有泛型参数,这些参数可用在此实现中的其他地方。实现里的泛型参数直接写在关键字 impl 之后。
#![allow(unused)] fn main() { trait Seq<T> { fn dummy(&self, _: T) { } } impl<T> Seq<T> for Vec<T> { /* ... */ } impl Seq<bool> for u32 { /* 将整数处理为 bits 序列 */ } }
如果泛型参数在以下情况下出现过,则其就能约束某个实现:
- 需要被实现的 trait中存在泛型参数
- 实现类型中存在泛型参数
- 作为类型的约束中的[关联类型],该类型包含另一个约束实现的形参。
类型和常量参数必须能在相应实现里体现出约束逻辑。如果关联类型中有生存期参数在使用,则该生存期的约束意义也必须在实现中体现。
约束必须被传递实现的例子:
#![allow(unused)] fn main() { trait Trait{} trait GenericTrait<T> {} trait HasAssocType { type Ty; } struct Struct; struct GenericStruct<T>(T); struct ConstGenericStruct<const N: usize>([(); N]); // T 通过作为 GenericTrait 的参数来体现约束 impl<T> GenericTrait<T> for i32 { /* ... */ } // T 是作为 GenericStruct 的参数来体现约束 impl<T> Trait for GenericStruct<T> { /* ... */ } // 同样,N 作为 ConstGenericStruct 的参数来体现约束 impl<const N: usize> Trait for ConstGenericStruct<N> { /* ... */ } // T 通过类型 `U` 内的关联类型来体现约束,而 `U` 本身就是一个trait的泛型参数 impl<T, U> GenericTrait<U> for u32 where U: HasAssocType<Ty = T> { /* ... */ } // 这个和前面一样,除了类型变为 `(U, isize)`。`U` 出现在包含 `T` 的类型中,而不是类型本身。 impl<T, U> GenericStruct<U> where (U, isize): HasAssocType<Ty = T> { /* ... */ } }
约束未能被传递实现的例子:
#![allow(unused)] fn main() { // 下面的都是错误的,因为它们的类型或常量参数没能被传递和实现约束 // T没有实现约束,因为实现内部根本就没有出现 T impl<T> Struct { /* ... */ } // 同样 N 也没有可能被实现 impl<const N: usize> Struct { /* ... */ } // 在实现中使用了 T,但却并不约束此实现 impl<T> Struct { fn uses_t(t: &T) { /* ... */ } } // 在 U 的约束中,T 被用作关联类型,但 U 本身在 Struct 里无法被约束 impl<T, U> Struct where U: HasAssocType<Ty = T> { /* ... */ } // T 是在约束中使用了,但不是作为关联类型使用的,所以它没有实现约束 impl<T, U> GenericTrait<U> for u32 where U: GenericTrait<T> {} }
允许的非约束生存期参数的示例:
#![allow(unused)] fn main() { struct Struct; impl<'a> Struct {} }
不允许的非约束生存期参数的示例:
#![allow(unused)] fn main() { struct Struct; trait HasAssocType { type Ty; } impl<'a> HasAssocType for Struct { type Ty = &'a Struct; } }
实现上的属性
实现可以在关键字 impl 之前引入外部属性,在代码体内引入内部属性。内部属性必须位于任何关联程序项之前。这里有意义的属性有 cfg、deprecated、doc 和 lint检查类属性。
外部块
句法
ExternBlock :
unsafe?externAbi?{
InnerAttribute*
ExternalItem*
}ExternalItem :
OuterAttribute* (
MacroInvocationSemi
| ( Visibility? ( StaticItem | Function ) )
)
外部块提供未在当前 crate 中定义的程序项的声明,外部块是 Rust 外部函数接口的基础。这其实是某种意义上的不受安全检查的导入入口。
外部块里允许存在两种形式的程序项声明:函数和静态项。只有在非安全(unsafe)上下文中才能调用在外部块中声明的函数或访问在外部块中声明的静态项。
在句法上,关键字 unsafe 允许出现在关键字 extern 之前,但是在语义层面却会被弃用。这种设计允许宏在将关键字 unsafe 从 token流中移除之前利用此句法来使用此关键字。
函数
外部块中的函数与其他 Rust函数的声明方式相同,但这里的函数不能有函数体,取而代之的是直接以分号结尾。外部块中的函数的参数不允许使用模式,只能使用标识符(IDENTIFIER) 或 _ 。函数限定符(const、async、unsafe 和 extern)也不允许在这里使用。
外部块中的函数可以被 Rust 代码调用,就跟调用在 Rust 中定义的函数一样。Rust 编译器会自动在 Rust ABI 和外部 ABI 之间进行转换。
在外部块中声明的函数隐式为非安全(unsafe)的。当强转为函数指针时,外部块中声明的函数的类型就为 unsafe extern "abi" for<'l1, ..., 'lm> fn(A1, ..., An) -> R,其中 'l1,…'lm 是其生存期参数,A1,…,An 是该声明的参数的类型,R 是该声明的返回类型。
静态项
静态项在外部块内部与在外部块之外的声明方式相同,只是在外部块内部声明的静态项没有对应的初始化表达式。访问外部块中声明的静态项是 unsafe 的,不管它是否可变,因为没有任何保证来确保静态项的内存位模式(bit pattern)对声明它的类型是有效的,因为初始化这些静态项的可能是其他的任意外部代码(例如 C)。
就像外部块之外的静态项,外部静态项可以是不可变的,也可以是可变的。在执行任何 Rust 代码之前,不可变外部静态项必须被初始化。也就是说,对于外部静态项,仅在 Rust 代码读取它之前对它进行初始化是不够的。
ABI
不指定 ABI 字符串的默认情况下,外部块会假定使用指定平台上的标准 C ABI 约定来调用当前的库。其他的 ABI 约定可以使用字符串 abi 来指定,具体如下所示:
#![allow(unused)] fn main() { // 到 Windows API 的接口。(译者注:指定使用 stdcall调用约定去调用 Windows API) extern "stdcall" { } }
有三个 ABI 字符串是跨平台的,并且保证所有编译器都支持它们:
extern "Rust"-- 在任何 Rust 语言中编写的普通函数fn foo()默认使用的 ABI。extern "C"-- 这等价于extern fn foo();无论您的 C编译器支持什么默认 ABI。extern "system"-- 在 Win32 平台之外,中通常等价于extern "C"。在 Win32 平台上,应该使用"stdcall",或者其他应该使用的 ABI 字符串来链接它们自身的 Windows API。
还有一些特定于平台的 ABI 字符串:
extern "cdecl"-- 通过 FFI 调用 x86_32 C 资源所使用的默认调用约定。extern "stdcall"-- 通过 FFI 调用 x86_32架构下的 Win32 API 所使用的默认调用约定extern "win64"-- 通过 FFI 调用 x86_64 Windows 平台下的 C 资源所使用的默认调用约定。extern "sysv64"-- 通过 FFI 调用 非Windows x86_64 平台下的 C 资源所使用的默认调用约定。extern "aapcs"--通过 FFI 调用 ARM 接口所使用的默认调用约定extern "fastcall"--fastcallABI——对应于 MSVC 的__fastcall和 GCC 以及 clang 的__attribute__((fastcall))。extern "vectorcall"--vectorcallABI ——对应于 MSVC 的__vectorcall和 clang 的__attribute__((vectorcall))。
可变参数函数
可以在外部块内的函数的参数列表中的一个或多个具名参数后通过引入 ... 来让该函数成为可变参数函数。注意可变参数... 前至少有一个具名参数,并且只能位于参数列表的最后。可变参数可以通过标识符来指定:
#![allow(unused)] fn main() { extern "C" { fn foo(x: i32, ...); fn with_name(format: *const u8, args: ...); } }
外部块上的属性
下面列出的属性可以控制外部块的行为。
link属性
link属性为外部(extern)块中的程序项指定编译器应该链接的本地库的名称。它使用 MetaListNameValueStr元项属性句法指定其输入参数。name键指定要链接的本地库的名称。kind键是一个可选值,它指定具有以下可选值的库类型:
dylib— 表示库类型是动态库。如果没有指定kind,这是默认值。static— 表示库类型是静态库。framework— 表示库类型是 macOS 框架。这只对 macOS 目标平台有效。
如果指定了 kind键,则必须指定 name键。
当从主机环境导入 symbols 时,wasm_import_module键可用于为外部(extern)块中的程序项指定 WebAssembly模块名称。如果未指定 wasm_import_module,则默认模块名为 env。
#[link(name = "crypto")]
extern {
// …
}
#[link(name = "CoreFoundation", kind = "framework")]
extern {
// …
}
#[link(wasm_import_module = "foo")]
extern {
// …
}
在空外部块上添加 link属性是有效的。可以用这种方式来满足代码中其他地方的外部块的链接需求(包括上游 crate),而不必向每个外部块都添加此属性。
link_name属性
可以在外部(extern)块内的程序项声明上指定 link_name属性,可以用它来指示要为给定函数或静态项导入的具体 symbol。它使用 MetaNameValueStr元项属性句法指定 symbol 的名称。
#![allow(unused)] fn main() { extern { #[link_name = "actual_symbol_name"] fn name_in_rust(); } }
函数参数上的属性
外部函数参数上的属性遵循与常规函数参数相同的规则和限制。
类型参数和生存期参数
句法
GenericParams :
<>
|<(GenericParam,)* GenericParam,?>GenericParam :
[OuterAttribute]* ( LifetimeParam | TypeParam | ConstParam )LifetimeParam :
[LIFETIME_OR_LABEL] (:[LifetimeBounds] )?TypeParam :
[IDENTIFIER](:[TypeParamBounds]? )? (=[Type] )?ConstParam:
const[IDENTIFIER]:[Type]
函数、类型别名、结构体、枚举、联合体、trait 和实现可以通过类型参数、常量参数和生存期参数达到参数化配置的的效果。这些参数在尖括号(<...>)中列出,通常都是紧跟在程序项名称之后和程序项的定义之前。对于实现,因为它没有名称,那它们就直接位于关键字 impl 之后。泛型参数的申明顺序是生存期参数在最前面,然后是类型参数,最后是常量参数。
下面给出一些带类型参数、常量参数和生存期参数的程序项的示例:
#![allow(unused)] fn main() { fn foo<'a, T>() {} trait A<U> {} struct Ref<'a, T> where T: 'a { r: &'a T } struct InnerArray<T, const N: usize>([T; N]); }
泛型参数在声明它们的程序项定义的范围内有效。它们不是函数体中声明的程序项,这个在程序项声明中有讲述。
引用、裸指针、数组、切片、元组和函数指针也有生存期参数或类型参数,但这些程序项不能使用路径句法去引用。
常量泛型
常量泛型参数允许程序项在常量值上泛型化。const标识符为常量参数引入了一个名称,并且该程序项的所有实例必须用给定类型的值去实例化该参数。
常量参数类型值允许为:u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, char 和 bool 这些类型。
常量参数可以在任何可以使用常量项的地方使用,但在类型或数组定义中的重复表达式中使用时,必须如下所述是独立的。也就是说,它们可以在以下地方上允许:
- 可以用于类型内部,用它来构成所涉及的程序项签名的一部分。
- 作为常量表达式的一部分,用于定义关联常量项,或作为关联类型的形参。
- 作为程序项里的任何函数体中的任何运行时表达式中的值。
- 作为程序项中任何函数体中使用到的任何类型的参数。
- 作为程序项中任何字段类型的一部分使用。
#![allow(unused)] fn main() { // 可以使用常量泛型参数的示例。 // 用于程序项本身的签名 fn foo<const N: usize>(arr: [i32; N]) { // 在函数体中用作类型。 let x: [i32; N]; // 用作表达。 println!("{}", N * 2); } // 用作结构体的字段 struct Foo<const N: usize>([i32; N]); impl<const N: usize> Foo<N> { // 用作关联常数 const CONST: usize = N * 4; } trait Trait { type Output; } impl<const N: usize> Trait for Foo<N> { // 用作关联类型 type Output = [i32; N]; } }
#![allow(unused)] fn main() { // 不能使用常量泛型参数的示例 fn foo<const N: usize>() { // 能在函数体中的程序项定义中使用 const BAD_CONST: [usize; N] = [1; N]; static BAD_STATIC: [usize; N] = [1; N]; fn inner(bad_arg: [usize; N]) { let bad_value = N * 2; } type BadAlias = [usize; N]; struct BadStruct([usize; N]); } }
作为进一步的限制,常量只能作为类型或数组定义中的重复表达式中的独立实参出现。在这种上下文限制下,它们只能以单段路径表达式的形式使用(例如 N 或以块{N} 的形式出现)。也就是说,它们不能与其他表达式结合使用。
#![allow(unused)] fn main() { // 不能使用常量参数的示例。 // 不允许在类型中的表达式中组合使用,例如这里的返回类型中的算术表达式 fn bad_function<const N: usize>() -> [u8; {N + 1}] { // 同样的,这种情况也不允许在数组定义里的重复表达式中使用 [1; {N + 1}] } }
路径中的常量实参指定了该程序项使用的常量值。实参必须是常量形参所属类型的常量表达式。常量表达式必须是块表达式(用花括号括起来),除非它是单独路径段(一个[标识符][IDENTIFIER])或一个字面量(此字面量可以是以 - 打头的 token)。
注意:这种句法限制是必要的,用以避免在解析类型内部的表达式时可能会导致无限递归(infinite lookahead)。
#![allow(unused)] fn main() { fn double<const N: i32>() { println!("doubled: {}", N * 2); } const SOME_CONST: i32 = 12; fn example() { // 常量参数的使用示例。 double::<9>(); double::<-123>(); double::<{7 + 8}>(); double::<SOME_CONST>(); double::<{ SOME_CONST + 5 }>(); } }
当存在歧义时,如果泛型参数可以同时被解析为类型或常量参数,那么它总是被解析为类型。在块表达式中放置实参可以强制将其解释为常量实参。
#![allow(unused)] fn main() { type N = u32; struct Foo<const N: usize>; // 下面用法是错误的,因为 `N` 被解释为类型别名。 fn foo<const N: usize>() -> Foo<N> { todo!() } // ERROR // 可以使用花括号来强制将 `N` 解释为常量形参。 fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ok }
与类型参数和生存期参数不同,常量参数可以声明而不必在被它参数化的程序项中使用,但和泛型实现关联的实现例外:
#![allow(unused)] fn main() { // ok struct Foo<const N: usize>; enum Bar<const M: usize> { A, B } // ERROR: 参数未使用 struct Baz<T>; struct Biz<'a>; struct Unconstrained; impl<const N: usize> Unconstrained {} }
当处理 trait约束时,在确定是否满足相关约束时,不会考虑常量参数的所有实现的穷尽性。例如,在下面的例子中,即使实现了 bool类型的所有可能的常量值,仍会报错提示 trait约束不满足。
#![allow(unused)] fn main() { struct Foo<const B: bool>; trait Bar {} impl Bar for Foo<true> {} impl Bar for Foo<false> {} fn needs_bar(_: impl Bar) {} fn generic<const B: bool>() { let v = Foo::<B>; needs_bar(v); // ERROR: trait约束 `Foo<B>: Bar` 未被满足 } }
where子句
句法
WhereClause :
where( WhereClauseItem,)* WhereClauseItem ?WhereClauseItem :
LifetimeWhereClauseItem
| TypeBoundWhereClauseItemLifetimeWhereClauseItem :
[Lifetime]:[LifetimeBounds]TypeBoundWhereClauseItem :
ForLifetimes? [Type]:[TypeParamBounds]?ForLifetimes :
for<GenericParams>
where子句提供了另一种方法来为类型参数和生存期参数指定约束(bound),甚至可以为非类型参数的类型指定约束。
关键字for 可以用来引入高阶生存期参数。它只允许在 [LifetimeParam] 参数上使用。
定义程序项时,其约束没有使用程序项的泛型参数或高阶生存期参数,这样是可以通过编译器的安全检查,但这样的做法将必然导致错误。
在定义程序项时,编译器还会检查某些泛型参数的类型是否存在 Copy、Clone 和 Sized 这些约束。将Copy 或 Clone 作为可变引用、trait object或slice这些程序项的约束是错误的,或将 Sized 作为 trait对象或切片的约束也是错误的。
#![allow(unused)] fn main() { struct A<T> where T: Iterator, // 可以用 A<T: Iterator> 来替代 T::Item: Copy, String: PartialEq<T>, i32: Default, // 允许,但没什么用 i32: Iterator, // 错误: 此 trait约束不适合,i32 没有实现 Iterator [T]: Copy, // 错误: 此 trait约束不适合,切片上不能有此 trait约束 { f: T, } }
属性
泛型生存期参数和泛型类型参数允许属性,但在目前这个位置还没有任何任何有意义的内置属性,但用户可能可以通过自定义的派生属性来设置一些有意义的属性。
下面示例演示如何使用自定义派生属性修改泛型参数的含义。
// 假设 MyFlexibleClone 的派生项将 `my_flexible_clone` 声明为它可以理解的属性。
#[derive(MyFlexibleClone)]
struct Foo<#[my_flexible_clone(unbounded)] H> {
a: *const H
}
关联项
句法
AssociatedItem :
OuterAttribute* (
MacroInvocationSemi
| ( Visibility? ( TypeAlias | ConstantItem | Function ) )
)
关联程序项是在 traits 中声明或在实现中定义的程序项。之所以这样称呼它们,是因为它们是被定义在一个相关联的类型(即实现里指定的类型)上的。关联程序项是那些可在模块中声明的程序项的子集。具体来说,有[关联函数][associated functions](包括方法)、[关联类型][associated types]和[关联常量][associated constants]。
当关联程序项与被关联程序项在逻辑上相关时,关联程序项就非常有用。例如,Option 上的 is_some 方法内在逻辑定义上就与 Option枚举类型相关,所以它应该和 Option 关联在一起。
每个关联程序项都有两种形式:(包含实际实现的)定义和(为定义声明签名的)声明。1
正是这些声明构成了 trait 的契约(contract)以及其泛型参数中的可用内容。
关联函数和方法
关联函数是与一个类型相关联的函数。
关联函数声明为关联函数定义声明签名。它的书写格式和函数项一样,除了函数体被替换为 ;。
标识符是关联函数的名称。关联函数的泛型参数、参数列表、返回类型 和 where子句必须与它们在关联函数声明中声明的格式一致。
关联函数定义定义与另一个类型相关联的函数。它的编写方式与函数项相同。
常见的关联函数的一个例子是 new函数,它返回此关联函数所关联的类型的值。
struct Struct { field: i32 } impl Struct { fn new() -> Struct { Struct { field: 0i32 } } } fn main () { let _struct = Struct::new(); }
当关联函数在 trait 上声明时,此函数也可以通过一个指向 trait,再后跟函数名的路径来调用。当发生这种情况时,可以用 trait 的实际路径和关联函数的标识符按 <_ as Trait>::function_name 这样的形式来组织实际的调用路径。
#![allow(unused)] fn main() { trait Num { fn from_i32(n: i32) -> Self; } impl Num for f64 { fn from_i32(n: i32) -> f64 { n as f64 } } // 在这个案例中,这4种形式都是等价的。 let _: f64 = Num::from_i32(42); let _: f64 = <_ as Num>::from_i32(42); let _: f64 = <f64 as Num>::from_i32(42); let _: f64 = f64::from_i32(42); }
方法
如果关联函数的参数列表中的第一个参数名为 self2,则此关联函数被称为方法,方法可以使用方法调用操作符(.)来调用,例如 x.foo(),也可以使用常用的函数调用形式进行调用。
如果名为 self 的参数的类型被指定了,它就通过以下文法(其中 'lt 表示生存期参数)来把此指定的参数限制解析成此文法中的一个类型:
P = &'lt S | &'lt mut S | Box<S> | Rc<S> | Arc<S> | Pin<P>
S = Self | P
此文法中的 Self终结符(terminal)表示解析为实现类型(implementing type)的类型。这种解析包括解析上下文中的类型别名 Self、其他类型别名、或使用投射解析把(self 的类型中的)关联类型解析为实现类型。3
译者注:原谅译者对上面这句背后知识的模糊理解,那首先给出原文:
TheSelfterminal in this grammar denotes a type resolving to the implementing type. This can also include the contextual type aliasSelf, other type aliases, or associated type projections resolving to the implementing type.
译者在此先邀请读者中的高手帮忙翻译清楚。感谢感谢。另外译者还是要啰嗦以下译者对这句话背后知识的理解,希望有人能指出其中的错误,以让译者有机会进步:
首先终结符(terminal)就是不能再更细分的词法单元,可以理解它是一个 token,这里它代表 self(即方法接受者)的类型的基础类型。上面句法中的 P 代表一个产生式,它内部定义的规则是并联的,就是自动机在应用这个产生式时碰到任意符合条件的输入就直接进入终态。S 代表有限自动机从 S 这里开始读取 self 的类型。这里 S 是 Self 和 P 的并联,应该表示是:如果 self 的类型直接是 Self,那就直接进入终态,即返回 Self,即方法接收者的直接类型就是结果类型;如果 self 的类型是 P 中的任一种,就返回那一种,比如 self 的类型是一个 box指针,那么就返回Box<S>。
#![allow(unused)] fn main() { use std::rc::Rc; use std::sync::Arc; use std::pin::Pin; // 结构体 `Example` 上的方法示例 struct Example; type Alias = Example; trait Trait { type Output; } impl Trait for Example { type Output = Example; } impl Example { fn by_value(self: Self) {} fn by_ref(self: &Self) {} fn by_ref_mut(self: &mut Self) {} fn by_box(self: Box<Self>) {} fn by_rc(self: Rc<Self>) {} fn by_arc(self: Arc<Self>) {} fn by_pin(self: Pin<&Self>) {} fn explicit_type(self: Arc<Example>) {} fn with_lifetime<'a>(self: &'a Self) {} fn nested<'a>(self: &mut &'a Arc<Rc<Box<Alias>>>) {} fn via_projection(self: <Example as Trait>::Output) {} } }
(方法的首参)可以在不指定类型的情况下使用简写句法,具体对比如下:
| 简写模式 | 等效项 |
|---|---|
self | self: Self |
&'lifetime self | self: &'lifetime Self |
&'lifetime mut self | self: &'lifetime mut Self |
注意: (方法的)生存期也能,其实也经常是使用这种方式来省略。
如果 self 参数以 mut 为前缀,它就变成了一个可变的变量,类似于使用 mut 标识符模式的常规参数。例如:
#![allow(unused)] fn main() { trait Changer: Sized { fn change(mut self) {} fn modify(mut self: Box<Self>) {} } }
以下是一个关于 trait 的方法的例子,现给定如下内容:
#![allow(unused)] fn main() { type Surface = i32; type BoundingBox = i32; trait Shape { fn draw(&self, surface: Surface); fn bounding_box(&self) -> BoundingBox; } }
这里定义了一个带有两个方法的 trait。当此 trait 被引入当前作用域内后,所有此 trait 的实现的值都可以调用此 trait 的 draw 和 bounding_box 方法。
#![allow(unused)] fn main() { type Surface = i32; type BoundingBox = i32; trait Shape { fn draw(&self, surface: Surface); fn bounding_box(&self) -> BoundingBox; } struct Circle { // ... } impl Shape for Circle { // ... fn draw(&self, _: Surface) {} fn bounding_box(&self) -> BoundingBox { 0i32 } } impl Circle { fn new() -> Circle { Circle{} } } let circle_shape = Circle::new(); let bounding_box = circle_shape.bounding_box(); }
版本差异: 在 2015 版中, 使用匿名参数来声明 trait方法是可能的 (例如:
fn foo(u8))。在 2018 版本中,这已被弃用,再用会导致编译错误。新版本种所有的参数都必须有参数名。
方法参数上的属性
方法参数上的属性遵循与常规函数参数上相同的规则和限制。
关联类型
关联类型是与另一个类型关联的类型别名(type aliases) 。关联类型不能在固有实现中定义,也不能在 trait 中给它们一个默认实现。
关联类型声明为关联类型定义声明签名。书写形式为:先是 type,然后是一个标识符,最后是一个可选的 trait约束列表。
这里的标识符是声明的类型的别名名称;可选的 trait约束必须由此类型别名的实现来履行实现。
关联类型定义在另一个类型上定义了一个类型别名。书写形式为:先是 type,然后是一个[标识符],然后再是一个 =,最后是一个类型。
如果类型 Item 上有一个来自 trait Trait的关联类型 Assoc,则表达式 <Item as Trait>::Assoc 也是一个类型,具体就是关联类型定义中指定的类型的一个别名。此外,如果 Item 是类型参数,则 Item::Assoc 也可以在类型参数中使用。
trait AssociatedType { // 关联类型声明 type Assoc; } struct Struct; struct OtherStruct; impl AssociatedType for Struct { // 关联类型定义 type Assoc = OtherStruct; } impl OtherStruct { fn new() -> OtherStruct { OtherStruct } } fn main() { // 使用 <Struct as AssociatedType>::Assoc 来引用关联类型 OtherStruct let _other_struct: OtherStruct = <Struct as AssociatedType>::Assoc::new(); }
示例展示容器内的关联类型
下面给出一个 Container trait 示例。请注意,该类型可用在方法签名内:
#![allow(unused)] fn main() { trait Container { type E; fn empty() -> Self; fn insert(&mut self, elem: Self::E); } }
为了能让实现类型来实现此 trait,实现类型不仅必须为每个方法提供实现,而且必须指定类型 E。下面是一个为标准库类型 Vec 实现了此 Container 的实现:
#![allow(unused)] fn main() { trait Container { type E; fn empty() -> Self; fn insert(&mut self, elem: Self::E); } impl<T> Container for Vec<T> { type E = T; fn empty() -> Vec<T> { Vec::new() } fn insert(&mut self, x: T) { self.push(x); } } }
关联常量
关联常量是与具体类型关联的常量。
关联常量声明为关联常量定义声明签名。书写形式为:先是 const 开头,然后是标识符,然后是 :,然后是一个类型,最后是一个 ;。
这里标识符是(外部引用)路径中使用的常量的名称;类型是(此关联常量的)定义必须实现的类型。
关联常量定义定义了与类型关联的常量。它的书写方式与常量项相同。
示例展示关联常量
基本示例:
trait ConstantId { const ID: i32; } struct Struct; impl ConstantId for Struct { const ID: i32 = 1; } fn main() { assert_eq!(1, Struct::ID); }
使用默认值:
trait ConstantIdDefault { const ID: i32 = 1; } struct Struct; struct OtherStruct; impl ConstantIdDefault for Struct {} impl ConstantIdDefault for OtherStruct { const ID: i32 = 5; } fn main() { assert_eq!(1, Struct::ID); assert_eq!(5, OtherStruct::ID); }
固有实现中声明和定义是在一起的。
把简写形式转换成等价的标准形式。
结合下面的示例理解。