C programmers usually think of scope first as name visibility:
void handle(void)
{
int offset = 0;
{
int len = 16;
offset += len;
}
/* len is not visible, offset is still visible */
}
Rust block scope has the same basic rule:
fn handle() {
let mut offset = 0;
{
let len = 16;
offset += len;
}
// len is not visible, offset is still visible
}
But Rust scope is not only about whether a name can be used. It also affects when resources are released, whether a binding can be shadowed, whether a reference can continue to exist, and which names become part of a module’s public API.
Start with four rules:
{ ... }creates a block scopeletcreates a binding, not just a “variable declaration”- shadowing creates a new binding that hides the old one
- leaving a scope runs resource cleanup
Block scope is close to C
Local variables in C are visible only inside their block:
if (ready) {
int code = read_code();
handle_code(code);
}
/* code is not visible */
Rust is similar:
if ready {
let code = read_code();
handle_code(code);
}
// code is not visible
Function bodies, if, while, loop, and plain { ... } blocks all create scopes.
A manual block is also common:
let header_len = {
let fixed = 4;
let options = 8;
fixed + options
};
The block limits the visibility of fixed and options, and it also produces a value for header_len.
let creates a binding
C declarations create object names:
int len = 10;
Rust usually says “binding”:
let len = 10;
At the first layer, the name len is bound to a value.
Bindings are immutable by default:
let len = 10;
len = 11; // compile error
Use mut for reassignment:
let mut len = 10;
len = 11;
This differs from C’s mutable-by-default local variables. Rust makes it visible at the binding site whether the name may be reassigned later.
Shadowing is a new binding, not reassignment
C does not allow redeclaring the same name in the same block:
int len = 10;
int len = 20; // error
Rust allows shadowing:
let len = "1024";
let len: usize = len.parse().unwrap();
The second len is a new binding that hides the first one. It is not reassignment.
This is useful when the same business concept changes type:
let path = "./config.toml";
let path = std::path::Path::new(path);
It can also turn a mutable calculation into an immutable result:
let mut checksum = 0_u32;
for byte in data {
checksum += *byte as u32;
}
let checksum = checksum;
The last line creates a new immutable binding. Later code cannot change checksum.
Leaving a scope releases resources
In C, ordinary locals go out of scope, but heap memory and file handles usually need manual cleanup:
FILE *fp = fopen(path, "r");
if (!fp) {
return -1;
}
/* ... */
fclose(fp);
In Rust, many resources are managed by values themselves. When the scope ends, Drop runs:
fn read_config(path: &str) -> std::io::Result<String> {
let text = std::fs::read_to_string(path)?;
Ok(text)
}
String, Vec, file objects, lock guards, and similar types release their owned resources when they leave scope.
You can shorten a scope manually:
{
let file = std::fs::File::open(path)?;
// use file
} // file is closed here
// file is no longer held
This is not full ownership theory yet. The first useful intuition is: Rust scope is both a name boundary and a resource boundary.
Inner scopes can borrow outer values, not the other way around
Returning the address of a local variable is a classic C bug:
int *bad(void)
{
int value = 10;
return &value;
}
Rust rejects references that escape the scope of the data they refer to.
This shape works:
let value = 10;
let r = &value;
println!("{r}");
r can be used because value is still alive.
This shape does not work:
let r;
{
let value = 10;
r = &value;
}
println!("{r}"); // compile error
value is gone when the inner scope ends, so the outer scope cannot keep using a reference to it.
Lifetimes deserve their own article. For now, keep this rule: Rust scope participates in checking that a reference does not outlive the data it points to.
Name visibility is not API visibility
C often uses file-scope static to limit a symbol to the current .c file:
static int parse_line(const char *line);
Rust module visibility uses pub. By default, items are private to their module:
fn parse_line(line: &str) -> usize {
line.len()
}
To expose it, write pub:
pub fn parse_line(line: &str) -> usize {
line.len()
}
The first mapping is:
no pub -> internal to the current module
pub -> accessible from outside the module
Full crate, module, pub(crate), pub(super), and use rules are easier to understand as module organization. For now, separate two questions:
- scope: whether a name can be seen inside a code block
- visibility: whether an item can be exposed as module API
static is global storage, not an ordinary local
C file-scope variables:
static int counter;
They have static storage duration and live for the whole program.
Rust has static too:
static APP_NAME: &str = "iot-tool";
static has a fixed storage location. It is suitable for a small number of global constants or objects that genuinely need a global address.
Do not treat static mut as a direct replacement for C globals:
static mut COUNTER: u32 = 0;
Mutable global state touches concurrency and data races, and usually requires unsafe or synchronization types. Ordinary configuration constants should usually start as const; ordinary state should usually live inside a struct or an explicit owner.
Minimal migration table
C block scope -> Rust block scope
C local variable -> Rust let binding
C mutable local -> Rust let mut
C same-scope redeclare -> Rust shadowing creates a new binding
C stack variable out of scope -> Rust binding no longer usable
C manual cleanup -> Rust Drop at scope end
C return local address -> Rust rejected by borrow checking
C file-static function -> Rust private item by default
C exported function -> Rust pub fn
C static storage object -> Rust static
Rust scope is not an isolated syntax point. It affects names, resources, and API boundaries. Many range rules C maintains through comments and conventions become compiler-checkable boundaries in Rust.