При написании оператора switch есть два ограничения на то, что вы можете включить в операторах case.
Например (и да, я знаю, что если вы делаете такие вещи, это, вероятно, означает, что ваша объектно-ориентированная (OO) архитектура ненадежна - это просто надуманный пример!),
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 должен поддаваться только статическому анализу и почему включаемое значение должно быть целым (то есть примитивным). Какое оправдание?
Ответы:
Это мой оригинальный пост, который вызвал некоторые дискуссии ... потому что он неверен :
Фактически, оператор switch в C # не всегда является ветвью с постоянным временем.
В некоторых случаях компилятор будет использовать оператор переключения CIL, который действительно является ветвью с постоянным временем с использованием таблицы переходов. Однако в редких случаях, как указал Иван Гамильтон, компилятор может генерировать что-то совершенно другое.
На самом деле это довольно легко проверить, написав различные операторы переключения C #, некоторые разреженные, некоторые плотные, и посмотрев на полученный CIL с помощью инструмента ildasm.exe.
источник
switch
инструкции (CIL), которая не совпадает сswitch
инструкцией C #.Важно не путать оператор переключения 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.
Это полно "майя" и "майя", и это зависит от компилятора (может отличаться от Mono или Rotor).
Я воспроизвел ваши результаты на своей машине, используя соседние случаи:
Затем я также использовал несмежные выражения:
Что забавно, так это то, что поиск в двоичном дереве выглядит немного (вероятно, не статистически) быстрее, чем инструкция переключения 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
источник
Первая причина, которая приходит на ум, - историческая :
Поскольку большинство программистов на языках C, C ++ и Java не привыкли иметь такие свободы, они не требуют их.
Другая, более веская причина состоит в том, что сложность языка увеличится :
В первую очередь следует сравнивать объекты
.Equals()
с==
оператором или с ним ? Оба действительны в некоторых случаях. Должны ли мы для этого ввести новый синтаксис? Должны ли мы позволить программисту вводить свой собственный метод сравнения?Кроме того, разрешение включать объекты нарушит основные предположения об операторе switch . Есть два правила, управляющих оператором switch, которые компилятор не сможет применить, если бы объекты были разрешены для включения (см. Спецификацию языка C # версии 3.0 , §8.7.2):
Рассмотрим этот пример кода в гипотетическом случае, когда разрешены непостоянные значения case:
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; } }
Что будет делать код? Что, если операторы case переупорядочены? В самом деле, одна из причин, по которой C # сделал провал переключения незаконным, заключается в том, что операторы переключения могут быть произвольно переставлены.
Эти правила действуют по определенной причине - так, чтобы программист мог, глядя на один блок case, точно знать точное условие, при котором блок вводится. Когда вышеупомянутый оператор switch вырастет до 100 или более строк (а так и будет), такие знания станут бесценными.
источник
Между прочим, VB, имеющий ту же базовую архитектуру, допускает гораздо более гибкие
Select Case
операторы (приведенный выше код будет работать в VB) и по-прежнему создает эффективный код там, где это возможно, поэтому аргумент, связанный с техническими ограничениями, следует тщательно учитывать.источник
Select Case
Ан VB является очень гибким и супер экономит время. Я очень по нему скучаю.В основном эти ограничения действуют из-за дизайнеров языков. Основным оправданием может быть совместимость с историей языков, идеалами или упрощением конструкции компилятора.
Компилятор может (и делает) следующее:
Оператор switch НЕ ЯВЛЯЕТСЯ ветвью постоянного времени. Компилятор может находить короткие пути (с использованием хэш-корзин и т. Д.), Но более сложные случаи будут генерировать более сложный код MSIL, причем некоторые случаи будут ветвиться раньше, чем другие.
Для обработки случая String компилятор в конечном итоге (в какой-то момент) использует a.Equals (b) (и, возможно, a.GetHashCode ()). Я думаю, что компилятору было бы тривиально использовать любой объект, удовлетворяющий этим ограничениям.
Что касается потребности в статических выражениях case ... некоторые из этих оптимизаций (хеширование, кэширование и т.д.) были бы недоступны, если бы выражения case не были детерминированными. Но мы уже видели, что иногда компилятор в любом случае просто выбирает упрощенный путь if-else-if-else ...
Изменить: lomaxx - Ваше понимание оператора "typeof" неверно. Оператор typeof используется для получения объекта System.Type для типа (не имеющего отношения к его супертипам или интерфейсам). Проверка совместимости во время выполнения объекта с заданным типом - задача оператора is. Использование «typeof» здесь для выражения объекта не имеет значения.
источник
Говоря об этой теме, по словам Джеффа Этвуда, оператор switch является злодеянием программирования . Используйте их экономно.
Часто ту же задачу можно выполнить с помощью таблицы. Например:
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]);
источник
enum
типа. Также не случайно intellisense автоматически заполняет оператор switch, когда вы включаете переменную определенногоenum
типа.switch
оператора. Он не говорит, что вам не следует писать конечные автоматы, просто вы можете сделать то же самое, используя хорошие конкретные типы. Конечно, это намного проще в таких языках, как F #, у которых есть типы, которые могут легко охватывать довольно сложные состояния. В вашем примере вы можете использовать размеченные объединения, где состояние становится частью типа, и заменитьswitch
сопоставление с образцом. Или используйте, например, интерфейсы.Dictionary
будет значительно медленнее, чем оптимизированныйswitch
оператор ...?Да, это не обязательно , и многие языки действительно используют операторы динамического переключения. Однако это означает, что переупорядочение предложений case может изменить поведение кода.
Здесь есть некоторая интересная информация, лежащая в основе дизайнерских решений, которые вошли в «switch»: почему оператор switch в C # спроектирован так, чтобы не допускать провалов, но все же требует перерыва?
Разрешение динамических выражений регистра может привести к чудовищам, таким как этот PHP-код:
switch (true) { case a == 5: ... break; case b == 10: ... break; }
который, честно говоря, должен просто использовать это
if-else
утверждение.источник
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)); }
источник
Это не причина, но в разделе 8.7.2 спецификации C # указано следующее:
Спецификация C # 3.0 находится по адресу: http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc
источник
Ответ Иуды выше дал мне идею. Вы можете «подделать» поведение переключателя 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.
источник
Я полагаю, что нет фундаментальной причины, по которой компилятор не мог автоматически преобразовать ваш оператор switch в:
if (t == typeof(int)) { ... } elseif (t == typeof(string)) { ... } ...
Но от этого мало что получается.
Оператор case для целочисленных типов позволяет компилятору выполнить ряд оптимизаций:
Нет дублирования (если вы не дублируете метки регистра, которые обнаруживает компилятор). В вашем примере t может соответствовать нескольким типам из-за наследования. Следует ли выполнить первый матч? Все они?
Компилятор может реализовать оператор switch над целым типом с помощью таблицы переходов, чтобы избежать всех сравнений. Если вы включаете перечисление с целочисленными значениями от 0 до 100, оно создает массив со 100 указателями в нем, по одному для каждого оператора switch. Во время выполнения он просто ищет адрес в массиве на основе включенного целочисленного значения. Это обеспечивает гораздо лучшую производительность во время выполнения, чем выполнение 100 сравнений.
источник
switch (t) { case typeof(int): ... }
поскольку ваш перевод подразумевает, что переменнаяt
должна быть извлечена из памяти дважды, еслиt != typeof(int)
, тогда как последний будет (предположительно) всегда считывайте значениеt
ровно один раз . Это различие может нарушить правильность параллельного кода, который полагается на эти отличные гарантии. Для получения дополнительной информации см. Джо Даффи Параллельное программирование WindowsСогласно документации оператора switch, если существует однозначный способ неявного преобразования объекта в целочисленный тип, он будет разрешен. Я думаю, вы ожидаете поведения, при котором для каждого оператора case он будет заменен на него
if (t == typeof(int))
, но это откроет целую банку червей, когда вы перегрузите этот оператор. Поведение изменится при изменении деталей реализации для оператора switch, если вы неправильно написали переопределение ==. За счет сокращения сравнений до целочисленных типов и строк и тех вещей, которые могут быть сведены к целым типам (и предназначены для этого), они избегают потенциальных проблем.источник
Поскольку язык допускает строку тип в операторе switch, я предполагаю, что компилятор не может сгенерировать код для реализации ветвления с постоянным временем для этого типа и ему необходимо создать стиль «если-то».
@mweerden - Ага, понятно. Спасибо.
У меня нет большого опыта работы с C # и .NET, но кажется, что разработчики языка не разрешают статический доступ к системе типов, за исключением узких случаев. TypeOf ключевое слово возвращает объект , так это доступно только на время выполнения.
источник
Я думаю, что Хенк добился этого, предложив «запретить статический доступ к системе типов».
Другой вариант состоит в том, что в типах нет порядка, в котором могут быть числа и строки. Таким образом, переключатель типа не может построить двоичное дерево поиска, только линейный поиск.
источник
Я согласен с этим комментарием, что часто лучше использовать табличный подход.
В C # 1.0 это было невозможно, потому что в нем не было универсальных шаблонов и анонимных делегатов. В новых версиях C # есть все необходимое для этой работы. Также помогает обозначение объектных литералов.
источник
Я практически ничего не знаю о C #, но подозреваю, что либо переключение было просто использовано, как в других языках, не думая о том, чтобы сделать его более общим, либо разработчик решил, что его расширять не стоит.
Строго говоря, вы абсолютно правы, что нет никаких оснований для наложения на него таких ограничений. Можно подумать, что причина в том, что для допустимых случаев реализация очень эффективна (как было предложено Брайаном Энсинком ( 44921 )), но я сомневаюсь, что реализация очень эффективна (относительно if-операторов), если я использую целые числа и некоторые случайные случаи. (например, 345, -4574 и 1234203). И в любом случае, какой вред в том, чтобы разрешить это для всего (или, по крайней мере, больше) и сказать, что это эффективно только для определенных случаев (например, (почти) последовательных чисел).
Я могу, однако, представить себе , что можно было бы хотеть , чтобы исключить типы из - за причин , таких , как один дается lomaxx ( 44918 ).
Изменить: @Henk ( 44970 ): если строки максимально разделены, строки с одинаковым содержимым также будут указателями на одно и то же место в памяти. Затем, если вы можете убедиться, что строки, используемые в случаях, последовательно сохраняются в памяти, вы можете очень эффективно реализовать переключение (т.е. с выполнением в порядке 2 сравнений, добавления и двух переходов).
источник
C # 8 позволяет элегантно и компактно решить эту проблему с помощью выражения switch:
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
Вы можете узнать больше о новой функции здесь .
источник