C#がcaseブロックにローカルスコープを持たないのはなぜですか?


38

私はこのコードを書いていました:

private static Expression<Func<Binding, bool>> ToExpression(BindingCriterion criterion)
{
    switch (criterion.ChangeAction)
    {
        case BindingType.Inherited:
            var action = (byte)ChangeAction.Inherit;
            return (x => x.Action == action);
        case BindingType.ExplicitValue:
            var action = (byte)ChangeAction.SetValue;
            return (x => x.Action == action);
        default:
            // TODO: Localize errors
            throw new InvalidOperationException("Invalid criterion.");
    }
}

そして、コンパイルエラーを見つけて驚いた:

「action」という名前のローカル変数がこのスコープで既に定義されています

解決するのは非常に簡単な問題でした。2番目の要素を取り除くだけでvarうまくいきました。

明らかに宣言された変数caseのブロックは親のスコープを持っているswitchが、私はこれが理由として興味があります。C#は、実行が(それは他のケースを落下することはできませんことを考える必要があり breakreturnthrow、またはgoto caseすべてのの終わりに文caseブロック)、1内部変数宣言ができるようになるということは非常に奇妙に思えるcase他に使用されるか、または変数と競合することがcase。言い換えると、変数はcase、実行できない場合でもステートメントを通り抜けるように見えます。C#は、混乱したり簡単に悪用されたりする他の言語の一部の構造を禁止することにより、読みやすさを高めるために多大な苦労をしています。しかし、これは混乱を引き起こすだけのように思えます。以下のシナリオを検討してください。

  1. これに変更する場合:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        return (x => x.Action == action);

    未割り当てのローカル変数「アクション」の使用」を取得します。C#の他のすべてのコンストラクトでvar action = ...は、変数を初期化すると考えられるため、これは混乱を招きますが、ここでは単純に宣言しています。

  2. 私がこのようなケースを交換した場合:

    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        return (x => x.Action == action);
    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);

    宣言される前にローカル変数 'action'を使用できません」と表示されます。したがって、ここではケースブロックの順序は完全には明らかではない方法で重要に見えます-通常、私は望む順序でこれらを書くことができますが、使用されるvar最初のブロックに現れる必要があるため、ブロックactionを微調整caseする必要がありますそれに応じて。

  3. これに変更する場合:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        goto case BindingType.Inherited;

    エラーは発生しませんが、ある意味で、変数は宣言される前に値が割り当てられているように見えます。
    (あなたが実際にこれをやりたいと思う時間は考えられませんが、goto case今日まで存在することすら知りませんでした)

私の質問は、なぜC#のデザイナーがcaseブロックに独自のローカルスコープを与えなかったのかということです。これには歴史的または技術的な理由がありますか?


4
ステートメントのaction前に変数を宣言するswitchか、各ケースを独自の中括弧に入れると、賢明な動作が得られます。
ロバートハーヴェイ

3
@RobertHarveyはい、私はさらに進んswitchで、まったく使用せずにこれを書くことができます-私はこのデザインの背後にある理由に興味があります。
pswg

c#がすべてのケースの後に休憩を必要とする場合、私はそれがそうではないc / javaのように歴史的な理由である必要があると思います!
tgkprog

@tgkprogこれは歴史的な理由によるものではなく、C#の設計者は、a break#を忘れるというよくある間違いがC#では不可能であることを確認するために意図的にそれを行ったと思います。
svick

12
この関連するSOの質問に関するEric Lippertの回答を参照してください。stackoverflow.com/a/1076642/414076満足するかもしれないし、しないかもしれません。それは「1999年に彼らがそれをすることを選んだ方法だから」と読みます。
アンソニーペグラム

回答:


24

正当な理由は、他のすべてのケースで、「通常の」ローカル変数のスコープがブレース()で区切られたブロックだからだと思います{}。正常ではないローカル変数は、forループ変数やで宣言された変数のように、ステートメント(通常はブロック)の前の特別な構造に現れますusing

もう1つの例外はLINQクエリ式のローカル変数ですが、これらは通常のローカル変数宣言とはまったく異なるため、そこで混乱する可能性はないと思います。

参考のため、規則は§3.7C#仕様の範囲にあります。

  • local-variable-declarationで宣言されたローカル変数のスコープは、宣言が発生するブロックです。

  • ステートメントのswitch-blockで宣言されたローカル変数のスコープはswitch-blockです。switch

  • ステートメントのfor-initializerで宣言されたローカル変数のスコープforは、for-initializerfor-conditionfor-iterator、およびステートメントに含まれるステートメントですfor

  • foreach-statementusing-statementlock-statementまたはquery-expressionの一部として宣言された変数のスコープは、指定されたコンストラクトの展開によって決定されます。

switchブロックが明示的に言及されている理由は完全には分かりませんが、他の言及されたすべての構成とは異なり、ローカル変数の宣言のための特別な構文はありません。)


1
+1引用しているスコープへのリンクを提供できますか?
pswg

@pswg仕様をダウンロードするためのリンクは、MSDNにあります。(そのページの「Microsoft Developer Network(MSDN)」というリンクをクリックしてください。)
svick

そこで、Javaの仕様を調べて、switchesこの点で同じように振る舞うようです(の末尾にジャンプが必要な場合を除くcase)。この動作はそこから単純にコピーされたようです。簡単な答えは、caseステートメントはブロックを作成せず、switchブロックの分割を単に定義するだけであり、したがってそれ自体ではスコープを持たないことだと思います。
pswg

3
Re:switchブロックが明示的に言及されている理由は完全にはわかりません。仕様の作成者は、スイッチブロックが通常のブロックとは異なる文法を持っていることを暗黙的に指摘しているだけです。
エリックリッパー

42

しかし、そうです。で行をラップすることで、どこでもローカルスコープを作成できます{}

switch (criterion.ChangeAction)
{
  case BindingType.Inherited:
    {
      var action = (byte)ChangeAction.Inherit;
      return (x => x.Action == action);
    }
  case BindingType.ExplicitValue:
    {
      var action = (byte)ChangeAction.SetValue;
      return (x => x.Action == action);
    }
  default:
    // TODO: Localize errors
    throw new InvalidOperationException("Invalid criterion.");
}

Yupはこれを使用し、説明
どおりに

1
+1これは良いトリックですが、元の質問に最も近づいたというsvickの答えを受け入れます。
pswg

10

Eric Lippertの言葉を引用しますが、その答えは主題に関してはかなり明確です。

合理的な質問は、「なぜこれが合法ではないのですか?」です。合理的な答えは「まあ、なぜそうなのか」です。2つの方法のいずれかを使用できます。これは合法です:

switch(y) 
{ 
    case 1:  int x = 123; ...  break; 
    case 2:  int x = 456; ...  break; 
}

またはこれは合法です:

switch(y) 
{
    case 1:  int x = 123; ... break; 
    case 2:  x = 456; ... break; 
}

しかし、両方の方法を使用することはできません。C#の設計者は、より自然な方法であると思われる2番目の方法を選択しました。

この決定は、わずか10年前の1999年7月7日に行われました。その日のメモのコメントは非常に短く、「スイッチケースは独自の宣言スペースを作成しません」と述べ、何が機能して何が機能しないかを示すサンプルコードを提供します。

この特定の日にデザイナーの心にあったことをもっと知るには、10年前に考えていたことについて多くの人々をバグにしなければならず、最終的に些細な問題についてバグを立てる必要があります。私はそうするつもりはありません。

つまり、いずれかの方法を選択する特に説得力のある理由はありません。どちらにもメリットがあります。言語設計チームは、1つを選択する必要があるため、1つの方法を選択しました。彼らが選んだものは私にとって理にかなっているようです。

したがって、1999年のC#開発者チームにエリックリッパーよりも時間がない限り、正確な理由はわかりません。


ダウナーではありませんが、これは昨日の質問に対するコメントでした。
ジェシーC.スライサー

4
私はそれを見るが、コメント者が答えを投稿しないので、リンクのコンテンツを引用することによって明示的にアンサーを作成することは良い考えだと思った。そして、私は下票を気にしません!OPは、「それを行う方法」の答えではなく、歴史的な理由を尋ねます。
シリルガンドン

5

説明は簡単です-Cのようなものだからです。C++、Java、C#のような言語は、慣れ親しむためにswitchステートメントの構文とスコープをコピーしました。

(別の回答で述べたように、C#の開発者には、ケーススコープに関するこの決定がどのように行われたかについてのドキュメントがありません。 )

Cでは、caseステートメントはgoto-labelsに似ています。switchステートメントは、計算されたgotoの構文としては本当に優れています。ケースは、スイッチブロックへのエントリポイントを定義します。デフォルトでは、明示的な終了がない限り、残りのコードが実行されます。したがって、同じスコープを使用するだけで理にかなっています。

(より基本的には、Gotoは構造化されていません。コードのセクションを定義または区切るのではなく、ジャンプポイントのみを定義します。したがって、Gotoラベルスコープを導入できません。)

C#は構文を保持しますが、各(空でない)case節の後に終了を要求することにより、「フォールスルー」に対する保護手段を導入します。しかし、これはスイッチの考え方を変えます!ケースは、if-elseのブランチのような代替ブランチになりました。これは、各分岐がif-clausesまたは反復句のように独自のスコープを定義することを期待することを意味します。

つまり、これはCの方法であるため、ケースは同じスコープを共有します。しかし、C#では、gotoターゲットではなく代替ブランチとしてケースを考えるため、奇妙で一貫性がないように見えます。


1
C#では、ケースをgotoターゲットではなく代替ブランチと考えているため、奇妙で一貫性がないように思われます」これはまさに私が抱えていた混乱です。+1は、C#で間違っていると感じる理由の背後にある美的理由を説明します。
pswg

4

スコープを見る簡単な方法は、ブロックごとにスコープを考慮すること{}です。

以来switch任意のブロックが含まれていない、それは異なるスコープを持つことができません。


3
どうfor (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */?ブロックはありませんが、異なるスコープがあります。
svick

@svick:私が言ったように、単純化すると、forステートメントによって作成されたブロックがあります。switch各ケースにブロックを作成するのではなく、最上位にのみ作成します。類似点は、各ステートメントが1つのブロックを作成することです(switchステートメントとしてカウント)。
グバンテ

1
では、なぜあなたはcase代わりに数えられなかったのですか?
svick

1
@svick:caseオプションでブロックを追加することを決めない限り、ブロックが含まれていないためです。for追加した場合を除き{}、常にブロックを持っている場合を除き、次のステートメントの間case続きますが、何かが実際のブロックであるswitchステートメントを離れるまで続きます。
グバンテ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.