Skip to content

Посты

Рассмотрение аргументов в пользу оператора switch

6 декабря 2015 г. • 9 мин чтения

Рассмотрение аргументов в пользу оператора switch

На протяжении почти 50 лет оператор switch (также известный как оператор case) был неотъемлемой частью программирования. Однако в последние годы некоторые утверждают, что оператор switch исчерпал свою полезность. Другие идут еще дальше, называя оператор switch признаком плохого кода.

В 1952 году Стивен Клини разработал оператор switch в своей работе Introduction to Metamathematics. Первая заметная реализация была в 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 pseudo-object в 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. Чем выше число, тем лучше.
Циклическая сложностьИндекс поддерживаемости
Оператор Switch672
Словарь373
Полиморфизм1594

Сначала рассмотрим циклическую сложность.

Результаты циклической сложности очевидны. Реализация со словарем является самой простой. Означает ли это, что это лучшее решение? Нет, как мы увидим при оценке индекса поддерживаемости.

Большинство, как и я, думали, что реализация с наименьшей циклической сложностью является наиболее поддерживаемой — как это может быть иначе?

В нашем сценарии реализация с наименьшей циклической сложностью не является наиболее поддерживаемой. На самом деле в нашем сценарии все наоборот. Самая сложная реализация является наиболее поддерживаемой! Ум взорван!

Если вы помните, чем выше оценка индекса поддерживаемости, тем лучше. Переходя к сути, полиморфизм имеет лучший показатель индекса поддерживаемости — но он также имеет наивысшую циклическую сложность. Что происходит? Это не кажется правильным.

Почему самая сложная реализация является наиболее поддерживаемой? Чтобы ответить на это, мы должны понять индекс поддерживаемости.

Индекс поддерживаемости состоит из 4 метрик: циклическая сложность, количество строк кода, количество комментариев и объем Халстеда. Первые три метрики относительно хорошо известны, но последняя, объем Халстеда, относительно неизвестна. Как и циклическая сложность, объем Халстеда пытается объективно измерить сложность кода.

Проще говоря, объем Халстеда измеряет количество движущихся частей (переменные, системные вызовы, арифметика, конструкции кодирования и т. д.) в коде. Чем больше количество движущихся частей, тем больше сложность. Чем меньше количество движущихся частей, тем ниже сложность. Это объясняет, почему полиморфная реализация получает высокий балл по индексу поддерживаемости; классы имеют мало или вообще не имеют движущихся частей. Другой способ посмотреть на объем Халстеда — это измерение плотности “движущихся частей”.

Что такое программное обеспечение, если не изменение? Чтобы отразить реальный мир, мы вводим изменение. Я добавил новый цвет в каждую реализацию.

Ниже приведены пересмотренные результаты.

Циклическая сложностьИндекс поддерживаемости
Оператор Switch770
Словарь373
Полиморфизм1795

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

Обратив внимание на индекс поддерживаемости, только один — оператор switch — снизился в поддерживаемости. Показатель поддерживаемости полиморфизма улучшился, и все же сложность также увеличилась (мы бы предпочли, чтобы она уменьшилась). Как я упомянул выше, это противоинтуитивно.

Наше сравнение показывает, что словари могут, с точки зрения сложности, масштабироваться бесконечно. Полиморфный подход является наиболее поддерживаемым и, похоже, увеличивает поддерживаемость по мере добавления новых сценариев. Оператор switch увеличивает сложность и снижает поддерживаемость при добавлении нового сценария. Даже до того, как мы добавили новый сценарий, он имел наихудшие показатели циклической сложности и индекса поддерживаемости.

Джем Финч из Google поделился своими мыслями о недостатках операторов switch:

1. Реализации полиморфных методов лексически изолированы друг от друга. Переменные можно добавлять, удалять, изменять и т. д. без риска влияния на несвязанный код в другой ветви оператора switch.

2. Реализации полиморфных методов гарантированно возвращаются в правильное место, при условии, что они завершаются. Операторы switch в языке с fallthrough, таком как C/C++/Java, требуют подверженного ошибкам оператора “break”, чтобы убедиться, что они возвращаются к оператору после switch, а не к следующему блоку case.

3. Существование реализации полиморфного метода может быть обеспечено компилятором, который откажется компилировать программу, если реализация полиморфного метода отсутствует. Операторы switch не обеспечивают такую проверку полноты.

4. Отправка полиморфных методов расширяется без доступа к (или перекомпиляции) другому исходному коду. Добавление еще одного case к оператору switch требует доступа к исходному коду отправки не только в одном месте, но и в каждом месте, где переключается соответствующий enum.

5. … вы можете тестировать полиморфные методы независимо от механизма переключения. Большинство функций, которые переключаются, как в примере, который привел автор, будут содержать другой код, который затем нельзя будет отдельно протестировать; вызовы виртуальных методов, с другой стороны, могут.

6. Вызовы полиморфных методов гарантируют отправку за постоянное время. Не требуется достаточно умный компилятор для преобразования того, что естественно является линейной конструкцией (оператор switch с fallthrough) в конструкцию с постоянным временем.

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

Существует три оптимизации оператора switch, которые могут произойти:

  1. Операторы if-elseif — когда оператор switch имеет небольшое количество case или разреженные case (неинкрементные значения, такие как 10, 250, 1000), он преобразуется в оператор if-elseif.
  2. Таблица переходов — для больших наборов соседних case (1, 2, 3, 4, 5) компилятор преобразует оператор switch в таблицу переходов. Таблица переходов — это по сути хеш-таблица с указателем (подумайте об операторе goto) на функцию в памяти.
  3. Бинарный поиск — для больших наборов разреженных case компилятор может реализовать бинарный поиск для быстрого определения case, аналогично тому, как работает индекс в базе данных. В экстраординарных случаях, когда case представляют собой большое количество разреженных и соседних case, компилятор будет использовать комбинацию всех трех оптимизаций.

Резюме

В объектно-ориентированном мире оператор switch, разработанный в 1952 году, является основой инженера-программиста. Заметным исключением является Smalltalk, где разработчики решили исключить оператор switch.

При сравнении с альтернативными эквивалентными реализациями, словарем и полиморфизмом, оператор switch показал себя не так хорошо.

Оператор switch останется, но, как показало наше сравнение, существуют лучшие альтернативы оператору switch.

Реализации доступны на Github.

Автор: Chuck Conway — инженер AI с почти 30-летним опытом разработки программного обеспечения. Он создает практические системы AI — конвейеры контента, агенты инфраструктуры и инструменты, которые решают реальные проблемы — и делится тем, что он узнает на этом пути. Свяжитесь с ним в социальных сетях: X (@chuckconway) или посетите его на YouTube и на SubStack.

↑ Вернуться в начало

Вам также может понравиться