Чому у 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.");
    }
}

І був здивований, виявивши помилку компіляції:

Локальна змінна назва "дія" вже визначена в цій області

Вирішити це було досить просто; просто позбувшись другого 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);

    Я отримую " Неможливо використовувати локальну змінну" дія "до того, як вона буде оголошена ". Отже, порядок блоків регістрів тут є важливим таким чином, що не зовсім очевидно - зазвичай я можу писати їх у будь-якому бажаному порядку, але тому, що 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
Дивіться відповідь Еріка Ліпперта на це пов'язане з цим питанням. stackoverflow.com/a/1076642/414076 Ви можете, а може і не залишитися задоволеними. Це звучить як "тому, що саме так вони вирішили зробити це в 1999 році."
Ентоні Пеграм

Відповіді:


24

Я думаю, що вагомою причиною є те, що в будь-якому іншому випадку область "нормальної" локальної змінної - це блок, обмежений дужками ( {}). Локальні змінні, які не є нормальними, з'являються у спеціальній конструкції перед оператором (який, як правило, є блоком), як forпеременная циклу або змінна, оголошена в using.

Ще одним винятком є ​​локальні змінні в виразах запитів LINQ, але вони повністю відрізняються від звичайних декларацій локальної змінної, тому я не думаю, що існує ймовірність плутанини там.

Для довідки, правила містяться в §3.7 Області застосування специфікації C #:

  • Область локальної змінної, оголошеної в локальній змінній-декларації, є блоком, в якому відбувається декларація.

  • Видимості локальної змінної , оголошеної в вимикача блоку у вигляді switchтвердження є включення блоку .

  • Видимості локальної змінної , оголошеної в течение-ініціалізатор у вигляді forтвердження є для-ініціалізатор , то для умову , то для-ітератора , і міститься твердження про forзаяві.

  • Область змінної, оголошеної частиною оператора foreach , using-statement , lock-statement або query-izraz визначається розширенням даної конструкції.

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


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 року, лише сором'язливо десять років тому. Зауваження у примітках цього дня є надзвичайно короткими, просто зазначаючи, що "випадок комутації не створює власний простір декларацій", а потім надає деякий зразок коду, який показує, що працює, а що ні.

Щоб дізнатися більше про те, що було в думках дизайнерів у цей конкретний день, мені доведеться клопотати багато людей про те, про що вони думали десять років тому, - і запитати їх про те, що в кінцевому рахунку є тривіальним питанням; Я не збираюся цього робити.

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

Тож, якщо ви більше не будете інтим з командою розробників C # 1999 року, ніж Ерік Ліпперт, ви ніколи не дізнаєтесь точної причини!


Не прихильник, але це був коментар, який було зроблено вчора .
Джессі К. Слікер

4
Я бачу це, але оскільки коментатор не опублікував відповіді, я подумав, що це було б хорошою ідеєю явно створити привид, цитуючи вміст посилання. І я не байдужий проти низів! ОП запитують історичну причину, а не відповідь "Як це зробити".
Кирило Гендон

5

Пояснення просте - це тому, що воно подібне до C. Такі мови, як C ++, Java та C #, скопіювали синтаксис та обстеження оператора перемикання заради ознайомлення.

(Як сказано в іншій відповіді, розробники C # не мають документації про те, як було прийнято це рішення щодо обсягів справи. Але нестаціонарний принцип для синтаксису C # полягав у тому, що, якщо у них не було вагомих причин зробити це по-іншому, вони скопіювали Java. )

У З твердженнями регістру подібні до goto-міток. Оператор переключення - це дійсно приємніший синтаксис для обчисленого goto . Випадки визначають точки входу в блок комутатора. За замовчуванням решта коду буде виконана, якщо немає явного виходу. Тож має сенс лише використовувати один і той же обсяг.

(Більш принципово. Goto не структуровані - вони не визначають і не розмежовують розділи коду, вони лише визначають точки стрибків. Тому мітка goto не може ввести область застосування.)

C # зберігає синтаксис, але вводить захист від "проникнення", вимагаючи виходу після кожного (не порожнього) пункту випадку. Але це змінює те, як ми думаємо про вимикач! Тепер справи - це чергові гілки , як гілки в іншому випадку. Це означає, що ми очікуємо, що кожна галузь визначає власну сферу дії, подібно до клавіш if-clause або ітерації.

Отже, коротко: випадки мають однакову сферу застосування, оскільки це так, як у C. Але це здається дивним та непослідовним у C #, тому що ми вважаємо випадки альтернативними гілками, а не гот-цілями.


1
" Це здається дивним і непослідовним у C #, тому що ми вважаємо випадки альтернативними гілками, а не готовими цілями ". +1 для пояснення естетичних причин того, чому він почуває себе неправильно на C #.
pswg

4

Спрощений спосіб розгляду сфери - розглянути область застосування за блоками {}.

Оскільки switchне містить блоків, він не може мати різні сфери застосування.


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заяву.
Гуванте
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.