在 Rust 中,遮蔽(shadowing)是一種允許重複使用相同變數名稱的特性。
遮蔽會在變數作用域內逐層生效,即在某一層的變數遮蔽了外層或之前定義的同名變數,當該層作用域結束後,外層的變數會重新生效。
main() {
let x = 5;
println!("{}", x); // 5
let x = x + 1; // 遮蔽前一個 x
println!("{}", x); // 6
let x = x * 2; // 再次遮蔽
println!("{}", x); // 12
}
特別的是,再宣告一個相同名稱的變數,並不是原本變數的值被更新了,而是創建了一個新的變數,只是原本的變數被遮蔽了所以編譯器讀取到的會是新的變數,佔據變數名稱的使用權,直到它自己也被遮蔽或是離開作用域:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("x 在內部範圍的數值為:{x}");
} // 內層定義 x 離開作用域,外部 x 重新生效
println!("x 的數值為:{x}");
}
$ cargo run
x 在內部範圍的數值為:12
x 的數值為:6
這種做法有幾個好處:
fn main() {
let start: i32 = 1;
println!("{}", start); // 1
let start: bool = true; // 這行之後 1 被遮蔽了
println!("{}", start); // true
}
這樣的設計和可變性(mutability)之間形成有一種有趣的關係,讓我們可以在不違反 Rust 安全原則的情況下,靈活地改變變數的值和型別。
安全性來說,新的變數一樣預設不可變,不受原本變數影響,所以如果直接去更新值還是一樣會報錯,驗證看看:
fn main() {
let mut x = 5;
x = x + 1;
{
let x = x * 2;
x = x + 1;
println!("x 在內部範圍的數值為:{x}");
}
println!("x 的數值為:{x}");
}
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:23:9
|
22 | let x = x * 2;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
23 | x = x + 1;
| ^^^^^^^^^ cannot assign twice to immutable variable
比較一下一開始的例子,如果我們不使用遮蔽而使用可變變數的情況:
fn main() {
let mut x = 5;
println!("{}", x); // 5
x = x + 1; // 直接修改 x 的值
println!("{}", x); // 6
x = x * 2; // 再次修改 x 的值
println!("{}", x); // 12
}
這個版本看起來更簡潔,但有幾個潛在的問題:
複雜數據轉換:當需要對數據進行多次轉換,但是不想宣告新變數的時候。這樣可以保持變數名稱的一致性,而不需要引入新的名稱,例如金額計算的時候。
fn main() {
let service_charge = true; // 要不要收服務費
let price = 100.0; // 基本價格
let price = price * 1.08; // 加上 8% 的稅
let price = price + 5.0; // 加上 5 元的點餐費
let price = if service_charge {
price * 1.1 // 如果需要加收服務費
} else {
price // 如果不需要加收服務費
};
println!("The final price is: ${:.0}", price);
}
型別轉換:有時候最初拿到的型別只是暫時的,和我們實際要用的型別不同的時候,就不用變數名稱裡要再加上型別區分。
use std::io;
fn main() {
println!("Please type a integer:");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = guess.trim().parse()
.expect("Input is not a integer");
// 其他操作、數字比較等等
}
不只是 Rust ,很多其他程式語言也有 Shadowing 的概念。
和 JavaScript 比較一下:
let x = 5;
{
let x = 10;
console.log(x); // 10, 內部作用域的 x
}
console.log(x); // 5, 外部作用域的 x
這個情況看起來差異不大,但其他情況就有差了。
JavaScript 要宣告一個已經被宣告的變數,要在不同作用域。
如果在同一個作用域這樣寫就會報錯:
let x = 5;
let x = 10;
console.log(x);
那再來我們會改成這樣寫:
let x = 5;
x = 10;
console.log(x); // 10,
這樣可以的確正確執行,但其實就是 Rust 用可變變數的寫法,缺點也是一樣的,這個變數不具備不可變的特性,後面操作可以很輕易地改變原本的數值。當然 JavaScript 倒不會被型別限制住就是。
那 C++ 的情況呢?
#include <iostream>
int main() {
int x = 5; // 外部作用域中的 x
if (true) {
int x = 10; // 內部作用域中的 x,遮蔽了外部的 x
std::cout << "Inner x: " << x << std::endl; // 這裡輸出的是內部的 x,值為 10
}
std::cout << "Outer x: " << x << std::endl; // 這裡輸出的是外部的 x,值仍為 5
return 0;
}
比較一下 Rust 的版本:
fn main() {
let x = 5; // 外部作用域中的 x
if true {
let x = 10; // 內部作用域中的 x,遮蔽了外部的 x
println!("Inner x: {}", x); // 這裡輸出的是內部的 x,值為 10
}
println!("Outer x: {}", x); // 這裡輸出的是外部的 x,值仍為 5
}
乍看還是和 Rust 沒有太大差別,但有一個差別是,和 Rust 是顯式地引入遮蔽(需要開發者明確地用 let 宣告,換句話說,開發者主動的),而 C++ 不是,沒有仔細看很容易沒發現現在看到的變數是在哪一層的作用域中,容易造成非預期影響。Rust 的這種顯式遮蔽設計不僅提高了代碼的可讀性,還能在編譯時就捕獲潛在的錯誤,體現了Rust 對安全性的重視。
Rust 的遮蔽機制讓我們能夠在保持變數不可變性的同時,實現一種受控的可變性。這種設計體現了 Rust 的哲學:透過語言設計來提供安全保證,同時保持足夠的彈性。兼顧彈性的設計都是從很多細節的地方累積起來的,Rust 真的是一個很細心的語言。