
Почти 50 лет оператор switch (также известный как оператор case) был неотъемлемой частью программирования. Однако в последние годы некоторые утверждают, что оператор switch изжил себя. Другие идут еще дальше, называя оператор switch признаком плохого кода.
В 1952 году Стивен Клини концептуализировал оператор switch в своей работе Введение в метаматематику. Первая заметная реализация была в ALGOL 58 в 1958 году. Позже оператор switch был включен в неизгладимый язык программирования C, который, как мы знаем, повлиял на большинство современных языков программирования.
Перенесемся в наши дни, и практически каждый язык имеет оператор switch. Однако несколько языков исключили оператор switch. Наиболее заметным является Smalltalk.
Это пробудило мое любопытство: почему оператор switch был исключен из Smalltalk?
Энди Бауэр, один из создателей/сторонников Dolphin Smalltalk, поделился своими мыслями о том, почему Smalltalk исключил оператор switch:
Когда я впервые пришел к Smalltalk из C++, я не мог понять, как якобы полноценный язык не поддерживает конструкцию switch/case. В конце концов, когда я впервые перешел к “структурному программированию” от BASIC, я думал, что switch — одна из лучших вещей после нарезанного хлеба. Однако, поскольку Smalltalk не поддерживал switch, мне пришлось искать и понимать, как преодолеть этот недостаток. Правильный ответ, конечно, заключается в использовании полиморфизма и заставлении самих объектов направлять к правильному фрагменту кода. Тогда я понял, что это вовсе не “недостаток”, а Smalltalk заставлял меня использовать гораздо более детализированный ООП-дизайн, чем тот, к которому я привык в C++. Если бы был доступен оператор switch, мне потребовалось бы гораздо больше времени, чтобы изучить это, или, что еще хуже, я мог бы до сих пор программировать в псевдо-объектном стиле C++/Java в Smalltalk.
Я бы утверждал, что в обычном ООП нет реальной необходимости в операторе switch. Иногда, при взаимодействии с не-ООП миром (например, при получении и обработке сообщений WM_XXXX Windows, которые не являются объектами, а просто целыми числами), оператор switch был бы полезен. В таких ситуациях есть альтернативы (например, диспетчеризация из Dictionary), и количество случаев, когда они возникают, не оправдывает включение дополнительного синтаксиса.
Был ли Энди прав? Лучше ли нам без оператора switch? Выиграли бы другие языки также от исключения оператора switch?
Чтобы пролить свет на этот вопрос, я подготовил сравнение между оператором switch, словарем и полиморфизмом. Назовем это противостоянием. Пусть победит лучшая реализация!
Каждая реализация имеет метод, принимающий один параметр — целое число, и возвращающий строку. Мы будем использовать цикломатическую сложность и индекс сопровождаемости для изучения каждой реализации. Затем мы рассмотрим все три реализации целостно.
Код.
Оператор Switch
Индекс сопровождаемости | 72 |
---|---|
Цикломатическая сложность | 6 |
public class SwitchWithFourCases
{
public string SwitchStatment(int color)
{
var colorString = "Red";
switch (color)
{
case 1:
colorString = "Green";
break;
case 2:
colorString = "Blue";
break;
case 3:
colorString = "Violet";
break;
case 4:
colorString = "Orange";
break;
}
return colorString;
}
}
Словарь
Индекс сопровождаемости | 73 |
---|---|
Цикломатическая сложность | 3 |
public class DictionaryWithFourItems
{
public string Dictionary(int color)
{
var colorString = "Red";
var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
colorString = colors[color];
}
return colorString;
}
}
Полиморфизм
Общий индекс сопровождаемости | 94 |
---|---|
Общая цикломатическая сложность | 15 |
Интерфейс
Индекс сопровождаемости | 100 |
---|---|
Цикломатическая сложность | 1 |
public interface IColor
{
string ColorName { get; }
}
Фабрика
Индекс сопровождаемости | 76 |
---|---|
Цикломатическая сложность | 4 |
public class ColorFactory
{
public string GetColor(int color)
{
IColor defaultColor = new RedColor();
var colors = GetColors();
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
var c = colors[color];
return c.ColorName;
}
return defaultColor.ColorName;
}
private static IDictionary<int, IColor> GetColors()
{
return new Dictionary<int, IColor>
{
{1, new GreenColor()},
{2, new BlueColor()},
{3, new VioletColor()},
{4, new OrangeColor()},
{5, new MagentaColor()}
};
}
}
Реализация
Индекс сопровождаемости | 97 |
---|---|
Цикломатическая сложность | 2 |
public class BlueColor : IColor
{
public string ColorName => "Blue";
}
public class RedColor : IColor
{
public string ColorName => "Red";
}
public class GreenColor : IColor
{
public string ColorName => "Green";
}
public class MagentaColor : IColor
{
public string ColorName => "Magenta";
}
public class VioletColor : IColor
{
public string ColorName => "Violet";
}
Результаты
Прежде чем я углублюсь в результаты, давайте определим цикломатическую сложность и индекс сопровождаемости:
- Цикломатическая сложность — это мера ветвления логики. Чем меньше число, тем лучше.
- Индекс сопровождаемости измеряет сопровождаемость кода. Он находится в диапазоне от 0 до 100. Чем выше число, тем лучше.
Цикломатическая сложность | Индекс сопровождаемости | |
---|---|---|
Оператор Switch | 6 | 72 |
Словарь | 3 | 73 |
Полиморфизм | 15 | 94 |
Сначала мы рассмотрим цикломатическую сложность.
Результаты по цикломатической сложности прямолинейны. Реализация со словарем самая простая. Означает ли это, что это лучшее решение? Нет, как мы увидим при оценке индекса сопровождаемости.
Большинство думали бы, как и я, что реализация с наименьшей цикломатической сложностью наиболее сопровождаема — как может быть иначе?
В нашем сценарии реализация с наименьшей цикломатической сложностью не является наиболее сопровождаемой. Фактически, в нашем сценарии все наоборот. Самая сложная реализация наиболее сопровождаема! Взрыв мозга!
Если вы помните, чем выше оценка индекса сопровождаемости, тем лучше. Переходя к сути, полиморфизм имеет лучшую оценку индекса сопровождаемости — но он также имеет самую высокую цикломатическую сложность. В чем дело? Это кажется неправильным.
Почему самая сложная реализация наиболее сопровождаема? Чтобы ответить на это, мы должны понять индекс сопровождаемости.
Индекс сопровождаемости состоит из 4 метрик: цикломатическая сложность, строки кода, количество комментариев и объем Холстеда. Первые три метрики относительно хорошо известны, но последняя, объем Холстеда, относительно неизвестна. Как и цикломатическая сложность, объем Холстеда пытается объективно измерить сложность кода.
Простыми словами, объем Холстеда измеряет количество движущихся частей (переменные, системные вызовы, арифметика, конструкции кодирования и т.д.) в коде. Чем больше количество движущихся частей, тем больше сложность. Чем меньше количество движущихся частей, тем меньше сложность. Это объясняет, почему полиморфная реализация получает высокие баллы по индексу сопровождаемости; классы имеют мало или вообще не имеют движущихся частей. Другой способ взглянуть на объем Холстеда — он измеряет плотность “движущихся частей”.
Что такое программное обеспечение, если не изменение? Чтобы отразить реальный мир, мы вводим изменение. Я добавил новый цвет к каждой реализации.
Ниже приведены пересмотренные результаты.
Цикломатическая сложность | Индекс сопровождаемости | |
---|---|---|
Оператор Switch | 7 | 70 |
Словарь | 3 | 73 |
Полиморфизм | 17 | 95 |
Оператор switch и полиморфные подходы оба увеличились в цикломатической сложности на одну единицу, но интересно, что словарь не увеличился. Сначала я был озадачен этим, но затем понял, что словарь рассматривает цвета как данные, а две другие реализации рассматривают цвета как код. Я перейду к сути.
Обращая внимание на индекс сопровождаемости, только один, оператор switch, снизился в сопровождаемости. Оценка сопровождаемости полиморфизма улучшилась, и все же сложность также увеличивается (мы бы предпочли, чтобы она уменьшилась). Как я упоминал выше, это противоречит интуиции.
Наше сравнение показывает, что словари могут, с точки зрения сложности, масштабироваться бесконечно. Полиморфный подход безусловно наиболее сопровождаем и, кажется, увеличивается в сопровождаемости по мере добавления новых сценариев. Оператор switch увеличивается в сложности и уменьшается в сопровождаемости при добавлении нового сценария. Даже до того, как мы добавили новый сценарий, он имел худшие показатели цикломатической сложности и индекса сопровождаемости.
Джем Финч из Google поделился своими мыслями о недостатках операторов switch:
1. Реализации полиморфных методов лексически изолированы друг от друга. Переменные могут быть добавлены, удалены, изменены и так далее без какого-либо риска воздействия на несвязанный код в другой ветви оператора switch.
2. Реализации полиморфных методов гарантированно возвращаются в правильное место, предполагая, что они завершаются. Операторы switch в языке с проваливанием, таком как C/C++/Java, требуют подверженного ошибкам оператора “break”, чтобы гарантировать, что они возвращаются к оператору после switch, а не к следующему блоку case.
3. Существование реализации полиморфного метода может быть обеспечено компилятором, который откажется компилировать программу, если реализация полиморфного метода отсутствует. Операторы switch не предоставляют такой проверки полноты.
4. Диспетчеризация полиморфных методов расширяема без доступа к (или перекомпиляции) другому исходному коду. Добавление еще одного случая к оператору switch требует доступа к исходному коду диспетчеризации, не только в одном месте, но и в каждом месте, где соответствующий enum переключается.
5. … вы можете тестировать полиморфные методы независимо от механизма переключения. Большинство функций, которые переключаются, как в примере, который дал автор, будут содержать другой код, который затем не может быть отдельно протестирован; вызовы виртуальных методов, с другой стороны, могут быть.
6. Вызовы полиморфных методов гарантируют диспетчеризацию за постоянное время. Никакой достаточно умный компилятор не нужен для преобразования того, что естественно является конструкцией линейного времени (оператор switch с проваливанием) в конструкцию постоянного времени.
К сожалению или к счастью, в зависимости от вашего лагеря, большинство языков имеют оператор switch, и они никуда не денутся в ближайшее время. Имея это в виду, хорошо знать, что происходит под капотом при компиляции операторов switch.
Существует три оптимизации оператора switch, которые могут произойти:
- Операторы If-elseif — Когда оператор switch имеет небольшое количество случаев или разреженные случаи (неинкрементальные значения, такие как 10, 250, 1000), он преобразуется в оператор if-elseif.
- Таблица переходов — В больших наборах смежных случаев (1, 2, 3, 4, 5) компилятор преобразует оператор switch в таблицу переходов. Таблица переходов — это по сути хеш-таблица с указателем (думайте об операторе goto) на функцию в памяти.
- Бинарный поиск — Для больших наборов разреженных случаев компилятор может реализовать бинарный поиск для быстрого определения случая, аналогично тому, как работает индекс в базе данных. В исключительных случаях, где случаи представляют большое количество разреженных и смежных случаев, компилятор будет использовать комбинацию трех оптимизаций.
Заключение
В объектно-ориентированном мире оператор switch, задуманный в 1952 году, является основой инженера-программиста. Заметным исключением является Smalltalk, где дизайнеры решили исключить оператор switch.
При сравнении с альтернативными эквивалентными реализациями — словарем и полиморфизмом — оператор switch показал себя не так хорошо.
Оператор switch никуда не денется, но, как показало наше сравнение, есть лучшие альтернативы оператору switch.
Реализации доступны на Github.
Автор: Чак Конвей специализируется на разработке программного обеспечения и генеративном ИИ. Свяжитесь с ним в социальных сетях: X (@chuckconway) или посетите его на YouTube.