Обмеження заяви # перемикача C # - чому?


140

Під час написання заяви про перемикання, як видається, існує два обмеження щодо того, що можна ввімкнути у випадках заяви.

Наприклад (і так, я знаю, якщо ви робите подібні речі, це, ймовірно, означає, що ваша об'єктно-орієнтована (OO) архітектура iffy - це лише надуманий приклад!),

  Type t = typeof(int);

  switch (t) {

    case typeof(int):
      Console.WriteLine("int!");
      break;

    case typeof(string):
      Console.WriteLine("string!");
      break;

    default:
      Console.WriteLine("unknown!");
      break;
  }

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

Чому ці обмеження діють і що є основним виправданням? Я не бачу ніяких причин , чому заяву перемикач має піддатися тільки статичного аналізу, і тому значення включення живлення повинен бути вбудований (тобто, примітивний). Яке виправдання?



Ще одним варіантом перемикання вбудованих типів є використання TypeCode Enum .
Ерік Філіпс

Просто створіть ENUM та використовуйте NameOf у регістрі Switch. Він буде працювати як постійний на динамічній змінній.
Вайбхав. Натхненний

Відповіді:


98

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

Оператор switch - це не те саме, що велике твердження if-else. Кожен випадок повинен бути унікальним і оцінюватися статично. Оператор перемикання має постійну гілку часу незалежно від кількості випадків. Оператор if-else оцінює кожну умову, поки не знайде ту, що відповідає дійсності.


Насправді оператор перемикання C # не завжди є постійною гілкою часу.

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

Це насправді досить просто перевірити, написавши різні заяви C # про перемикання, деякі рідкісні, деякі щільні та переглянувши отриманий CIL за допомогою інструменту ildasm.exe.


4
Як зазначено в інших відповідях (включаючи мою), твердження, висловлені в цій відповіді, не є правильними. Я рекомендував би видалити (хоч би щоб уникнути примусового виконання цього (мабуть, поширеного) оману).
mweerden

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

Дуже дякую за вашу відповідь, Брайан. Дивіться відповідь Івана Гамільтона ((48259) [ beta.stackoverflow.com/questions/44905/#48259] ). Коротше кажучи: ви говорите про switch інструкцію (CIL), яка не збігається із switchзаявою C #.
mweerden

Я не вірю, що компілятор генерує розгалуження постійного часу і при включенні рядків.
Дрю Ноакс

Чи це все ще застосовується для відповідності шаблонів у виписках з переключеннями у C # 7.0?
Б. Даррен Олсон

114

Важливо не плутати оператор перемикання C # з інструкцією перемикача CIL.

Перемикач CIL - це таблиця стрибків, для якої потрібен індекс у наборі адрес стрибків.

Це корисно лише в тому випадку, якщо корпуси перемикача C # суміжні:

case 3: blah; break;
case 4: blah; break;
case 5: blah; break;

Але мало користі, якщо їх немає:

case 10: blah; break;
case 200: blah; break;
case 3000: blah; break;

(Вам знадобиться таблиця розміром до 3000 записів, використовуються лише 3 слоти)

За допомогою суміжних виразів компілятор може почати виконувати лінійні перевірки if-else-if-else.

З більшими суміжними наборами виразів компілятор може починатись з пошуку бінарного дерева та, нарешті, останніх кількох елементів if-else-if-else.

З наборами виразів, що містять скупчення суміжних елементів, компілятор може виконати двійковий пошук по дереву і, нарешті, перемикач CIL.

Це повне "mays" і "mights", і це залежить від компілятора (може відрізнятися від Mono або Rotor).

Я тиражував ваші результати на моїй машині, використовуючи сусідні корпуси:

загальний час для виконання 10-ти напрямного вимикача, 10000 ітерацій (мс): 25.1383
приблизний час на 10-ти перехідний перемикач (мс): 0.00251383

загальний час виконання 50-ти маршрутного комутатора, 10000 ітерацій (мс): 26.593
приблизний час на 50-ти перехідних комутаторів (мс): 0,0026593

загальний час для виконання 5000-ти перемикачів, 10000 ітерацій (мс): 23.7094
приблизний час на 5000 перемикачів (мс): 0,00237094

загальний час для виконання перемикача 50000, 10000 ітерацій (мс): 20.0933
приблизний час на перемикач 50000 (мс): 0.00200933

Тоді я також використовував не суміжні вирази регістру:

загальний час для виконання 10-ти маршрутного комутатора, 10000 ітерацій (мс): 19.6189
приблизний час на 10-ти перемикачів (мс): 0.00196189

загальний час виконання 500-ти маршрутного вимикача, 10000 ітерацій (мс): 19.1664
приблизний час на 500 перемикачів (мс): 0.00191664

загальний час для виконання 5000-ти перемикачів, 10000 ітерацій (мс): 19.5871
приблизний час на 5000 перемикачів (мс): 0.00195871

Не суміжна заява про вимикач 50 000 справ не збирається.
"Вираз занадто довгий або складний для компіляції біля" ConsoleApplication1.Program.Main (string []) "

Тут смішно, що пошук у двійковому дереві виявляється трохи (можливо, не статистично) швидше, ніж інструкція про перемикання CIL.

Брайане, ти вжив слово " константа ", яке має дуже певне значення з точки зору теорії складності обчислювальної техніки. Хоча спрощений суміжний цілий приклад може створювати CIL, який вважається O (1) (константа), розрідженим прикладом є O (log n) (логарифмічний), приклади в кластері лежать десь посередині, а малі приклади - O (n) (лінійний ).

Це навіть не стосується ситуації String, в якій Generic.Dictionary<string,int32>може бути створена статика , і при першому використанні вона зазнає певних накладних витрат. Продуктивність тут буде залежати від продуктивності Generic.Dictionary.

Якщо ви перевірте специфікацію мови C # (не специфікацію CIL), ви знайдете "15.7.2 оператор перемикача" не згадує про "постійний час" або що в базовій реалізації навіть використовується інструкція з перемикання CIL (будьте дуже уважні до припущення такі речі).

Зрештою, перемикач C # на цілий вираз у сучасній системі - це субмікросекундна операція, і зазвичай не варто турбуватися.


Звичайно, ці часи залежатимуть від машин та умов. Я б не звертав уваги на ці випробування на хронометраж, тривалість мікросекунди, про яку ми говоримо, деформується будь-яким "реальним" кодом, який виконується (і ви повинні включити якийсь "реальний код", інакше компілятор оптимізує гілку), або тремтіння в системі. Мої відповіді засновані на використанні IL DASM для вивчення CIL, створеного компілятором C #. Звичайно, це не є остаточним, оскільки фактичні інструкції, які виконує ЦП, потім створюються JIT.

Я перевірив остаточні інструкції процесора, фактично виконані на моїй машині x86, і можу підтвердити простий перемикач суміжного набору, виконуючи щось на кшталт:

  jmp     ds:300025F0[eax*4]

Там, де рядовий пошук у бінарному дереві:

  cmp     ebx, 79Eh
  jg      3000352B
  cmp     ebx, 654h
  jg      300032BB
  
  cmp     ebx, 0F82h
  jz      30005EEE

Результати ваших експериментів мене трохи дивують. Ти поміняв свою на Брайана? Його результати показують збільшення розміру, а ваші - ні. Я щось пропускаю? У будь-якому випадку, дякую за чітку відповідь.
mweerden

Точно обчислити терміни при такій невеликій операції важко. Ми не ділилися кодом чи процедурами тестування. Я не бачу, чому його час повинен збільшитися для суміжних справ. Шахта була в 10 разів швидше, тому середовища та тестовий код можуть сильно відрізнятися.
Іван Гамільтон

23

Перша причина, яка спадає на думку, - це історична :

Оскільки більшість програмістів на C, C ++ та Java не звикли до таких свобод, вони не вимагають їх.

Ще одна, більш вагома причина - це складність мови :

Перш за все, чи слід порівнювати об'єкти .Equals()з ==оператором чи з ним ? І те й інше дійсне в деяких випадках. Чи варто вводити для цього новий синтаксис? Чи варто дозволити програмісту ввести власний метод порівняння?

Крім того, дозволяючи вмикати об'єкти, було б порушено основні припущення щодо оператора switch . Є два правила, що регулюють оператор перемикання, який компілятору не вдасться застосувати, якщо було дозволено включити об'єкти (див. Специфікацію мови C # версії 3.0 , § 8.7.2):

  • Щоб значення міток перемикачів були постійними
  • Щоб значення міток комутатора були чіткими (так що для заданого виразу перемикача можна вибрати лише один блок комутації)

Розглянемо цей приклад коду в гіпотетичному випадку, що дозволені нестабільні значення випадку:

void DoIt()
{
    String foo = "bar";
    Switch(foo, foo);
}

void Switch(String val1, String val2)
{
    switch ("bar")
    {
        // The compiler will not know that val1 and val2 are not distinct
        case val1:
            // Is this case block selected?
            break;
        case val2:
            // Or this one?
            break;
        case "bar":
            // Or perhaps this one?
            break;
    }
}

Що робитиме код? Що робити, якщо заяви справи упорядковані? Дійсно, одна з причин, через яку C # зробила незаконне перемикання, полягає в тому, що оператори перемикання можуть бути довільно переставлені.

Ці правила існують з причини - щоб програміст, переглядаючи один блок справ, точно знав точну умову, під яким блок вводиться. Коли вищезазначене твердження перемикача виростає в 100 або більше рядків (і воно буде), такі знання є неоціненними.


2
Зверніть увагу на упорядкування перемикача. Провал є законним, якщо справа не містить коду. Наприклад, Case 1: Case 2: Console.WriteLine ("Привіт"); перерва;
Joel McBeth

10

До речі, VB, маючи однакову базову архітектуру, дозволяє набагато гнучкіші Select Caseоператори (вищезазначений код працював би в VB) і все ще створює ефективний код, де це можливо, тому аргумент через технічні обмеження потрібно ретельно розглядати.


1
Select CaseАн VB є дуже гнучким і супер економить час. Я цього дуже сумую.
Едуардо Молтені

@EduardoMolteni Переключиться на F # тоді. Це робить вимикачі Паскаля та ВБ у порівнянні з ідіотськими дітьми.
Луаан

10

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

Компілятор може (і робить) вирішити:

  • створити велику операцію if if else
  • використовувати інструкцію перемикача MSIL (таблиця стрибків)
  • побудуйте Generic.Dictionary <string, int32>, заповніть його при першому використанні та зателефонуйте Generic.Dictionary <> :: TryGetValue (), щоб індекс перейшов до інструкції перемикача MSIL (таблиця стрибків)
  • використовувати комбінацію стрибків if-elses & MSIL "switch"

Оператор перемикання НЕ є постійною гілкою часу. Компілятор може знайти скорочення (використовуючи хеш-відра тощо), але більш складні випадки генерують складніший код MSIL, деякі випадки розгалужуються раніше, ніж інші.

Для обробки випадку String компілятор закінчується (в якийсь момент), використовуючи a.Equals (b) (і, можливо, a.GetHashCode ()). Я думаю, що компілятору буде доцільно використовувати будь-який об'єкт, який задовольняє цим обмеженням.

Щодо потреби у статичних виразах регістру ... деякі з цих оптимізацій (хешування, кешування тощо) не були б доступні, якби вирази регістру не були детермінованими. Але ми вже бачили, що іноді компілятор просто вибирає спрощену дорогу if-else-if-else ...

Редагувати: lomaxx - Ваше розуміння оператора "typeof" невірне . Оператор "typeof" використовується для отримання об'єкта System.Type для типу (нічого спільного з його супертипами чи інтерфейсами). Перевірка сумісності об'єкта з заданим типом є роботою оператора "є". Використання тут "typeof" для вираження об'єкта не має значення.


6

На думку теми, за словами Джеффа Етвуда, заява про перемикання - це жорстокість програмування . Використовуйте їх економно.

Ви часто можете виконувати одне і те ж завдання за допомогою таблиці. Наприклад:

var table = new Dictionary<Type, string>()
{
   { typeof(int), "it's an int!" }
   { typeof(string), "it's a string!" }
};

Type someType = typeof(int);
Console.WriteLine(table[someType]);

7
Ви серйозно цитуєте когось із манжети в Twitter, не маючи доказів? Принаймні посилання на надійне джерело.
Іван Гамільтон

4
Це з надійного джерела; питання, про яке йдеться в Twitter, від Джеффа Етвуда, автора сайту, який ви переглядаєте. :-) Джефф має кілька публікацій блогів на цю тему, якщо вам цікаво.
Іуда Габріель Хіманго

Я вважаю, що це загальний BS - незалежно від того, написав це Джефф Етвуд чи ні. Смішно, наскільки добре оператор перемикання піддається обробці стаціонарних машин та інших прикладах зміни потоку коду на основі значення enumтипу. Також не випадково, що intellisense автоматично заповнює оператор перемикача при включенні змінної enumтипу.
Джонатан Райнхарт

@JonathonReinhart Так, я думаю, в цьому справа - є кращі способи обробки поліморфного коду, ніж використання switchоператора. Він не каже, що ви не повинні писати державні машини, а лише те, що ви можете зробити те саме, використовуючи приємні конкретні типи. Звичайно, це набагато простіше в мовах на зразок F #, які мають типи, які легко охоплюють досить складні стани. Для вашого прикладу ви можете використовувати дискриміновані об'єднання, коли стан стає частиною типу, і замінити відповідність switchшаблонів. Або використовувати інтерфейси, наприклад.
Луаан

Стара відповідь / питання, але я б подумав, що (виправте мене, якщо я помиляюся) Dictionaryбув би значно повільніше, ніж оптимізований switchвислів ...?
Пол,

6

Я не бачу жодної причини, через яку оператор переключення повинен піддаватися лише статичному аналізу

Щоправда, цього не потрібно , і багато мов насправді використовують оператори динамічного перемикання. Однак це означає, що переупорядкування пунктів "case" може змінити поведінку коду.

За дизайнерськими рішеннями, які перейшли у "перемикач", є якась цікава інформація: Чому оператор перемикача C # призначений не допускати пропускання, але все ж вимагає перерви?

Дозволити динамічні вирази регістру можуть призвести до жахливих ситуацій, таких як цей PHP-код:

switch (true) {
    case a == 5:
        ...
        break;
    case b == 10:
        ...
        break;
}

який відверто повинен просто використовувати if-elseвислів.


1
Це те, що мені подобається в PHP (зараз, коли я переходжу на C #), це свобода. З цим з’являється свобода писати поганий код, але це те, що мені дуже не вистачає в C #
silkfire

5

Microsoft нарешті вас почула!

Тепер із C # 7 ви можете:

switch(shape)
{
case Circle c:
    WriteLine($"circle with radius {c.Radius}");
    break;
case Rectangle s when (s.Length == s.Height):
    WriteLine($"{s.Length} x {s.Height} square");
    break;
case Rectangle r:
    WriteLine($"{r.Length} x {r.Height} rectangle");
    break;
default:
    WriteLine("<unknown shape>");
    break;
case null:
    throw new ArgumentNullException(nameof(shape));
}

3

Це не є причиною, але в розділі 8.7.2 специфікації C # зазначено наступне:

Управляючий тип оператора перемикання встановлюється виразом перемикача. Якщо тип виразу перемикача - sbyte, byte, short, ushort, int, uint, long, ulong, char, string або enum-type, то таким є керуючий тип оператора switch. В іншому випадку точно одна визначена користувачем неявна конверсія (§6.4) повинна існувати від типу виразу перемикання до одного з наступних можливих типів керування: sbyte, byte, short, ushort, int, uint, long, ulong, char, string . Якщо такої неявної конверсії немає або якщо існує більше одного такого неявного перетворення, виникає помилка часу компіляції.

Специфікація C # 3.0 розміщена за адресою: http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc


3

Відповідь Юди вище дала мені думку. Ви можете "підробити" поведінку перемикача ОП вище, використовуючи Dictionary<Type, Func<T>:

Dictionary<Type, Func<object, string,  string>> typeTable = new Dictionary<Type, Func<object, string, string>>();
typeTable.Add(typeof(int), (o, s) =>
                    {
                        return string.Format("{0}: {1}", s, o.ToString());
                    });

Це дозволяє асоціювати поведінку з типом у тому ж стилі, що і оператор switch. Я вважаю, що це має додаткову перевагу, коли будете клавіш замість таблиці перемикання у стилі перемикання при компіляції в IL.


0

Я припускаю, що немає жодної принципової причини, чому компілятор не міг автоматично перекласти ваш оператор переключення на:

if (t == typeof(int))
{
...
}
elseif (t == typeof(string))
{
...
}
...

Але від цього не багато чого здобуто.

Випадок справи про цілісні типи дозволяє компілятору зробити ряд оптимізацій:

  1. Не існує дублювання (якщо ви не дублюєте мітки регістру, які виявляє компілятор). У вашому прикладі t може відповідати декільком типам через спадщину. Чи повинен бути виконаний перший матч? Усі?

  2. Компілятор може вибрати реалізацію оператора перемикання над цілісним типом таблицею стрибків, щоб уникнути всіх порівнянь. Якщо ви перемикаєте перерахунок, який має цілі значення від 0 до 100, то він створює масив зі 100 вказівниками, по одному для кожного оператора перемикача. Під час виконання він просто шукає адресу з масиву на основі цілого значення, яке вмикається. Це робить набагато кращими показники виконання, ніж 100 порівнянь.


1
Важливу складність, яку слід зазначити тут, є те, що модель пам'яті .NET має певні гарантії, які роблять ваш псевдокод не точно еквівалентним (гіпотетичний, недійсний C # ), switch (t) { case typeof(int): ... }оскільки ваш переклад означає, що змінна t повинна вийматися із пам'яті двічі, якщо t != typeof(int), тоді як остання буде (імовірно) завжди читайте значення t рівно один раз . Ця різниця може порушити правильність паралельного коду, який спирається на ці чудові гарантії. Більш детальну інформацію про це дивіться у програмі Joe Duffy за одночасним програмуванням у Windows
Glenn Slayden

0

Відповідно до документації твердження перемикача, якщо існує однозначний спосіб неявного перетворення об’єкта в цілісний тип, тоді це буде дозволено. Я думаю, ви очікуєте поведінки, коли для кожної заяви випадку його замінять if (t == typeof(int)), але це відкриє цілу банку глистів, коли ви перевантажите цього оператора. Поведінка буде змінюватися, коли деталі реалізації для заяви переключення змінилися, якщо ви неправильно написали == переосмислити. Скорочуючи порівняння до інтегральних типів та рядків та тих речей, які можна звести до цілісних типів (і призначених для цього), вони уникають потенційних проблем.


0

написав:

"Оператор перемикання має постійну гілку часу незалежно від того, скільки випадків у вас є."

Оскільки мова дозволяє використовувати тип рядка в операторі комутатора, я припускаю, що компілятор не може генерувати код для постійної реалізації гілки часу для цього типу і йому потрібно створити стиль if-then.

@mweerden - Ах, бачу. Дякую.

У мене немає великого досвіду роботи в C # і .NET, але, здається, мовні дизайнери не дозволяють статичний доступ до системи типів, за винятком вузьких обставин. TypeOf ключове слово повертає об'єкт , так це доступно тільки на час виконання.


0

Я думаю, що Хенк прибив це за допомогою "нестатичного доступу до системи типів"

Інший варіант полягає в тому, що немає порядку для типів, де можуть бути числові чи рядки. Таким чином, тип комутатора не може побудувати двійкове дерево пошуку, а лише лінійний пошук.


0

Я погоджуюся з цим коментарем, що використання підходу, керованого таблицею, часто краще.

У C # 1.0 це було неможливо, оскільки в ньому не було дженериків та анонімних делегатів. Нові версії C # мають ліси, щоб зробити цю роботу. Позначення об’єктних літералів також допомагає.


0

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

Строго кажучи, ви абсолютно праві, що немає підстав ставити ці обмеження на це. Можна підозрювати, що причина полягає в тому, що для дозволених випадків реалізація є дуже ефективною (як це запропонував Брайан Енсінк ( 44921 )), але я сумніваюся, що реалізація є дуже ефективною (wrt if-заяви), якщо я використовую цілі числа та деякі випадкові випадки (наприклад, 345, -4574 та 1234203). І в будь-якому випадку, яка шкода в тому, щоб дозволити це на все (або принаймні більше) і сказати, що це ефективно лише для конкретних випадків (таких як (майже) порядкові номери).

Я можу, однак, уявити, що можна захотіти виключати типи через такі причини, як, наприклад, той, який вказав lomaxx ( 44918 ).

Редагувати: @Henk ( 44970 ): Якщо рядки будуть максимально спільними, рядки з рівним вмістом будуть також вказівниками на те саме місце пам'яті. Потім, якщо ви можете переконатися, що рядки, які використовуються у випадках, послідовно зберігаються в пам'яті, ви можете дуже ефективно реалізувати комутатор (тобто з виконанням у порядку 2 порівняння, додавання та два стрибки).


0

C # 8 дозволяє вирішити цю проблему елегантно та компактно, використовуючи вираз перемикача:

public string GetTypeName(object obj)
{
    return obj switch
    {
        int i => "Int32",
        string s => "String",
        { } => "Unknown",
        _ => throw new ArgumentNullException(nameof(obj))
    };
}

Як результат, ви отримуєте:

Console.WriteLine(GetTypeName(obj: 1));           // Int32
Console.WriteLine(GetTypeName(obj: "string"));    // String
Console.WriteLine(GetTypeName(obj: 1.2));         // Unknown
Console.WriteLine(GetTypeName(obj: null));        // System.ArgumentNullException

Більше про нову функцію ви можете прочитати тут .

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