🥷

Rust は同じスコープで同じ名前の変数を複数宣言できる (シャドーイング)

2024/08/26に公開

はじめに

シャドーイング (shadowing) は、外側のスコープに存在する変数と同名の変数を内側のスコープで宣言すると、外側の変数にはアクセスできなくなるという、ごく一般的な機能です。一方で Rust は同じスコープで同名の変数を複数宣言し、前に宣言された変数をシャドーイングすることができます。この記事では、Rust のシャドーイングの挙動、シャドーイングで何ができるかについて解説します。

対象読者

  • 何らかのプログラミング言語を習得している
  • Rust の基礎的な文法や所有権の概念を理解している

要約

  • Rust は同じスコープで同名の変数を複数宣言することができる。後に宣言された変数は前に宣言された変数をシャドーイングする
  • シャドーイングは、一時的な変数の命名の手間を省き、用済みになったこれらの変数の使用をその後使用できなくすることができる
  • シャドーイングは、可変な値を不変にしたり、逆もできる

シャドーイング

シャドーイングといえば、外側のスコープで宣言した変数と同名の変数を現在のスコープで宣言すると、
現在のスコープの変数が外側のスコープの変数を隠す、よくある機能です。

例えば、Rust で以下のようなコードを書いてみます。

fn main() {
    // スコープの外側で `food` という名前の変数を宣言
    let food = "sushi🍣";

    // ブロックの作成
    {
        // ブロックの内側で同名の変数を宣言
        let food = "sakana🐟";
    
        // `food` の内容を出力する
        println!("内側: {food}");
    }

    // `food` の内容を出力する
    println!("外側: {food}");
}

実行結果は以下のとおりになります。

内側: sakana🐟
外側: sushi🍣

ブロックの内側で food を出力しようとすると、sushi🍣 ではなく sakana🐟 が出力されています。外側の変数と同じ名前の変数を内側で宣言してもエラーにはならず、また、そのブロック内では外側の foodへアクセスできなくなります。これがシャドーイングです。他言語でもよくありますよね。

Rust は同じスコープでシャドーイングができる

Rust の面白い特徴として、同じスコープ内で既存の変数をシャドーイングできるという点が挙げられます。

つまり、同じスコープ内で既存の変数と同じ名前の変数を宣言することができるのです!

// 1個目の `food` の宣言
let food = "sushi🍣"; 
println!("{food}"); // 出力: sushi🍣
w
// 同名の変数を宣言する
// 最初の `food` はシャドーイングされ、これ以降 sushi🍣 にアクセスできない
let food = "sakana🐟"; 
println!("{food}"); // 出力: sakana🐟

再代入ではないことに気をつけてください。1回目と2回目の money の両方で let キーワードが使われているので、どちらも立派な変数宣言です。

シャドーイングと再代入の違い

これだけ見ていると、シャドーイングは可変変数への再代入と同じように見えますね。

1個目の変数に mut をつけて可変変数として宣言して再代入することと、1個目の変数を不変変数として宣言してシャドーイングすることではどのような違いがあるのでしょうか?

同名の違う型の変数によるシャドーイング

シャドーイングでは、型が異なる同じ名前の変数を宣言することができます。

let number: i64 = 256; // `i32` (32ビット整数) 型の変数
println!("{number}"); // 出力: 256

let number: &str = "にひゃくごじゅうろく"; // `&str` (文字列) 型の変数
println!("{number}"); // 出力: にひゃくごじゅうろく

より内側のスコープから値が変えられることがない

不変変数は、名前の通り書き換えることができません。次のようなコードを考えてみましょう。

let message = "こんにちは"; // <-- 1個目の不変変数を宣言

// ..これ以降 `message` にアクセスした時の値は "こんにちは"

// `message` が宣言されたスコープより内側のスコープ
{
    // 再代入は可変変数に対してのみ行える
    // スコープの内側から外側の `message` を書き換えることはできない
    message = "おはよう"; // エラー! 不変変数へ再代入はできない
}

let message = "こんばんは"; // <-- 2個目の不変変数を宣言

// ..これ以降 `message` にアクセスした時の値は "こんばんは"

1個目の変数を不変変数として宣言したので、その変数が存在するスコープより内側のスコープでこの変数が書き換えることができません。

一方で、2個目の変数宣言(シャドーイング)以降、message にアクセスした時に得られる値が こんにちは から こんばんは見かけ上変化します

message という変数にアクセスした時に得られる値がどこで変わるかは、同じスコープの let message がある行を考慮するだけでよいため、再代入と違って値の変化が追いやすいメリットがあります。

シャドーイングのつかいどころ

一時的な変数

関数の引数や、ユーザーからのインプットを処理するときに、一時的な変数を作ることはよくあります。

例えば、ファイルパスの配列の各要素のファイルを取得したい状況を考えてみましょう。具体的にどういった処理を行なっているかは気にせず、単純に変数にはどういった値が束縛されているかについてのみ述べます。

use std::path::Path;

fn main() {
    // 取得したいファイルのパスの配列
    let files = [ // <-- (#1)
        Path::new("a.txt"),
        Path::new("b.txt"),
        Path::new("c.txt"),
        Path::new("d.txt"),
        Path::new("e.txt"),
    ];
    
    // 各ファイルパスから取得したファイルの中身の配列
    let files: Vec<String> = files // <-- 1個目の `files` にアクセス
            .iter()
            .map(|file| std::fs::read_to_string(file).unwrap())
            .collect();

    // ... 以下 `files` を使った処理が続く 
}

1個目の files は、2個目のファイルの取得するためだけに必要な一時的な変数です。

2個目の files では、1個目の files の値にアクセスしつつ、1個目をシャドーイングしてアクセスできなくしています。つまり、1個目が役目を終えた時点でアクセス不可になっており、今後使ってしまう可能性が一切なくなります。

また、一時的な変数を作る過程で命名の手間が省ける利点もあります。

可変な値を不変にする

Rust の所有権とシャドーイングを組み合わせると、可変の値を不変にすることができます。

// `String` 型の可変変数を宣言
let mut message = String::from("から");

// `message` は可変なので文字列を追加できる
message.push_str("あげ"); 

println!("{message}"); // 出力: からあげ

// 1個目の可変変数 `message` に束縛されていた `String` の所有権を
// 2個目の不変変数 `message` にムーブする
let message = message;

// エラー! `message` は不変なので文字列を追加できない
message.push_str("にレモンかけといたよ🍋");

1個目の messagemut キーワードがついているので、可変変数です。そのため、文字列の追加、編集が可能です。String 型の push_str() は文字列の末尾に文字列を追加するメソッドです。

2個目の message の宣言の行では、右辺の可変変数である message の値を、左辺の新しい不変変数 message に束縛しています。この時点で、String の所有権は1個目から2個目の message にムーブされ、1個目の message は無効になります。

2個目の message は不変変数なので、String の内容を編集することができなくなりました。

最初は値を可変にする必要があったけど、これ以降は可変にする必要がないな、というときは値を不変変数に束縛させると、意図しない変更を防ぐことができます。

一時的に変数を可変にする

前項の「可変を不変にする」とは逆に、不変な値を一時的に可変にするトリッキーな使い方です。こんなことができるんだ程度で読んでください。

let message = String::from("から"); 
let message = {
    let mut message = message;
    message.push_str("あげ");
    message
};

// エラー! `message` は不変なので文字列を追加できない
message.push_str("にレモンかけといたよ🍋");

このコードでは message という変数が3回宣言されています。複雑なので、以下のようにそれぞれ (#1), (#2), (#3) という名前を振ってコメントを加えてみました。

// 不変変数 `message` (#1) を宣言し、`String` 型の値を束縛する
let message = String::from("から"); // <-- (#1)

// 不変変数 `message` (#3) を宣言
// 一時的に可変にした変数を最終的に不変に戻す役割をもつ
// { } で評価された値を (#3) に束縛する
let message = {
//  ^^^^^^^(#3)

    // スコープ内で新たに可変変数 `message` (#2) を宣言
    // `String` の所有権を、右辺の (#1) から 左辺 (#2) にムーブする
    let mut message = message;
    //      ^^^^^^^   ^^^^^^^(#1)
    //      |         
    //      (#2)

    // (#2) は可変なので文字列を追加できる
    message.push_str("あげ");

    // (#2) の値を返す
    message
};

// エラー! `message` (#3) は不変なので文字列を追加できない
message.push_str("にレモンかけといたよ🍋");

初めて見るとどういった動きをしているか分からないので、1行ずつ見ていきましょう。

まずは、大元となる最初の不変変数 (#1) の宣言です。

let message = String::from("から"); // <-- (#1)

次に、再び不変変数 (#3) の宣言が行われています。右辺が { } ブロックで囲まれていますが、これは、ブロックで評価された値が最終的に (#3) に束縛される値になるという意味です。

ブロックの中で一時的に message を可変にし、可変操作をした後、戻り値として message を不変である (#3) に返すことで、再び不変にするという戦略です。

let message = {
//  ^^^^^^^(#3)

(#3) の右辺を追ってみましょう。String の所有権は不変変数 (#1) から可変変数 (#2) にムーブされ、String の編集が可能になります。この時点で、(#1) は無効になります。

また、(#2) は外側のスコープにある (#1) をシャドーイングしており、無効になった (#1) へのアクセスを防いでいます。

    let mut message = message;
    //      ^^^^^^^   ^^^^^^^(#1)
    //      |         
    //      (#2)

    // (#2) は可変なので文字列を追加できる
    message.push_str("あげ");

最後に、文字列の追加が終わり、可変である必要がなくなったので、message をブロックの最後の行にことで、message 自体をブロックの評価結果として返します。値は、不変変数 (#3) に束縛され、再び不変になります。

    message

(#3) の変数宣言は (#1) をシャドーイングしており、既に無効となった (#1) へのアクセスを防いでいます。(#3) は "からあげ" になっています。

(#3) は不変変数なので、文字列の追加はできません。

// エラー! `message` (#3) は不変なので文字列を追加できない
message.push_str("にレモンかけといたよ🍋");

このように、最初に不変として定められたものを、小さいスコープ内で一時的に可変にして、また不変に戻すという操作が可能です。また、(#1), (#2), (#3) を同名で宣言することは、所有権のムーブによって無効になった変数へのアクセスを防ぐ効果もあります。

ただし、この書き方を多用すると可読性をさげてしまう可能性があるので注意が必要です。

おわりに

Rust のシャドーイングの機能について解説しました。乱用すると可読性が落ちてしまいますが、用済みの変数をアクセスできなくしたり、可変の値を不変にすることができるなど、価値のある機能です。状況と好みに応じて使ってみてください。🦀⚙️

Discussion