Chapter 18: Rust Scope
Rust Scope.
This is a very important topic because scope is the foundation of ownership, borrowing, lifetimes, dropping resources, and almost all memory safety in Rust. Many beginners mix up “scope” with “lifetime” or think it’s just “where the variable is visible” — but in Rust it’s much deeper.
Let me explain it like your patient teacher sitting next to you with code examples: slowly, clearly, with Hyderabad analogies (like traffic signals or biryani portions), lots of copy-paste runnable code, and step-by-step building from simple to advanced.
1. What is “Scope” in Rust? (Simple Definition First)
A scope is a region of code where a name (variable, function, etc.) is visible and usable.
In Rust:
- Scopes are created by curly braces { } (blocks)
- Every block { … } starts a new scope
- Variables declared inside a block are only visible inside that block (and nested inner blocks)
- When the block ends (closing }), variables declared in that block go out of scope → they are dropped (memory freed if needed)
This is called block scope or lexical scope.
Basic example:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
fn main() { let outer = "Hyderabad"; // visible in whole main { // ← new inner scope starts here let inner = "Biryani"; // only visible inside this { } println!("Inner: {}", inner); // OK println!("Outer from inner: {}", outer); // OK — outer still visible } // ← inner goes out of scope & dropped here println!("Outer still alive: {}", outer); // OK // println!("Inner? {}", inner); // ERROR! inner no longer exists } |
- inner lives only inside the { } block
- When } closes → inner is dropped (if it owned heap data like String, memory is freed automatically — RAII style)
- outer lives longer because its declaration is in a wider scope
2. Scope Controls Ownership & Dropping (The Heart of Rust Safety)
Rust’s famous rule:
“When the owner goes out of scope, the value is dropped.”
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fn main() { println!("Before block"); { // new scope let s = String::from("Hello Telangana"); // s owns the String on heap println!("Inside: {}", s); // s is valid here } // ← s goes out of scope → drop() called → heap memory freed! // println!("{}", s); // ERROR! s dropped, invalid println!("After block — memory cleaned automatically"); } |
No manual free(), no garbage collector — scope end = automatic cleanup. This prevents memory leaks & use-after-free bugs at compile time.
3. Scope & Borrowing Rules (Why Scope Matters So Much)
Borrowing rules depend on how long a reference lives compared to the owner.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fn main() { let mut owner = String::from("Namaste"); { // inner scope let borrow = &owner; // immutable borrow — lives only in this block println!("Borrowed: {}", borrow); } // borrow ends here — safe owner.push_str(" Hyderabad!"); // now we can mutate owner again println!("Owner after: {}", owner); } |
But this fails:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fn main() { let r; // declare reference { // inner scope let s = String::from("short"); r = &s; // r borrows s } // s dropped here — but r still wants to use it! // println!("{}", r); // ERROR! dangling reference prevented } |
Compiler error: “borrowed value does not live long enough” → Scope of s ends before r is used → invalid.
4. Shadowing & Scope (Very Common in Rust)
You can re-declare same name in inner scope — it shadows the outer one.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fn main() { let city = "Hyderabad"; { let city = "Secunderabad"; // shadows outer city println!("Inner city: {}", city); } println!("Outer city still: {}", city); // original value unchanged } |
Shadowing ≠ mutation — it’s a new variable hiding the old one.
5. Scope vs Lifetime (Students Always Confuse These)
- Scope = where the name is visible (textual region in code)
- Lifetime = how long the value/reference is valid (time span during execution)
They often align, but not always.
Example where they differ:
|
0 1 2 3 4 5 6 7 8 |
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } |
Here ‘a is a lifetime parameter — it says: “The returned reference lives at least as long as the shorter of x and y”.
Scope is about code blocks, lifetime is about borrow validity over time.
6. Function Scope & Parameters
Each function has its own scope:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fn main() { let msg = String::from("Hello"); print_message(msg); // moved — msg invalid after // println!("{}", msg); // ERROR } fn print_message(text: String) { // text owns it in function scope println!("{}", text); } // text dropped at end of function |
Borrow version:
|
0 1 2 3 4 5 6 7 8 |
fn print_message_borrow(text: &String) { println!("{}", text); } // borrow ends — no drop |
Quick Summary Table (Teacher Style)
| Concept | What it Controls | When it Ends | What Happens at End |
|---|---|---|---|
| Variable Scope | Where name is visible/usable | Closing } of declaring block | Name no longer usable |
| Ownership Drop | When value is cleaned up | Owner goes out of scope | drop() called (memory freed, etc.) |
| Borrow Lifetime | How long reference is safe to use | Reference goes out of scope | Borrow ends — can mutate again if mut |
| Shadowing | Re-using name in inner scope | Inner block ends | Outer name becomes visible again |
Practice Tip
Create cargo new rust_scope and try:
- Make nested blocks with same variable name (shadowing)
- Create String in inner block → try use after block ends
- Borrow in inner block → mutate owner after inner block
- Try to return reference to local variable (see compiler fight you!)
This is where Rust starts feeling “strict but helpful” — the compiler is your strict teacher preventing bugs before they happen.
Ready for next?
- Lifetimes in detail (next logical step after scope)?
- Ownership + borrowing deep dive with scope examples?
- Or structs + impl with scope rules?
Just tell me — your Rust guru from Hyderabad is right here! 🦀🚀
