C # switch оператор ограничения - почему? - PullRequest
133 голосов
/ 05 сентября 2008

При написании оператора 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 имеет , который поддается только статическому анализу, и почему включаемое значение должно быть целым (то есть примитивным). В чем оправдание?

Ответы [ 16 ]

110 голосов
/ 07 сентября 2008

Важно не путать оператор переключения 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.

При больших несмежных наборах выражений компилятор может начать поиск в двоичном дереве и, наконец, если-еще-если-еще, последние несколько элементов.

В случае наборов выражений, содержащих скопления смежных элементов, компилятор может выполнять поиск в двоичном дереве и, наконец, переключение 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
96 голосов
/ 05 сентября 2008

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

оператор switch не тот вещь, как большое заявление если-еще. Каждый случай должен быть уникальным и оцененным статически. Переключатель делает ветвь с постоянным временем независимо от сколько у вас дел Если-иначе оператор оценивает каждое условие пока он не найдет тот, который является истинным.


На самом деле оператор C # switch - это , а не всегда ветвь с постоянным временем.

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

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

21 голосов
/ 05 сентября 2008

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

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

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

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

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

10 голосов
/ 05 сентября 2008

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

10 голосов
/ 05 сентября 2008

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

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

  • создать большой оператор if-else
  • использовать команду переключения MSIL (таблица переходов)
  • построить Generic.Dictionary , заполнить его при первом использовании и вызвать 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» для выражения объекта не имеет значения.

6 голосов
/ 17 февраля 2017

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));
}
6 голосов
/ 05 июня 2009

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

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

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

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

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

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

6 голосов
/ 05 сентября 2008

В то время как по теме, по словам Джеффа Этвуда, оператор 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]);
3 голосов
/ 07 марта 2010

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

Это не причина, но в разделе 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

...