Scope

From Rust Community Wiki
Jump to navigation Jump to search

A scope (e.g. a module or function) is a block or section of code that can contain items and/or variable bindings. Scopes can be nested; things declared within a scope are only accessible within the same scope, unless they are exported.

Scopes play an important role for borrow checking: Values are dropped when they go out of scope.

The :: sigil used in paths is called the “scope operator”, because it accesses items in a different scope.

Scoping models[edit | edit source]

Different things follow different scoping models. The biggest difference is between items (types, traits, modules, functions, ...) and variables. Furthermore, macro_rules! based macros behave differently than other items.

Scoping of items[edit | edit source]

Items (except macros) use path-based scoping. This means that items must have a name (mostly) unique within the scope, and the order in which items appear is irrelevant. Furthermore, child modules don't automatically inherit items from their parent module. Instead, the items must be imported:

mod inner {
    use super::A;
}
struct A;

Scoping of macros[edit | edit source]

Macros based on macro_rules! default to textual scoping, which relies mostly on the order in which items appear in the source file. Macros can't be invoked in a sibling module of the macro definition, but they can be invoked in a child module. Macros can't be invoked before they are defined:

foo!();

macro_rules! foo {
    () => {};
}

error: cannot find macro `foo` in this scope

If multiple macros with the same name are defined in the same file, they shadow the ones defined before:

macro_rules! foo {
    (1) => {};
}
macro_rules! foo {
    (2) => {};
}

mod inner {
    // no import necessary
    foo!(2);
}

Macros can be used across multiple files, as long as they are declared before the module where they are used:

//// src/lib.rs
macro_rules! foo {
    () => {};
}

mod uses_macro;

//// src/uses_macro.rs
foo!(); // OK: appears after declaration of foo in src/lib.rs

If the macro invocation is a path containing a ::, e.g. super::foo!(), or if it can't be resolved using textual scoping, it is resolved with path-based scoping instead:

mod inner {
    // Make the macro available in the crate root:
    #[macro_export]
    macro_rules! foo {
        () => {};
    }

    foo!(); // resolved with textual scoping
}

foo!(); // Works because we are in the crate root

mod inner2 {
    use crate::foo;

    mod inner3 {
        super::foo!();
    }
}

See also[edit | edit source]

Scoping of variables[edit | edit source]

Scoping of variables (let bindings) shares a lot of similarities with textual scoping. A variable cannot be used before it has been declared and bindings shadow one another. Unlike macros, variables cannot appear directly inside a module, and must be contained in a scope which allows statements and expressions. Static variables and constants are not considered variables by Rust, but rather items.

// global scope; no variables allowed here
mod inner {
    // also a global scope
}

fn main() {
    let a = true;
    let a = 42;
    {
        // a can be used here
        let b = a * 2;
    }
    // b is NOT accessible here
}

Types of scopes[edit | edit source]

Scopes are usually (but not always) surrounded by curly braces ({}). The following items and expressions create scopes that can contain statements and expressions:

  • Functions
  • Regular blocks ({})
  • for, while, while let, loop, unsafe and async blocks
  • if, else if, else, if let branches
  • each match arm in a match block
  • try blocks (these are still experimental[1])

Items can be declared anywhere where statements are allowed:

fn main() {
    struct S;
    {
        struct S;
    }
}

Moreover, they can also be declared in

Variables declared in a let bindings have an implicit scope starting at the let binding and ending at the end of the enclosing scope. So this:

let a = true;
let a = 42;

is semantically equivalent to

let a = true;
{
    let a = 42;
}

Namespaces[edit | edit source]

Every scope introduces not one, but three namespaces[2][3] (also called universes):

  • The type namespace, which contains modules, types, type aliases, traits, trait aliases and enum variants
  • The value namespace, which contains functions, constants, static items and certain constructors (see below)
  • The macro namespace

Items that live in different namespaces can coexist in the same scope. This is allowed, because Rusts syntax is designed to be able to distinguish types, values and macros. For example, a macro, a function and a module in the same scope can have the same name:

mod func {}

fn func() {}

macro_rules! func {
    () => {};
}

Constructors[edit | edit source]

A constructor is what creates a new instance of a type. For example, structs can be constructed with SomeStruct { field1: value1, field2: value2, ... }. Constructors are part of the value namespace for the following types:

  • Unit structs and unit-like enum variants — their constructor is a constant.
  • Tuple structs and tuple-like enum variants — their constructor is a function.

Because of this, unit/tuple structs and unit-like/tuple-like enum variants are in both the type namespace and the value namespace, so the following doesn't work:

struct A();
fn A() {}

error[E0428]: the name `A` is defined multiple times

Fields and associated items[edit | edit source]

Some identifiers, e.g. fields, are not in any namespace, so a type can have a method with the same name as a field:

struct MyStruct {
    field: bool,
}

impl MyStruct {
    fn field(&self) -> bool {
        self.field
    }
}

Since associated types are in a different namespace than associated functions and constants, a trait can contain associated items with the same name:

trait Trait1 {
    type A;   // type namespace
    fn A();   // value namespace
}
impl Trait2 {
    type B;            // type namespace
    const B: i32 = 42; // value namespace
}

The namespaces of inherent impls of the same type are combined:

struct S;
impl S {
    fn foo() {}
}
impl S {
    fn foo() {}
}

error[E0592]: duplicate definitions with name `foo`

Desugaring of structs and enums[edit | edit source]

I wrote above that constructors are part of the value namespace for the following types:

  • Unit structs and unit-like enum variants — their constructor is a constant.
  • Tuple structs and tuple-like enum variants — their constructor is a function.

Let's see what this means with a concrete example:

struct A;       // unit struct

enum BEnum {
    B,          // unit-like enum variant
}

struct C(i32);  // tuple struct

enum DEnum {
    D(i32),     // tuple-like enum variant
}

This desugars roughly to the following code:

struct A {}
const A: A = A {};

enum BEnum {
    _B {},
}
impl BEnum {
    const B: BEnum = BEnum::_B {};
}

struct C { _0: i32 }
fn C(_0: i32) -> C {
    C { _0 }
}

enum DEnum {
    D { _0: i32 }
}
impl DEnum {
    fn D(_0: i32) -> DEnum {
        DEnum::D { _0 }
    }
}

We had to add a few underscores to satisfy the compiler. In reality, there are no underscores, of course. You can see for yourself that the following works:

struct A;       // unit struct

enum BEnum {
    B,          // unit-like enum variant
}

struct C(i32);  // tuple struct

enum DEnum {
    D(i32),     // tuple-like enum variant
}

let _ = A {};
let _ = BEnum::B {};
let _ = C { 0: 42 };
let _ = DEnum::D { 0: 42 };

Name resolution[edit | edit source]

When there are multiple items/variables with the same name in scope (e.g. a struct A {}, a fn A(), a macro A and a local variable A), Rust resolves the name by looking at how it is used, for example:

  • If it is followed by an exclamation mark (A!), it must be a macro
  • If it is followed by an opening parenthesis (A() or a dot (A.), it must be in the value namespace
  • If it is followed by ::, it must be in the type namespace

When resolving a value and both an item and a local variable is in scope, the local variable is preferred:

fn f() {
    println!("function");
}

f(); // calls the function
let f = || println!("closure");
f(); // calls the closure

When calling something that could be a field or a method, Rust always assumes that it is a method:

struct Foo {
    field: Box<dyn Fn()>,
}
fn call_field(f: Foo) {
    f.field()
}

error[E0599]: no method named `field` found for struct `Foo` in the current scope

This can be fixed by wrapping f.field in parentheses:

fn call_field(f: Foo) {
    (f.field)()
}

When importing items, different namespaces aren't differentiated; instead, items from all three namespaces are imported:

fn A() {}

struct A {}

#[macro_export]
macro_rules! A {
    () => {};
}

mod inner {
    use crate::A; // imports all three items
}

References[edit | edit source]