Code Story

Chapter #2 – Variables & Binding

declaration, mutability, scope, shadowing, constants

2.0  Prologue

A variable is a named binding from an identifier to a storage location. How languages handle declaration syntax, mutability defaults, scope boundaries, and compile-time constants shapes both program correctness and readability.

2.1  Variable Declaration

Most modern languages support type inference: the compiler deduces the type from the initializer, removing redundant annotations while retaining full static typing. // Rust (let introduces a binding; type inferred) let x = 42; // i32 inferred let y: f64 = 3.14; // explicit annotation // C++ (auto deduces type from initializer) auto x = 42; // int auto y = 3.14; // double // C# (var deduces type; requires an initializer) var x = 42; // int var y = 3.14; // double // Python (no declaration keyword; assignment creates the binding) x = 42 y = 3.14 Python bindings are not typed - the name can be rebound to a value of any type. Rust, C++, and C# bindings are statically typed; the type is fixed at declaration.

2.2  Mutability

Languages differ in their default mutability. Rust bindings are immutable by default; C++, C#, and Python bindings are mutable by default. // Rust (immutable by default; opt-in mutation) let x = 5; // x = 6; // compile error let mut y = 5; y = 6; // ok // C++ (mutable by default; const opts out) int y = 5; y = 6; // ok const int x = 5; // x = 6; // compile error // C# int y = 5; y = 6; // ok const int x = 5; // compile-time constant, not a variable // Python (no enforced immutability for scalars) x = 5 x = 6 # rebinds the name; previous int object is unchanged Immutability by default pushes mutation to be explicit, making data flow easier to reason about. Rust’s borrow checker enforces these rules at compile time.

2.3  Scope and Lifetime

Scope is the region of source text where a name is visible. Lifetime is the duration for which the binding’s storage is valid. In languages with automatic storage, scope and lifetime coincide: the binding lives as long as its enclosing block. // Rust (block scope; value dropped at closing brace) { let s = String::from("hello"); // s is valid here } // s is dropped here; memory freed // C++ (same block-scope rule; destructor called at closing brace) { std::string s = "hello"; } // destructor runs here // C# / Python: garbage-collected; lifetimes are not block-scoped Rust goes further: it tracks lifetimes through references and rejects code that would allow a reference to outlive the value it points to, eliminating use-after-free at compile time.

2.4  Shadowing

Shadowing occurs when a new binding with the same name is introduced in the same or an inner scope, hiding the previous binding. Unlike mutation, shadowing can change the type of the name. // Rust (shadowing is explicit and intentional) let x = 5; let x = x + 1; // shadows previous x; new binding of type i32 let x = x.to_string(); // shadows again; now x is String // C++ (inner scope shadows outer) int x = 5; { double x = 2.5; // hides outer x within this block } // outer int x visible again // C# and Python behave similarly within nested scopes Rust’s same-scope shadowing is used to transform a value without a separate name, keeping code compact after parsing or converting input.

2.5  Constants and Statics

Constants are evaluated at compile time and inlined wherever they appear. Statics have a single address that persists for the duration of the program (static storage duration). // Rust const MAX_SIZE: usize = 1024; // compile-time constant static GREETING: &str = "hello"; // static storage, 'static lifetime static mut COUNTER: u32 = 0; // mutable static (unsafe to access) // C++ constexpr int MAX_SIZE = 1024; // compile-time constant static const char* GREETING = "hello"; // static storage // C# const int MaxSize = 1024; // compile-time constant static readonly string Greeting = "hello"; // runtime-initialized static // Python (convention only; no enforced constants) MAX_SIZE = 1024 # uppercase names signal "don't mutate this" Mutable statics in Rust require unsafe because concurrent access without synchronization is undefined behavior. Prefer atomic types or mutexes for shared mutable state.

2.6  Epilogue

Binding rules determine which operations are safe before execution begins. Immutability by default and strong scoping eliminate a large class of bugs at zero runtime cost. The next chapter examines the operators that act on bound values.

2.7  References

Rust Variables and Mutability - The Book
C++ Storage Duration - cppreference
C# const - Microsoft
Python Execution Model - Binding of Names