8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-18

##自己紹介

昔は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が自分のうっかり記事というのもどうかと思いましたが、同じように誤解している方もいるかもしれないと思い、書かせて頂きました。参考になれば幸いです!

8
0
2

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
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?