LoginSignup
0

More than 1 year has passed since last update.

posted at

updated at

変数スコープを誤解していた件

自己紹介

昔はDelphi、ここ数年はC#で業務アプリを開発しているarimooです。
もうLINQとリフレクションのない世界には戻れそうにありません。

Qiita初投稿、Advent Calendar初参加です。よろしくお願い致します。

本記事は「C# その2 Advent Calendar 2020」19日目です。

発端

いつも参考にさせて頂いている「未確認飛行C」の記事C# の null 判定の話を拝見していて(記事の趣旨とは無関係な部分で)気になるコード例が...

(引用開始)

void M(A a)
{
    if (a.X is not { } x) return; // null だったら early return。

    // x を使って何か処理をする。
    // ここでは x に非 null な値が入っているはず。
}

(引用終了)

あれっ!? if文カッコ内の"x"のスコープってifブロックで閉じてなかったっけ...?

検証

※環境はVisualStudio2019、ターゲットフレームワークを.NET 5.0にしたWinFormsアプリで検証しています。

という訳で適当なクラスAを作成し、記事中の"x を使って何か処理をする。"にDebug.WriteLineでxを表示させます。

クラスAとメソッドM
class A
{
    public string X;
}

void M(A a)
{
    if (a.X is not { } x) return; // null だったら early return。

    // x を使って何か処理をする。
    Debug.WriteLine($"x: {x}"); //ここでエラーが出ていない!
}

VS上でこのコードを書いてもエラーが検出されないので、この時点で"x"がifブロックの外で参照できる事、私が変数スコープを誤解していた件が確定っ!

早くもタイトルを回収してしまいましたが、でもまぁ折角コードを書いたので気を取り直して上記を呼び出してみます。

メソッドMの呼び出し側
//XがNULL
A a = new(); //Viva! C#9.0
M(a); //early returnが効いて、何も出力されない

//Xを埋める
a.X = "hoge";
M(a); //"x: hoge"が出力される

...と、想定通り(※)の挙動になりました。

※私のではなく、元記事の想定の通り...

なぜ誤解したか...?

ぱっと見の印象

for文
for (int i = 0; i < 10; i++)
{

}
Debug.WriteLine($"{i}"); //iはスコープ外、ダメ

string[] strs = new string[] { "aaa", "bbb", "ccc" };

foreach (var s in strs)
{

}
Debug.WriteLine($"{s}"); //sはスコープ外、ダメ

上記for/foreach文やメソッドの引数のように、{}ブロック外で使えないという印象が強く効いていました。

マイクロソフトのドキュメントの誤読

"expr is type varname"に対応した当時に読んだ内容と同じかどうかは不明ですが...

マイクロソフトのドキュメント中、

(引用開始)

C#
expr is type varname

expr が true であり is が if ステートメントに使用されている場合は、varname は if ステートメント内のみに割り当てられます。 varname のスコープは、is 式から if ステートメントを閉じるブロックの末尾までになります。 他の任意の場所に varname を使用すると、割り当てられていない変数の使用によるコンパイル時エラーが生成されます。

(引用終了)

...の赤字部分を見て早とちりして、その先の青字をさらっと流してしまっていました。
冷静に考えるとスコープ外なら"現在のコンテキストにvarnameは存在しません"になるはずですね...

※とはいえ"varname のスコープは、is 式から if ステートメントを閉じるブロックの末尾までになります。"はダウトかな~。

"割り当てられていない変数の使用によるコンパイル時エラー"という事は、割り当てればエラーにならないハズ! ...という事で、検証した内容をコメント文に記載しています。
(最初の検証で上手く行く事は判っていますが、実際に弄ってみるとフロー解析の賢さを体験できます!)

検証コード
string s1 = null;

if (s1 is string w1)
{
    //ここに入る場合、w1はs1の中身で埋まっている
    w1 += ":TRUE"; //埋まっているから+=もOK
}
else
{
    //ここに入る場合、w1は未割当の状態
    w1 = "FALSE"; //ここでw1への代入で、WriteLineでも使える状態になる
    //w1 += "FALSE"; //先にこれはダメ。w1は"未割当"エラーになる

    //return; //returnするならWriteLineまで行かないからw1埋めなくてもOK!
}

Debug.WriteLine($"{w1}"); //elseブロックをコメントアウトすると、w1は"未割当"エラーになる

w1をifブロック外で使うためにはelse側で埋めてあげるかreturnする必要がある、という事ですね!
(is notにするとifブロック側で埋めるかreturnする必要があります)

まとめ

便利に使っているif (obj is Hoge h)構文のhが、ifブロックの外側でも使えるとは目から鱗でした!

※今のところ、元記事にあるような演算子オーバーロード回避の需要が発生しない限り使わないとは思いますが...(^^;
(そもそも演算子オーバーロード自体を余り使っていない...)

初投稿&初Advent Calendarが自分のうっかり記事というのもどうかと思いましたが、同じように誤解している方もいるかもしれないと思い、書かせて頂きました。参考になれば幸いです!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0