Ограничения оператора C # - почему?

При написании оператора switch, как представляется, есть два ограничения на то, что вы можете включить в операторах case.

Например (и да, я знаю, если вы делаете такие вещи, это, вероятно, означает, что ваша объектно-ориентированная (ОО) архитектура ненадежна - это просто надуманный пример!),

  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 () завершается с «ожидаемым значением целочисленного типа», а операторы case с «ожидаемым постоянным значением».

Почему эти ограничения существуют, и каково основное обоснование? Я не вижу причин, по которым оператор switch должен уступать только статическому анализу и почему включаемое значение должно быть целым (то есть примитивным). Какое оправдание?

4.09.2008 22:34:47
Michael Freidgeim 30.08.2017 11:27:04
Другим вариантом для включения встроенных типов является использование перечисления TypeCode .
Erik Philips 15.11.2019 23:25:27
17 ОТВЕТОВ
РЕШЕНИЕ

Это мой оригинальный пост, что вызвало некоторые споры ... потому что это не так :

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


Фактически оператор C # switch не всегда является ветвью с постоянным временем.

В некоторых случаях компилятор будет использовать оператор переключения CIL, который действительно является ветвью с постоянным временем, используя таблицу переходов. Однако в редких случаях, как указал Иван Гамильтон, компилятор может генерировать что-то еще полностью.

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

97
23.05.2017 11:46:52
Как отмечено в других ответах (включая мой), утверждения, сделанные в этом ответе, не верны. Я бы порекомендовал удалить (хотя бы для того, чтобы избежать применения этого (вероятно, распространенного) заблуждения).
mweerden 7.09.2008 00:49:12
Пожалуйста, смотрите мой пост ниже, где я убедительно показываю, что оператор switch выполняет ветвление с постоянным временем.
Brian Ensink 7.09.2008 02:38:14
Большое спасибо за ваш ответ, Брайан. Пожалуйста, смотрите ответ Ивана Гамильтона ((48259) [ beta.stackoverflow.com/questions/44905/#48259] ). Вкратце: вы говорите об switch инструкции (CIL), которая не совпадает с switchинструкцией C #.
mweerden 7.09.2008 09:59:02
Я не верю, что компилятор генерирует ветвление с постоянным временем при переключении строк.
Drew Noakes 3.12.2010 21:29:20
Это все еще применимо к сопоставлению с образцом в операторах регистра переключателя в C # 7.0?
B. Darren Olson 14.03.2018 18:11:18

Это не причина, но в разделе 8.7.2 спецификации C # говорится следующее:

Управляющий тип оператора switch устанавливается выражением switch. Если типом выражения-переключателя является sbyte, byte, short, ushort, int, uint, long, ulong, char, string или enum-type, то это является определяющим типом оператора switch. В противном случае должно существовать ровно одно пользовательское неявное преобразование (§6.4) из типа выражения switch в один из следующих возможных управляющих типов: 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
4.09.2008 22:54:32

Я полагаю, что нет фундаментальной причины, по которой компилятор не мог автоматически перевести ваш оператор switch в:

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

Но это мало что дает.

Оператор case для целочисленных типов позволяет компилятору выполнить ряд оптимизаций:

  1. Дублирования нет (если только вы не дублируете метки регистра, которые обнаруживает компилятор). В вашем примере t может соответствовать нескольким типам из-за наследования. Первый матч должен быть выполнен? Все они?

  2. Компилятор может выбрать реализацию оператора switch для целочисленного типа с помощью таблицы переходов, чтобы избежать всех сравнений. Если вы включаете перечисление, имеющее целочисленные значения от 0 до 100, то оно создает массив со 100 указателями в нем, по одному для каждого оператора switch. Во время выполнения он просто ищет адрес в массиве на основе целочисленного значения, которое включено. Это обеспечивает гораздо лучшую производительность во время выполнения, чем выполнение 100 сравнений.

0
4.09.2008 22:56:26
Важная сложность, которую следует здесь отметить, состоит в том, что модель памяти .NET имеет определенные строгие гарантии, которые делают ваш псевдокод не точно эквивалентным (гипотетическому, недопустимому C # ), switch (t) { case typeof(int): ... }поскольку ваш перевод подразумевает, что переменная t должна быть извлечена из памяти дважды if t != typeof(int), тогда как последняя будет (предположительно) всегда читайте значение t ровно один раз . Эта разница может нарушить правильность параллельного кода, который опирается на эти отличные гарантии. Для получения дополнительной информации об этом, смотрите Параллельное программирование
Glenn Slayden 30.08.2017 18:39:49

Согласно документации оператора switch, если существует однозначный способ неявного преобразования объекта в целочисленный тип, то это будет разрешено. Я думаю, что вы ожидаете поведения, при котором для каждого оператора case оно будет заменено if (t == typeof(int)), но это откроет целую банку червей, когда вы перегрузите этот оператор. Поведение изменилось бы, если бы изменились детали реализации оператора switch, если вы неправильно написали == переопределение. Сокращая сравнения до целочисленных типов и строк и тех вещей, которые могут быть сведены к целочисленным типам (и предназначены для них), они избегают потенциальных проблем.

0
4.09.2008 23:01:59

В то же время, по словам Джеффа Этвуда, утверждение о переключателе является злодеянием программирования . Используйте их экономно.

Вы часто можете выполнить ту же задачу, используя таблицу. Например:

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]);
6
4.09.2008 23:14:15
Вы серьезно цитируете чью-то непристойную публикацию в Твиттере без доказательств? По крайней мере, ссылка на надежный источник.
Ivan Hamilton 22.05.2009 07:39:54
Это из надежного источника; пост в Твиттере от Джеффа Этвуда, автора просматриваемого вами сайта. :-) У Джеффа есть несколько постов в блогах на эту тему, если вам интересно.
Judah Gabriel Himango 22.05.2009 18:47:24
Я полагаю, что это полная BS - независимо от того, написал ли это Джефф Этвуд. Забавно, насколько хорошо оператор switch пригоден для обработки конечных автоматов и других примеров изменения потока кода в зависимости от значения enumтипа. Также не случайно, что intellisense автоматически заполняет оператор switch, когда вы включаете переменную enumтипа.
Jonathon Reinhart 21.01.2012 04:51:24
@JonathonReinhart Да, я думаю, в этом суть - есть лучшие способы для обработки полиморфного кода, чем с помощью switchоператора. Он не говорит, что вы не должны писать конечные автоматы, просто вы можете сделать то же самое, используя хорошие специфические типы. Конечно, это намного проще в таких языках, как F #, которые имеют типы, которые могут легко охватывать довольно сложные состояния. Для вашего примера вы могли бы использовать различимые объединения, где состояние становится частью типа, и заменить на switchсопоставление с образцом. Или используйте интерфейсы, например.
Luaan 13.04.2015 11:21:41
Старый ответ / вопрос, но я бы подумал, что (поправьте меня, если я ошибаюсь) Dictionaryбыло бы значительно медленнее, чем оптимизированное switchутверждение ...?
Paul 25.09.2018 14:55:47

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

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

Я могу, однако, представить себе , что можно было бы хотеть , чтобы исключить типы из - за причин , таких , как один дается lomaxx ( 44918 ).

Редактировать: @Henk ( 44970 ): Если строки максимально используются совместно, строки с одинаковым содержимым также будут указателями на ту же область памяти. Затем, если вы можете убедиться, что строки, используемые в случаях, хранятся последовательно в памяти, вы можете очень эффективно реализовать переключатель (т. Е. С выполнением порядка 2 сравнений, сложения и двух переходов).

0
23.05.2017 11:46:52

писал:

«Оператор switch выполняет ветвление с постоянным временем независимо от того, сколько у вас дел».

Поскольку язык допускает использование строкового типа в операторе switch, я предполагаю, что компилятор не может сгенерировать код для реализации ветви с постоянным временем для этого типа и должен генерировать стиль if-then.

@ Mweerden - Ах, я вижу. Спасибо.

У меня нет большого опыта в C # и .NET, но, похоже, разработчики языка не разрешают статический доступ к системе типов, за исключением узких обстоятельств. TypeOf ключевое слово возвращает объект , так это доступно только на время выполнения.

0
5.09.2008 02:48:14

Я думаю, что Хенк прибил это с "отсутствием статического доступа к системе типов"

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

0
5.09.2008 03:30:30

Я согласен с этим комментарием, что использование подхода, основанного на таблицах, часто лучше.

В C # 1.0 это было невозможно, поскольку в нем не было обобщений и анонимных делегатов. Новые версии C # имеют строительные леса, чтобы сделать эту работу. Наличие нотации для литералов объекта также помогает.

0
23.05.2017 12:10:02

Главным образом, эти ограничения существуют из-за языковых дизайнеров. Основным оправданием может быть совместимость с историей языка, идеалами или упрощение дизайна компилятора.

Компилятор может (и делает) выбрать:

  • создать большой оператор 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 для типа (ничего общего с его супертипами или интерфейсами). Проверка совместимости объекта с заданным типом во время выполнения - это работа оператора "is". Использование «typeof» здесь для выражения объекта не имеет значения.

10
23.05.2017 12:34:14

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

10
5.09.2008 11:49:40
Select CaseАн VB является очень гибким и супер экономит время. Я очень скучаю по этому.
Eduardo Molteni 31.03.2012 22:01:17
@EduardoMolteni Тогда переключитесь на F #. По сравнению с этим переключатели Паскаля и В.Б. кажутся идиотскими детьми.
Luaan 13.04.2015 11:17:46

Первая причина, которая приходит на ум, является исторической :

Поскольку большинство программистов на C, C ++ и Java не привыкли к таким свободам, они не требуют их.

Другая, более веская причина заключается в том, что языковая сложность возрастет :

Прежде всего, объекты должны сравниваться .Equals()или с ==оператором? Оба действительны в некоторых случаях. Должны ли мы ввести новый синтаксис для этого? Должны ли мы позволить программисту ввести свой собственный метод сравнения?

Кроме того, разрешение на включение объектов нарушит основные предположения относительно оператора switch . Существует два правила, управляющих оператором 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 # сделал переключение через провал незаконным, заключается в том, что операторы switch могут быть произвольно переставлены.

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

23
5.09.2008 13:27:18
Обратите внимание на переключение порядка. Падение законно, если дело не содержит кода. Например, Случай 1: Случай 2: Console.WriteLine («Привет»); ломать;
Joel McBeth 5.10.2011 19:46:37

Важно не путать оператор переключения 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 Инструкция switch» не упоминает «постоянное время» или что базовая реализация даже использует инструкцию переключения CIL (будьте очень осторожны, предполагая, что такие вещи).

В конце концов, переключение C # против целочисленного выражения в современной системе - это операция за микросекунду, о которой обычно не стоит беспокоиться.


Конечно, эти времена будут зависеть от машин и условий. Я бы не стал обращать внимание на эти временные тесты: длительность микросекунды, о которой мы говорим, затмевается любым «реальным» выполняемым кодом (и вы должны включить некоторый «реальный код», иначе компилятор оптимизирует ветку), или джиттер в системе. Мои ответы основаны на использовании IL DASM для проверки CIL, созданного компилятором C #. Конечно, это не является окончательным, поскольку фактические инструкции, которые выполняет ЦП, затем создаются JIT.

Я проверил окончательные инструкции процессора, фактически выполненные на моем компьютере с архитектурой x86, и могу подтвердить, что простой смежный переключатель set выполняет что-то вроде:

  jmp     ds:300025F0[eax*4]

Где поиск в двоичном дереве полон:

  cmp     ebx, 79Eh
  jg      3000352B
  cmp     ebx, 654h
  jg      300032BB
  
  cmp     ebx, 0F82h
  jz      30005EEE
113
13.08.2014 00:50:44
Результаты ваших экспериментов меня немного удивляют. Ты поменял свой с Брайаном? Его результаты показывают увеличение с размером, а ваши нет. Я что-то упустил? В любом случае спасибо за четкий ответ.
mweerden 7.09.2008 10:13:03
С такой маленькой операцией сложно точно рассчитать время. Мы не передавали ни код, ни процедуры тестирования. Я не понимаю, почему его время должно увеличиться для соседних случаев. Мои были в 10 раз быстрее, поэтому среда и тестовый код могут сильно различаться.
Ivan Hamilton 7.09.2008 16:33:10

Я не вижу причин, по которым оператор switch должен следовать только статическому анализу

Правда, это не обязательно , и многие языки фактически используют операторы динамического переключения. Это означает, однако, что переупорядочение предложений case может изменить поведение кода.

Здесь есть некоторая интересная информация, лежащая в основе проектных решений, которая вошла в «switch»: почему оператор C # switch разработан так, чтобы не допустить провала, но все же требует перерыва?

Разрешение динамических выражений регистра может привести к чудовищностям, таким как этот код PHP:

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

который, честно говоря, должен просто использовать if-elseутверждение.

6
4.06.2009 20:40:15
Это то, что я люблю в PHP (теперь, когда я перехожу на C #), это свобода. С этим приходит свобода писать плохой код, но это то, чего мне очень не хватает в C #
silkfire 25.09.2015 11:21:01

Ответ Иуды выше дал мне идею. Вы можете "подделать" описанное выше поведение OP, используя 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 используется ключ вместо таблицы переходов в стиле переключателя.

3
7.03.2010 16:27:38

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));
}
5
24.04.2017 07:44:50

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

Вы можете прочитать больше о новой функции здесь .

0
1.02.2020 09:18:33