In PHP
A variable is mutable, and its name lives for the whole function. An if block does not hide it. After the brace, $points is still 2.
strlen("five") is 4.
const is a compile-time name. On PHP 8.3, assigning to it is a parse error: syntax error, unexpected token "=". A readonly property is different: the constructor sets it, and a later assignment throws Cannot modify readonly property.
<?php
function score(): void
{
$points = 1;
if (true) {
$points = 2;
}
echo $points, "\n";
$count = "five";
$count = strlen($count);
echo $count, "\n";
}
score();
const MAX_COUNT = 100;
echo MAX_COUNT, "\n";
That prints 2, then 4, then 100.
<?php
const MAX_COUNT = 100;
MAX_COUNT = 200;
PHP 8.3 rejects that file before it runs: Parse error: syntax error, unexpected token "=".
<?php
readonly class Config
{
public function __construct(public int $maxCount)
{
}
}
$config = new Config(100);
echo $config->maxCount, "\n";
$config->maxCount = 200 throws Error: Cannot modify readonly property Config::$maxCount.
In Rust
let binds a name. Without mut, a second assignment is a compile error. let again, in the same scope, creates a new binding. That is how a string becomes a number without a mutable variable: parse is the usual reason to shadow. The old binding is hidden for the rest of that scope.
A shadow inside a block ends at that block's }. The outer name is still there.
const is an item, not a let binding. It has a type, the name is SCREAMING_CASE, the value is computed at compile time, and it is never mut.
const MAX_POINTS: u32 = 100_000;
fn main() {
let x = 5;
println!("{x}");
let mut counter = 0;
counter += 1;
println!("{counter}");
let guess = "42";
let guess: u32 = guess.parse().expect("guess should be a number");
println!("{guess}");
let count = "five";
let count = count.len();
println!("{count}");
let name = 5;
{
let name = name + 1;
println!("inner {name}");
}
println!("outer {name}");
println!("{MAX_POINTS}");
}
count prints 4. inner prints 6. outer prints 5.
Watch for: A shadow inside a block ends at that block's
}. The outer name comes back. In PHP the assignment would still be visible, because the name belongs to the function.
Idiomatic Rust: Shadow with
parsewhen a name should change type and stay immutable. Useconstfor a typed value the compiler can compute up front.
Side by side
| PHP | Rust | |
|---|---|---|
| Second assignment | Updates the same variable | Needs let mut, or a new let |
"five" length |
strlen is 4 |
.len() is 4 |
Name inside if or { } |
Still visible after the block | A shadow lasts only until } |
| Constant | const. Assigning to it is a parse error |
const NAME: Type = value. Never mut |
| Immutable field | readonly property |
Fields are immutable unless the whole value is let mut |
A name you bind but never read is an unused-variable warning. Prefix it with _ (let _scratch = 1;) to silence that, the same idea as intentionally unused parameters. You can also declare without a value and assign once: let x; x = 5;. That is still one immutable binding, not mut.
Try it
Open src/main.rs in the Playground. Change "42" to "four" and run it. expect panics because the parse failed. Put "42" back. Under let x = 5;, add x = 6; and read the compiler error. It is E0384 (cannot assign twice to immutable variable). Remove that line. outer still prints 5.
Key takeaways
letis immutable.let mutis the assignment that changes one binding. A second assignment withoutmutisE0384.- Shadowing is a new
letin the same scope.parseis the usual reason. A shadow inside{ }ends at}. constis a typed item inSCREAMING_CASE. It is nevermut. PHP reportssyntax error, unexpected token "="if you assign to aconst._namesilences unused-variable warnings.let x; x = 5;is deferred init of one immutable binding.
What's next: Numbers, bool and char