Почему C # не имеет локальной области видимости в случае блоков?

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'

Это было довольно легко решить проблему; просто избавиться от второго varсделал свое дело.

Очевидно, что переменные, объявленные в caseблоках, имеют родительскую область switch, но мне любопытно, почему это так. Учитывая , что C # не позволяет выполнение провалиться других случаях (это требует break , return, throwили goto caseзаявления в конце каждого caseблока), кажется , довольно странно , что это позволило бы объявления переменных в один , 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);
    

    Я получаю « Не могу использовать локальную переменную« действие », прежде чем она объявлена ». Таким образом, порядок использования блоков case здесь важен не совсем очевидным образом. Обычно я могу записывать их в любом порядке, который мне нужен, но поскольку он 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блокам собственную локальную область видимости? Есть ли для этого исторические или технические причины?

PSWG
источник
4
Объявите вашу actionпеременную перед switchоператором или поместите каждый случай в свои фигурные скобки, и вы получите разумное поведение.
Роберт Харви
3
@RobertHarvey: да, и я мог бы пойти еще дальше и написать это switchвообще, не используя a - мне просто любопытно обосновать этот дизайн.
PSWG
если c # нужен перерыв после каждого случая, тогда я предполагаю, что это должно быть по историческим причинам, как в случае c / java, это не так!
tgkprog
@tgkprog Я полагаю, что это не по исторической причине, и что разработчики C # сделали это специально, чтобы убедиться, что распространенная ошибка забывания a breakневозможна в C #.
svick
12
См. Ответ Эрика Липперта на этот связанный с этим вопрос. stackoverflow.com/a/1076642/414076 Вы можете или не можете прийти удовлетворены. Это читается как «потому что именно так они решили сделать это в 1999 году».
Энтони Пеграм

Ответы:

24

Я думаю, что хорошей причиной является то, что в любом другом случае область действия «нормальной» локальной переменной является блоком, ограниченным фигурными скобками ( {}). Локальные переменные, которые не являются нормальными, появляются в специальной конструкции перед оператором (который обычно является блоком), как forпеременная цикла или переменная, объявленная в using.

Еще одним исключением являются локальные переменные в выражениях запросов LINQ, но они полностью отличаются от обычных объявлений локальных переменных, поэтому я не думаю, что есть вероятность путаницы там.

Для справки, правила приведены в §3.7 Области применения спецификации C #:

  • Область локальной переменной, объявленной в объявлении локальной переменной, является блоком, в котором происходит объявление.

  • Видимости локальной переменной , объявленной в выключателя блока в виде switchутверждения является включение блока .

  • Видимости локальной переменной , объявленной в течение-инициализатора в виде forутверждения является для-инициализатор , то для условие , то для-итератора , и содержится утверждение о forзаявлении.

  • Область видимости переменного объявлена как часть Еогеаспа-выписка , используя-заявление , блокировки заявление или запрос-выражение определяется расширением данной конструкции.

(Хотя я не совсем уверен, почему этот switchблок явно упоминается, поскольку у него нет специального синтаксиса для отклонений локальных переменных, в отличие от всех других упомянутых конструкций.)

svick
источник
1
+1 Можете ли вы дать ссылку на объем, который вы цитируете?
PSWG
@pswg Вы можете найти ссылку для загрузки спецификации на MSDN . (Нажмите ссылку «Microsoft Developer Network (MSDN)» на этой странице.)
svick
Поэтому я посмотрел спецификации Java и, switchesпохоже, веду себя одинаково в этом отношении (за исключением необходимости переходов в конце case). Кажется, это поведение было просто скопировано оттуда. Поэтому я думаю, что короткий ответ - caseоператоры не создают блоки, они просто определяют подразделения switchблока - и, следовательно, сами по себе не имеют границ.
PSWG
3
Re: Я не совсем уверен, почему явно упоминается блок переключателей - автор спецификации просто привередлив, неявно указывая, что блок переключателей имеет грамматику, отличную от обычного блока.
Эрик Липперт
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 использовал это, и это работает как описано
Mvision
1
+1 Это хороший трюк, но я приму ответ svick за то, что он ближе всего подходит к моему первоначальному вопросу.
PSWG
10

Я процитирую Эрика Липперта, ответ которого достаточно ясен по этому вопросу:

Резонный вопрос "почему это не законно?" Разумный ответ «ну, почему так должно быть»? Вы можете получить это одним из двух способов. Либо это законно

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 # выбрали второй способ, который кажется более естественным.

Это решение было принято 7 июля 1999 года, всего лишь десять лет назад. Комментарии в примечаниях того дня чрезвычайно кратки, просто заявляя, что «switch-case не создает свое собственное пространство объявлений», а затем приводим некоторый пример кода, который показывает, что работает, а что нет.

Чтобы узнать больше о том, что думали дизайнеры в этот конкретный день, мне нужно было бы расспросить многих людей о том, что они думали десять лет назад, и задать им вопрос о том, что в конечном итоге является тривиальной проблемой; Я не собираюсь этого делать.

Короче говоря, нет особой причины выбирать тот или иной путь; оба имеют свои достоинства. Команда языкового дизайна выбрала один путь, потому что им нужно было выбрать один; тот, который они выбрали, кажется мне разумным.

Так что, если вы не работаете в команде разработчиков C # 1999 больше, чем Эрик Липперт, вы никогда не узнаете точную причину!

Кирилл Гандон
источник
Не downvoter, но это был комментарий, сделанный вчера на вопрос .
Джесси С. Slicer
4
Я вижу это, но поскольку комментатор не публикует ответ, я подумал, что было бы неплохо явно создать ответ, цитируя содержание ссылки. И мне плевать на отрицательные голоса! ОП спрашивает историческую причину, а не ответ «Как это сделать».
Кирилл Гандон
5

Объяснение простое - это потому, что это похоже на то, что в C. Такие языки, как C ++, Java и C #, скопировали синтаксис и область видимости оператора switch для удобства.

(Как указано в другом ответе, разработчики C # не имеют документации о том, как было принято это решение в отношении кейс-областей. Но неустановленный принцип для синтаксиса C # заключался в том, что, если у них не было веских причин сделать это по-другому, они копировали Java. )

В C операторы case похожи на goto-label. Оператор switch - действительно лучший синтаксис для вычисляемого перехода . Случаи определяют точки входа в блок переключателей. По умолчанию остальная часть кода будет выполнена, если нет явного выхода. Так что имеет смысл только использовать одну и ту же область.

(Более фундаментально. Goto не структурированы - они не определяют или не разграничивают разделы кода, они только определяют точки перехода. Поэтому метка goto не может вводить область.)

C # сохраняет синтаксис, но вводит защиту от «провала», требуя выхода после каждого (не пустого) предложения case. Но это меняет наше представление о переключателе! Случаи теперь представляют собой альтернативные ветви , как ветви в if-else. Это означает, что мы ожидаем, что каждая ветвь будет определять свою собственную область видимости, как if-предложения или предложения итерации.

Итак, вкратце: дела имеют одинаковую область действия, потому что это то же самое, что и в C. Но это кажется странным и непоследовательным в C #, потому что мы рассматриваем случаи как альтернативные ветви, а не как цели goto.

JacquesB
источник
1
« это кажется странным и непоследовательным в C #, потому что мы рассматриваем дела как альтернативные ветви, а не как цели goto ». Это именно то заблуждение, которое у меня было. +1 за объяснение эстетических причин, почему он чувствует себя не так в C #.
PSWG
4

Упрощенный способ просмотра области видимости заключается в рассмотрении области видимости по блокам {}.

Поскольку switchне содержит никаких блоков, он не может иметь разные области видимости.

Guvante
источник
3
Как насчет for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Там нет блоков, но есть разные области применения.
svick
@svick: Как я уже сказал, упрощенно, есть блок, созданный forоператором. switchне создает блоки для каждого случая, только на верхнем уровне. Сходство в том, что каждый оператор создает один блок (считая switchоператор).
Гуванте
1
Тогда почему ты не можешь считать case?
svick
1
@svick: Потому caseчто не содержит блок, если только вы не решите добавить его. forдлится до следующего оператора, если вы не добавите {}его, поэтому он всегда имеет блок, но caseдлится до тех пор, пока что-то не заставит вас покинуть реальный блок, switchоператор.
Guvante