web-dev-qa-db-ja.com

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

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

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);
    

    未割り当てのローカル変数 '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ブロックに独自のローカルスコープを与えなかったのはなぜですか?これには歴史的または技術的な理由がありますか?

39
p.s.w.g

他のすべてのケースでは、「通常の」ローカル変数のスコープはa block中括弧({})。通常ではないローカル変数は、forループ変数やusingで宣言された変数のように、ステートメント(通常はブロック)の前の特別な構文に現れます。

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

参考までに、ルールは§3.7C#仕様のスコープにあります。

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

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

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

  • foreach-statementsing-statementlock-statementまたはquery-expressionの一部として宣言された変数のスコープは、与えられた構成の展開によって決定されます。

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

24
svick

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

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.");
}
42
Mike Koder

私はエリック・リッパートを引用しますが、その答えは主題に関してかなり明確です:

合理的な質問は、「なぜこれが合法ではないのか」です。理にかなった答えは「まあ、なぜそうあるべきなのか?」 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番目の方法を選択しました。

この決定は、1999年7月7日に行われたもので、10年ほど前のことではありません。その日のメモのコメントは非常に簡潔で、「スイッチケースは独自の宣言スペースを作成しない」と述べ、機能するものと機能しないものを示すサンプルコードを示しています。

この特定の日にデザイナーの心に何があったかについてもっと知るために、私は彼らが10年前に何を考えていたかについて多くの人々を悩ませなければならず、そして結局は些細な問題であるものについて彼らを悩ませなければなりません。私はそうしません。

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

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

10
Cyril Gandon

説明は単純です。これは、Cのようなものだからです。C++のような言語では、JavaとC#は、使いやすさのために、switchステートメントの構文とスコープをコピーしています。

(別の回答で述べたように、C#の開発者には、ケーススコープに関するこの決定がどのように行われたかに関するドキュメントがありません。しかし、C#構文の明記されていない原則は、異なる方法でやる説得力のある理由がない限り、Javaをコピーするというものでした。 )

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

(より根本的に、Gotoは構造化されていません-コードのsectionsを定義または区切らず、ジャンプポイントのみを定義します。したがって、Gotoラベルcannotはスコープを導入します。)

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

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

5
JacquesB

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

switchにはブロックが含まれていないため、異なるスコープを持つことはできません。

4
Guvante