高级 The Rust Project Developers 著,rustwiki.org 中译 2026-09-13 14:59:08 · 0 阅读

第13章 名称

*实体(entity)*是一种语言结构,在源程序中可以以某种方式被引用,通常是通过[路径(path)][paths]。实体包括类型程序项泛型参数变量绑定循环标签生存期字段属性和各种lints

声明(declaration)是一种句法结构,它可以引入名称来引用实体。实体的名称在相关作用域(scope)内有效。作用域是指可以引用该名称的源码区域。

有些实体是在源码中显式声明的,有些则隐式声明为语言或编译器扩展的一部分。

路径用于引用实体,该引用的实体可以在其他的作用域内。生存期和循环标签使用一个带有前导单引号的专用语法来表达

名称被分隔成不同的命名空间,这样允许不同名称空间中的实体拥有相同的名称,且不会发生冲突。

名称解析是将路径、标识符和标签绑定到实体声明的编译时过程。

对某些名称的访问可能会受到此名称的可见性的限制。

显式声明的实体

在源码中显式引入名称的实体有:

此外,宏调用属性可以通过扩展源代码到上述程序项之一来引入名称。

隐式声明的实体

以下实体由语言隐式定义,或由编译器选项和编译器扩展引入:

此外,crate 的根模块没有名称,但可以使用某些路径限定符或别名来引用。

命名空间

命名空间是已声明的名称的逻辑分组。根据名称所指的实体类型,名称被分隔到不同的命名空间中。 名称空间允许一个名称空间中出现的名称与另一个名称空间中的相同,且不会导致冲突。

在命名空间中,名称被组织在不同的层次结构中,层次结构的每一层都有自己的命名实体集合。

程序有几个不同的命名空间,每个名称空间包含不同种类的实体。使用名称时将根据上下文来在不同的命名空间中去查找该名称的声明,名称解析一章有讲到这些。

下面是一系列命名空间及其对应实体的列表:

如何清晰地使用不同命名空间中的同名名称的示例:


#![allow(unused)]
fn main() {
// Foo 在类型命名空间中引入了一个类型,在值命名空间中引入了一个构造函数
struct Foo(u32);

// 宏`Foo`在宏命名空间中声明
macro_rules! Foo {
    () => {};
}

// 参数`f` 的类型中的 `Foo` 指向类型命名空间中的 `Foo`
// `'Foo` 引入一个生存期命名空间里的新的生存期
fn example<'Foo>(f: Foo) {
    // `Foo` 引用值命名空间里的 `Foo`构造器。
    let ctor = Foo;
    // `Foo` 引用宏命名空间里的 `Foo`宏。
    Foo!{}
    // `'Foo` 引入一个标签命名空间里的标签。
    'Foo: loop {
        // `'Foo` 引用 `'Foo`生存期参数, `Foo` 引用类型命名空间中的类型。
        let x: &'Foo Foo;
        // `'Foo` 引用了 `'Foo`标签.
        break 'Foo;
    }
}
}

无命名空间的命名实体

下面的实体有显式的名称,但是这些名称不属于任何特定的命名空间。

字段

即使结构体、枚举和联合体的字段被命名,但这些命名字段并不存在于任何显式的命名空间中。它们只能通过字段表达式访问,该表达式只检测被访问的特定类型的字段名。

use声明

use声明命名了导入到当前作用域中的实体,但 use项本身不属于任何特定的命名空间。相反,它可以在多个名称空间中引入别名,这取决于所导入的程序项类型。

1

目前,在同一个作用域中,标签和生命周期使用相同名称时,rustc 会警告出现命名重复,但编译器内部仍然会独立地区别对待它们。这是一个关于该语言可能扩展的未来兼容性警告。具体见PR #24162

作用域

注意:这是未来本章内容扩展的占位符。

预导入包

预导入包是一组名称的集合,它会自动把这些名称导入到 crate 中的每个模块的作用域中。

预导入包中的那些名称不是当前模块本身的一部分,它们在名称解析期间被隐式导入。例如,即使像 Box 这样在每个模块的作用域中到处使用的名称,你也不能通过 self::Box 来引用它,因为它不是当前模块的成员。

有几个不同的预导入包:

标准库预导入包

标准库预导入包包含了 std::prelude::v1模块中的名称。如果使用no_std属性,那么它将使用core::prelude::v1模块中的名称。

外部预导入包

在根模块中使用 extern crate 导入的外部crate 或直接给编译器提供的的外部crate(也就是在 rustc命令下使用 --extern命令行参数选项)会被添加到外部预导入包中。如果使用 extern crate orig_name as new_name 这类别名导入,则符号 new_name 将被添加到此预导入包。

core crate 总是会被添加到外部预导入包中。只要 no_std属性没有在 crate根模块中指定,那么std crate 就会被添加进来

版本差异:在 2015 版中,在外部预导入包中的 crate 不能通过 use声明来直接引用,因此通常标准做法是用 extern crate 将那它们纳入到当前作用域。

从 2018 版开始, use声明可以直接引用外部预导入包里的 crate,所以再在代码里使用 extern crate 就会被认为是不规范的。

注意: 随 rustc 一起引入的 crate,如 alloctest,在使用 Cargo 时不会自动被包含在 --extern 命令行参数选项中。即使在 2018 版中,也必须通过外部crate(extern crate)声明来把它们引入到当前作用域内。


#![allow(unused)]
fn main() {
extern crate alloc;
use alloc::rc::Rc;
}

Cargo却会将 proc_macro 带入到编译类型为 proc-macro 的 crate 的外部预导入包中

no_std属性

默认情况下,标准库自动包含在 crate根模块中。在 std crate 被添加到根模块中的同时,还会隐式生效一个 macro_use属性,它将所有从 std 中导出的宏放入到macro_use预导入包中。默认情况下,corestd 都被添加到外部预导入包中。标准库预导入包包含了 std::prelude::v1模块中的所有内容。

*no_std属性*可以应用在 crate 级别上,用来防止 std crate 被自动添加到相关作用域内。此属性作了如下三件事:

注意:当 crate 的目标平台不支持标准库或者故意不使用标准库的功能时,使用核心预导入包而不是标准预导入包是很有用的。此时没有导入的标准库的那些功能主要是动态内存分配(例如:Box和' Vec)和文件,以及网络功能(例如:std::fsstd::io)。

警告:使用 no_std 并不会阻止标准库被链接进来。使用 extern crate std;std crate 导入仍然有效,相关的依赖项也可以被正常链接进来。

语言预导入包

语言预导入包包括语言内置的类型名称和属性名称。语言预导入包总是在当前作用域内有效的。它包括以下内容:

macro_use预导入包

macro_use预导入包包含了外部crate 中的宏,这些宏是通过在当前文档源码内部的 extern crate 声明语句上应用 macro_use属性来导入此声明中的 crate 内部的宏。

工具类预导入包

工具类预导入包包含了在类型命名空间中声明的外部工具的工具名称。请参阅工具类属性一节,以了解更多细节。

no_implicit_prelude属性

*no_implicit_prelude属性*可以应用在 crate级别或模块上,用以指示它不应该自动将标准库预导入包外部预导入包工具类预导入包引入到当前模块或其任何子模块的作用域中。

此属性不影响语言预导入包

版本差异: 在 2015版中,no_implicit_prelude属性不会影响macro_use预导入包,从标准库导出的所有宏仍然包含在 macro_use预导入包中。从 2018版开始,它也会禁止 macro_use预导入包生效。

路径

路径是一个或多个由命名空间限定符(::)逻辑分隔的路径段(path segments)组成的序列(译者注:如果只有一个段的话,:: 不是必须的)。如果路径仅由一个路径段组成,则它引用局部控制域(control scope)内的程序项变量。如果路径包含多个路径段,则总是引用程序项。

仅由标识符段组成的简单路径(simple paths)的两个示例:

x;
x::y::z;net

路径分类

Simple Paths

简单路径

句法
SimplePath :
   ::? SimplePathSegment (:: SimplePathSegment)*

SimplePathSegment :
   IDENTIFIER | super | self | crate | $crate

简单路径可用于可见性标记、属性use程序项中。示例:


#![allow(unused)]
fn main() {
use std::io::{self, Write};
mod m {
    #[clippy::cyclomatic_complexity = "0"]
    pub (in super) fn f1() {}
}
}

表达式中的路径

句法
PathInExpression :\net    ::? PathExprSegment (:: PathExprSegment)*

PathExprSegment :
   PathIdentSegment (:: GenericArgs)?

PathIdentSegment :
   IDENTIFIER | super | self | Self | crate | $crate

GenericArgs :
      < >
   | < ( GenericArg , )* GenericArg ,? >

GenericArg :\net    Lifetime | Type | GenericArgsConst | GenericArgsBinding

GenericArgsConst :
      BlockExpression
   | LiteralExpression
   | - LiteralExpression
   | SimplePathSegment

GenericArgsBinding :
   IDENTIFIER = Type

表达式中的路径允许指定带有泛型参数的路径。它们在各种表达式模式中都有使用。

token :: 必须在泛型参数的左尖括号(<)的前面,以避免和小于号操作符产生混淆。这就是俗称的“涡轮鱼(turbofish)”句法。


#![allow(unused)]
fn main() {
(0..10).collect::<Vec<_>>();
Vec::<u8>::with_capacity(1024);
}

泛型参数的顺序被限制为生存期参数,然后是类型参数,然后是常量参数,再后是相应的约束。

常量实参必须用花括号括起来,除非它们是字面量或单段路径。

限定性路径

句法
QualifiedPathInExpression :
   QualifiedPathType (:: PathExprSegment)+

QualifiedPathType :
   < Type (as TypePath)? >

QualifiedPathInType :
   QualifiedPathType (:: TypePathSegment)+

完全限定性路径可以用来为 trait实现(trait implementations)消除路径歧义,也可以用来指定规范路径。用在指定具体类型时,它支持使用如下所示的类型句法:


#![allow(unused)]
fn main() {
struct S;
impl S {
    fn f() { println!("S"); }
}
trait T1 {
    fn f() { println!("T1 f"); }
}
impl T1 for S {}
trait T2 {
    fn f() { println!("T2 f"); }
}
impl T2 for S {}
S::f();  // Calls the inherent impl.
<S as T1>::f();  // Calls the T1 trait function.
<S as T2>::f();  // Calls the T2 trait function.
}

类型中的路径

Syntax
TypePath :
   ::? TypePathSegment (:: TypePathSegment)*

TypePathSegment :
   PathIdentSegment ::? (GenericArgs | TypePathFn)?

TypePathFn :
( TypePathFnInputs? ) (-> Type)?

TypePathFnInputs :
Type (, Type)* ,?

类型路径用于类型定义、trait约束(trait bound)、类型参数约束,以及限定性路径。

尽管在泛型参数之前允许使用 token ::,但它不是必需的,因为在类型路径中不存在像 PathInExpression 句法中那样的歧义。


#![allow(unused)]
fn main() {
mod ops {
    pub struct Range<T> {f1: T}
    pub trait Index<T> {}
    pub struct Example<'a> {f1: &'a i32}
}
struct S;
impl ops::Index<ops::Range<usize>> for S { /*...*/ }
fn i<'a>() -> impl Iterator<Item = ops::Example<'a>> {
    // ...net
   const EXAMPLE: Vec<ops::Example<'static>> = Vec::new();
   EXAMPLE.into_iter()
}
type G = std::boxed::Box<dyn std::ops::FnOnce(isize) -> isize>;
}

路径限定符

路径可以被各种能改变其解析方式的前导限定符限定。

::

:: 开头的路径被认为是全局路径,其中的路径首段在不同的版本中的解析方式有所不同。但路径中的每个标识符都必须解析为一个程序项。

版本差异: 在2015版中,标识符解析从“create 根模块(crate root)”(2018版中表示为 crate::)开始,“create 根模块(crate root)”中包含了一系列不同的程序项,包括外部crate、默认create(如 stdcore),以及 crate下的各种顶层程序项(包括 use导入)。

从 2018 版开始,以 :: 开头的路径被解析为外部预导入包中的一个 crate。也就是说,其后必须跟一个 crate的名称。

mod a {
    pub fn foo() {}
}
mod b {
    pub fn foo() {
        ::a::foo(); // 调用 `a`'的 foo 函数
        // 在 Rust 2018 中, `::a` 会被解析为 crate `a`.
    }
}
fn main() {}

self

self 表示路径是相对于当前模块的路径。self 仅可以用作路径的首段,不能有前置 ::

fn foo() {}
fn bar() {
    self::foo();
}
fn main() {}

Self

Self(首字母大写)用于指代 trait实现中的类型。

Self 仅可以用作路径的首段,不能有前置 ::


#![allow(unused)]
fn main() {
trait T {
    type Item;
    const C: i32;
    // `Self` 将是实现 `T` 的任何类型。
    fn new() -> Self;
    // `Self::Item` 将是实现中指定的类型的别名。
    fn f(&self) -> Self::Item;
}
struct S;
impl T for S {
    type Item = i32;
    const C: i32 = 9;
    fn new() -> Self {           // `Self` 是类型 `S`.
        S
    }
    fn f(&self) -> Self::Item {  // `Self::Item` 是类型 `i32`.
        Self::C                  // `Self::C` 是常量值 `9`.
    }
}
}

super

super 在路径中被解析为父模块。它只能用于路径的前导段,可以置于 self 路径段之后。

mod a {
    pub fn foo() {}
}
mod b {
    pub fn foo() {
        super::a::foo(); // 调用 a'的 foo 函数
    }
}
fn main() {}

super 可以在第一个 superself 之后重复多次,以引用祖先模块。

mod a {
    fn foo() {}

    mod b {
        mod c {
            fn foo() {
                super::super::foo(); // 调用 a'的 foo 函数
                self::super::super::foo(); // 调用 a'的 foo 函数
            }
        }
    }
}
fn main() {}

crate

crate 解析相对于当前 crate 的路径。crate 仅能用作路径的首段,不能有前置 ::

fn foo() {}
mod a {
    fn bar() {
        crate::foo();
    }
}
fn main() {}

$crate

$crate 仅用在宏转码器(macro transcriber)中,且仅能用作路径首段,不能有前置 ::$crate 将被扩展为从定义宏的 crate 的顶层访问该 crate 中的各程序项的路径,而不用去考虑宏调用发生时所在的现场 crate。

pub fn increment(x: u32) -> u32 {
    x + 1
}

#[macro_export]
macro_rules! inc {
    ($x:expr) => ( $crate::increment($x) )
}
fn main() { }

规范路径

定义在模块或者实现中的程序项都有一个规范路径,该路径对应于其在其 crate 中定义的位置。在该路径之外,所有指向这些程序项的路径都是别名(路径)。规范路径被定义为一个路径前缀后跟一个代表程序项本身的那段路径段。

尽管实现所定义/实现的程序项有规范路径,但实现作为一个整体,自身没有规范路径。use声明也没有规范路径。块表达式中定义的程序项也没有规范路径。在没有规范路径的模块中定义的程序项也没有规范路径。在实现中定义的关联项(associated items)如果它指向没有规范路径的程序项——例如,实现类型(implementing type)、被实现的 trait、类型参数或类型参数上的约束——那它也没有规范路径。

模块的路径前缀就是该模块的规范路径。对于裸实现(bare implementations)来说,它里面的程序项的路径前缀是:它当前实现的程序项的规范路径,再用尖括号(<>括把此路径括起来。对于 trait实现来说,它内部的程序项的路径前缀是:当前实现的程序项的规范路径后跟一个 as,再后跟 trait 本身的规范路径,然后整个使用尖括号(<>括起来。

规范路径只在给定的 crate 中有意义。在 crate 之间没有全局命名空间,所以程序项的规范路径只在其 crate 中可标识。

// 注释解释了程序项的规范路径

mod a { // ::a
    pub struct Struct; // ::a::Struct

    pub trait Trait { // ::a::Trait
        fn f(&self); // ::a::Trait::f
    }

    impl Trait for Struct {
        fn f(&self) {} // <::a::Struct as ::a::Trait>::f
    }

    impl Struct {  // 译者注:这是一个裸实现
        fn g(&self) {} // <::a::Struct>::g
    }
}

mod without { // ::without
    fn canonicals() { // ::without::canonicals
        struct OtherStruct; // None

        trait OtherTrait { // None
            fn g(&self); // None
        }

        impl OtherTrait for OtherStruct {
            fn g(&self) {} // None
        }

        impl OtherTrait for ::a::Struct {
            fn g(&self) {} // None
        }

        impl ::a::Trait for OtherStruct {
            fn f(&self) {} // None
        }
    }
}

fn main() {}

名称解析

注意:这是未来本章内容扩展的占位符。

可见性与隐私权

句法
Visibility :
      pub
   | pub ( crate )
   | pub ( self )
   | pub ( super )
   | pub ( in SimplePath )

这两个术语经常互换使用,它们试图表达的是对“这个地方能使用这个程序项吗?”这个问题的回答。

Rust 的名称解析是在命名空间的层级结构的全局层(即最顶层)上运行的。此层级结构中的每个级别都可以看作是某个程序项。这些程序项就是我们前面提到的那些程序项之一(注意这也包括外部crate)。声明或定义一个新模块可以被认为是在定义的位置向此层次结构中插入一个新的(层级)树。

为了控制接口是否可以被跨模块使用,Rust 会检查每个程序项的使用,来看看它是否被允许使用。此处就是生成隐私告警的地方,或者说是提示:“you used a private item of another module and weren't allowed to.”的地方。(译者注:这句提示可以翻译为:您使用了另一个模块的私有程序项,但不允许这样做。)

默认情况下,Rust 中的所有内容都是私有的,但有两个例外:pub trait 中的关联程序项默认为公有的;pub 枚举中的枚举变体也默认为公有的。当一个程序项被声明为 pub 时,它可以被认为是外部世界能以访问的。例如:

fn main() {}
// 声明一个私有结构体
struct Foo;

// 声明一个带有私有字段的公有结构体
pub struct Bar {
    field: i32,
}

// 声明一个带有两个公有变体的公有枚举
pub enum State {
    PubliclyAccessibleState,
    PubliclyAccessibleState2,
}

依据程序项是公有的还是私有的,Rust 分两种情况来访问数据:

  1. 如果某个程序项是公有的,那么如果可以从外部的某一模块 m 访问到该程序项的所有祖先模块,则一定可以从这个模块 m 中访问到该程序项。甚至还可以通过重导出来命名该程序项。具体见后文。

  2. 如果某个程序项是私有的,则当前模块及当前模块的后代模块都可以访问它。

这两种情况在创建能对外暴露公共API 同时又隐藏内部实现细节的模块层次结构时非常好用。为了帮助理解,以下是一些常见案例和它们需要做的事情:

  • 库开发人员需要将一些功能暴露给链接了其库的 crate。作为第一种情况的结果,这意味着任何那些在外部可用的程序项自身以及其路径层级结构中的每一层都必须是公有的(pub)的。并且此层级链中的任何私有程序项都不允许被外部访问。

  • crate 需要一个全局可用的“辅助模块(helper module)”,但又不想将辅助模块公开为公共API。为了实现这一点,可以在整个 crate 的根模块(路径层级结构中的最顶层)下建一个私有模块,该模块在内部是“公共API”。因为整个 crate 都是根模块的后代,所以整个本地 crate 里都可以通过第二种情况访问这个私有模块。

  • 在为模块编写单元测试时,通常的习惯做法是给要测试的模块加一个命名为 mod test 的直接子模块。这个模块可以通过第二种情况访问父模块的任何程序项,这意味着内部实现细节也可以从这个子模块里进行无缝地测试。

在第二种情况下,我们提到了当前模块及其后代“可以访问”私有项,但是访问一个程序项的确切含义取决于该项是什么。例如,访问一个模块意味着要查看它的内部(以导入更多的程序项);访问一个函数意味着它被调用了。此外,路径表达式和导入语句也被视为访问一个程序项,但只有当访问目标位于当前可见的作用域内时,它们才算是有效的数据访问。

下面是一段示例程序,它例证了上述三种情况:

// 这个模块是私有的,这意味着没有外部crate 可以访问这个模块。
// 但是,由于它在当前 crate 的根模块下,
// 因此当前 crate 中的任何模块都可以访问该模块中任何公有可见性程序项。
mod crate_helper_module {

    // 这个函数可以被当前 crate 中的任何东西使用
    pub fn crate_helper() {}

    // 此函数*不能*被用于 crate 中的任何其他模块中。它在 `crate_helper_module` 之外不可见,
    // 因此只有当前模块及其后代可以访问它。
    fn implementation_detail() {}
}

// 此函数“对根模块是公有”的,这意味着它可被链接了此crate 的其他crate 使用。
pub fn public_api() {}

// 与 'public_api' 类似,此模块是公有的,因此其他的crate 是能够看到此模块内部的。
pub mod submodule {
    use crate_helper_module;

    pub fn my_method() {
        // 本地crate 中的任何程序项都可以通过上述两个规则的组合来调用辅助模块里的公共接口。
        crate_helper_module::crate_helper();
    }

    // 此函数对任何不是 `submodule` 的后代的模块都是隐藏的
    fn my_implementation() {}

    #[cfg(test)]
    mod test {

        #[test]
        fn test_my_implementation() {
            // 因为此模块是 `submodule` 的后代,因此允许它访问 `submodule` 内部的私有项,而不会侵犯隐私权。
            super::my_implementation();
        }
    }
}

fn main() {}

对于一个 Rust 程序要通过隐私检查,所有的路径都必须满足上述两个访问规则。这里说的路径包括所有的 use语句、表达式、类型等。

pub(in path), pub(crate), pub(super), and pub(self)

除了公有和私有之外,Rust 还允许用户(用关键字 pub )声明仅在给定作用域内可见的程序项。声明形式的限制规则如下:

  • pub(in path) 使一个程序项在提供的 path 中可见。path 必须是声明其可见性的程序项的祖先模块。
  • pub(crate) 使一个程序项在当前 crate 中可见。
  • pub(super) 使一个程序项对父模块可见。这相当于 pub(in super)
  • pub(self) 使一个程序项对当前模块可见。这相当于 pub(in self) 或者根本不使用 pub

版本差异: 从 2018版开始,pub(in path) 的路径必须以 crateselfsuper开头。2015版还可以使用以 :: 开头的路径,或以根模块下的模块名的开头的路径。

这里是一些示例:

pub mod outer_mod {
    pub mod inner_mod {
        // 此函数在 `outer_mod` 内部可见
        pub(in crate::outer_mod) fn outer_mod_visible_fn() {}
        // 同上,但这只能在2015版中有效
        pub(in outer_mod) fn outer_mod_visible_fn_2015() {}

        // 此函数对整个 crate 都可见
        pub(crate) fn crate_visible_fn() {}

        // 此函数在 `outer_mod` 下可见
        pub(super) fn super_mod_visible_fn() {
            // 此函数之所以可用,是因为我们在同一个模块下
            inner_mod_visible_fn();
        }

        // 这个函数只在 `inner_mod` 中可见,这与它保持私有的效果是一样的。
        pub(self) fn inner_mod_visible_fn() {}
    }
    pub fn foo() {
        inner_mod::outer_mod_visible_fn();
        inner_mod::crate_visible_fn();
        inner_mod::super_mod_visible_fn();

        // 此函数不再可见,因为我们在  `inner_mod` 之外
        // 错误! `inner_mod_visible_fn` 是私有的
        //inner_mod::inner_mod_visible_fn();
    }
}

fn bar() {
    // 此函数仍可见,因为我们在同一个 crate 里
    outer_mod::inner_mod::crate_visible_fn();

    // 此函数不再可见,因为我们在`outer_mod`之外
    // 错误! `super_mod_visible_fn` 是私有的
    //outer_mod::inner_mod::super_mod_visible_fn();

    // 此函数不再可见,因为我们在`outer_mod`之外
    // 错误! `outer_mod_visible_fn` 是私有的
    //outer_mod::inner_mod::outer_mod_visible_fn();

    outer_mod::foo();
}

fn main() { bar() }

注意: 此句法仅对程序项的可见性添加了另一个限制。它不能保证该程序项在指定作用域的所有部分都可见。要访问一个程序项,当前作用域内它的所有父项还是必须仍然可见。

重导出和可见性

Rust 允许使用指令 pub use 公开重导出程序项。因为这是一个公有指令,所以允许通过上面的规则验证后在当前模块中使用该程序项。重导出本质上允许使用公有方式访问重导出的程序项的内部。例如,下面程序是有效的:

pub use self::implementation::api;

mod implementation {
    pub mod api {
        pub fn f() {}
    }
}

fn main() {}

这意味着任何外部 crate,只要引用 implementation::api::f 都将收到违反隐私的错误报告,而使用路径 api::f 则被允许。

当重导出私有程序项时,可以认为它允许通过重导出短路了“隐私链(privacy chain)”,而不是像通常那样通过命名空间层次结构来传递“隐私链”。

评论 (0)