перемикач із дивною поведінкою var / null


91

Враховуючи такий код:

string someString = null;
switch (someString)
{
    case string s:
        Console.WriteLine("string s");
        break;
    case var o:
        Console.WriteLine("var o");
        break;
    default:
        Console.WriteLine("default");
        break;
}

Чому оператор switch відповідає case var o?

На моє розуміння, case string sце не збігається, коли s == null(ефективно) (null as string) != nullоцінює як хибне. IntelliSense на VS Code каже мені, що oце stringтеж. Будь-які думки?


Подібно до: регістр перемикача C # 7 з нульовими перевірками


9
Підтверджено. Я люблю це питання, особливо з наглядом , що oце string(підтверджується дженериків - тобто Foo(o)де Foo<T>(T template) => typeof(T).Name) - це дуже цікавий випадок , коли string xповодиться інакше , ніж var xнавіть тоді , коли xнабирається (компілятором) , якstring
Марк Gravell

7
Типовим випадком є ​​мертвий код. Повірте, ми повинні там виносити попередження. Перевірка.
JaredPar,

13
Мені дивно, що дизайнери C # вирішили varвзагалі дозволити в цьому контексті. Це, здається, схоже на те, що я знайшов би в C ++, а не мовою, яка передбачала б ввести програміста "в яму успіху". Тут varоднозначне і марне - речі, яких дизайн C #, як правило, прагне уникати.
Пітер Дуніхо,

1
@PeterDuniho Я б не сказав, що марний; вхідний вираз до switchможе бути невимовним - анонімні типи тощо; і це не є двозначним - компілятор чітко знає тип; це просто бентежить (принаймні для мене), що nullправила настільки різні!
Марк Гравелл

1
@PeterDuniho цікавий факт - колись ми шукали певні формальні правила призначення із специфікації C # 1.2, і ілюстративний код розширення мав оголошення змінної всередині блоку (там, де воно зараз); він перемістився назовні лише через 2.0, а потім знову всередину, коли проблема захоплення була очевидною.
Марк Гравелл

Відповіді:


69

Усередині switchоператора збігу шаблонів, що використовує caseдля явного типу, запитується, чи відповідає значення саме тому типу або похідному типу. Це точний еквівалентis

switch (someString) {
  case string s:
}
if (someString is string) 

Значення nullне має типу і, отже, не задовольняє жодній із зазначених вище умов. Статичний тип someStringне вступає в дію в жодному з прикладів.

varТипу , хоча в шаблоні відповідність діє як джокер і буде відповідати будь-якому значенню , включаючи null.

defaultСправа тут мертвий код. Значення case var oбуде відповідати будь-якому значенню, нульовому чи ненульовому. Справа, що не відповідає замовчуванню, завжди виграє над типовою, отже default, ніколи не буде вражена. Якщо ви подивитесь на ІЛ, то побачите, що він навіть не виділяється.

На перший погляд може здатися дивним, що це компілюється без будь-якого попередження (безумовно, мене відкинуло). Але це відповідає поведінці C #, яка повертається до 1.0. Компілятор допускає defaultвипадки, навіть коли він банально може довести, що його ніколи не вдарять. Розглянемо як приклад наступне:

bool b = ...;
switch (b) {
  case true: ...
  case false: ...
  default: ...
}

Тут defaultніколи не потрапить (навіть для того, boolщо має значення, яке не 1 або 0). Однак C # дозволив це з 1.0 без попередження. Збіг шаблонів просто відповідає цій поведінці тут.


4
Справжня проблема полягає в тому, що компілятор "показує" varтип типу, stringколи насправді це не так (чесно кажучи, не впевнений, який тип слід визнати)
shmuelie

@shmuelie розраховано на тип varу прикладі string.
JaredPar,

5
@JaredPar дякую за ідею тут; особисто я б підтримав більше попереджень, навіть коли раніше цього не робив, але я розумію обмеження мовної команди. Ви коли-небудь замислювались про "суєту щодо всього" (можливо, увімкнену за замовчуванням) проти "застарілого стоїчного режиму" (на вибір)? можливоcsc /stiffUpperLip
Марк Гравелл

3
@MarcGravell у нас є функція, яка називається попереджувальними хвилями, яка покликана полегшити, зменшити масштабність і ввести нові попередження. По суті, кожен випуск компілятора - це нова хвиля, і ви можете вибрати попередження за допомогою / wave: 1, / wave: 2, / wave: all.
JaredPar,

4
@JonathanDickinson Я не думаю, що це показує те, що ви думаєте. Це просто показує, що a nullє дійсним stringпосиланням, і будь-яке stringпосилання (включаючи null) може неявно передаватися (зберігаючи посилання) на objectпосилання, і будь-яке objectпосилання, яке є, nullможе бути успішно оновлене (явне) до будь-якого іншого типу, що все ще є null. Не зовсім те саме з точки зору системи типу компілятора.
Марк Гравелл

22

Я збираю тут кілька коментарів у твіттері - це насправді нове для мене, і я сподіваюся, що jaredpar прийде з більш вичерпною відповіддю, але; коротка версія, як я розумію:

case string s:

інтерпретується як if(someString is string) { s = (string)someString; ...або if((s = (someString as string)) != null) { ... }- будь-який з яких включає nullтест - який у вашому випадку не вдався; навпаки:

case var o:

де компілятор вирішує oтак, stringяк просто o = (string)someString; ...- жодного nullтесту, незважаючи на те, що він виглядає схожим на поверхні, лише із компілятором, який надає тип.

нарешті:

default:

тут неможливо дістатись , тому що наведена справа вловлює все. Це може бути помилка компілятора, оскільки вона не видавала попередження про недосяжний код.

Я погоджуюсь, що це дуже тонко, тонко і заплутано. Але, очевидно, case var oсценарій використовує використання з нульовим розповсюдженням ( o?.Length ?? 0і т.д.). Я згоден , що це дивно , що це працює так дуже по- різному між var oі string s, але це те , що в даний час робить компілятор.


14

Це тому, що case <Type>збіги за типом динамічний (час виконання), а не за статичним (час компіляції). nullне має динамічного типу, тому не може збігатися з string. varце просто запасний варіант.

(Допис, тому що я люблю короткі відповіді.)

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