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

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

Например (да, я знаю, если вы делаете такие вещи, это, вероятно, означает ваш object-oriented (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() терпит неудачу с "Ожидаемым значением целочисленного типа", а утверждения case терпят неудачу с "Ожидается постоянное значение".

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

Ответ 1

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

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


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

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

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

Ответ 2

Важно не путать оператор switch С# с инструкцией переключателя CIL.

Переключатель CIL представляет собой таблицу перехода, которая требует индекса в набор адресов перехода.

Это полезно, только если случаи переключения С# смежны:

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-way switch, 10000 итераций (мс): 26.593
приблизительное время на 50-контактный переключатель (мс): 0,0026593

общее время для запуска 5000-way switch, 10000 итераций (мс): 23.7094
приблизительное время на 5000-контактный переключатель (мс): 0,00237094

общее время для запуска переключателя 50000, 10000 итераций (мс): 20.0933
приблизительное время на 50000 переключателей (мс): 0,00200933

Затем я также использовал несмежные выражения case:

общее время для запуска 10-ходового переключателя, 10000 итераций (мс): 19.6189
приблизительное время на 10-позиционный переключатель (мс): 0,00196189

общее время для запуска 500-way switch, 10000 итераций (мс): 19.1664
приблизительное время на 500-контактный переключатель (мс): 0,00191664

общее время для запуска 5000-way switch, 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.

Если вы указали С# Language Specification (а не спецификацию CIL) вы найдете "15.7.2. Оператор switch" не упоминает о "постоянном времени" или о том, что базовая реализация даже использует инструкцию переключения CIL (будьте осторожны при принятии таких вещей).

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


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

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

  jmp     ds:300025F0[eax*4]

Если поиск двоичного дерева заполнен:

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

Ответ 3

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

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

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

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

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

  • Чтобы значения меток переключателя были постоянными
  • Значения ярлыков переключателей различны (так что для данного выражения switch можно выбрать только один блок переключателей)

Рассмотрим этот пример кода в гипотетическом случае, когда допустимы недопустимые значения:

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;
    }
}

Что будет делать код? Что делать, если заявления о делах переупорядочены? В самом деле, одна из причин, по которым С# сделала переход с ошибкой незаконным, заключается в том, что операторы switch могут быть произвольно перегруппированы.

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

Ответ 4

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

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

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

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

Чтобы обрабатывать случай String, компилятор закончится (в какой-то момент), используя a.Equals(b) (и, возможно, a.GetHashCode()). Я думаю, что для компилятора было бы триумфом использовать любой объект, который удовлетворяет этим ограничениям.

Что касается необходимости создания статических выражений... некоторые из этих оптимизаций (хеширование, кеширование и т.д.) не будут доступны, если выражения case не являются детерминированными. Но мы уже видели, что иногда компилятор просто выбирает упрощенную дорогу if-else-if-else...

Изменить: lomaxx - Ваше понимание оператора typeof неверно. Оператор "typeof" используется для получения объекта System.Type для типа (не имеет ничего общего с его супертипами или интерфейсами). Проверка совместимости времени выполнения объекта с заданным типом является оператором оператора "is". Использование "typeof" здесь для выражения объекта не имеет значения.

Ответ 5

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

Ответ 6

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

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

Там какая-то интересная информация по поводу дизайнерских решений, которые вошли в "переключатель" здесь: Почему оператор С# switch, предназначенный для того, чтобы не разрешить провал, но все еще требуется перерыв?

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

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

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

Ответ 7

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

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

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

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

Ответ 8

Microsoft, наконец, услышала вас!

Теперь с С# 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));
}

Ответ 9

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

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

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

Ответ 10

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

Ответ 11

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

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

Но этого не так много.

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

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

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

Ответ 12

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

Ответ 13

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

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

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

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

Ответ 14

писал:

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

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

@mweerden - А я вижу. Спасибо.

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

Ответ 15

Я думаю, что Хенк пригвоздил его с помощью функции "no sttatic access to the type system"

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

Ответ 16

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

В С# 1.0 это было невозможно, потому что у него не было дженериков и анонимных делегатов. В новых версиях С# есть строительные леса, чтобы сделать эту работу. Также полезно использовать обозначения для объектных литералов.