Строки или перечислимый тип: что лучше использовать в качестве параметров функций, если набор возможных значений ограничен

Рассказывает Arne Mertz 


Порой в качестве входных данных разрешен фиксированный набор строк. В дальнейшем они хранятся и как-то используются. Перечислимый тип будет лучшим вариантом. 

Пример

На прошлой неделе мы с коллегой работали над простым заданием. Тестирующий фреймворк мог обрабатывать два типа сообщений, посылаемых API: ошибки и предупреждения. API также отправляет сообщения, и нужно было настроить фреймворк для их обработки. Ничего удивительного: эта троица (сообщение, ошибка, предупреждение) практически неразлучна.

Добиться нужной функциональности было легко. К двум уже объявленным «шагам» в Cucumber нужно было добавить третий. Две функции вызывались этими шагами, и мы написали третью. Таким образом, мы добавили поддержку сообщений.

Всё работало. Мы могли проверить код и выпить по чашке кофе, но совесть наша была не чиста. Мы столкнулись с повторами кода и, что ещё хуже, сами написали третий повтор.

Don’t repeat yourself

В результате мы унифицировали все три «шага», добавив ещё один параметр: строку, принимающую значение "info", "warning" или "error". Функция, вызываемая в этом «шаге», тоже получала тип сообщения в качестве параметра, и так далее, до уровня доступа к API.

Тем не менее, ещё было, над чем поработать: строка, содержащая тип сообщения, всё ещё использовалась на всех уровнях.

Перечислимые типы вместо строк

Эти строки стоит конвертировать в перечислимый тип. Ниже приведены их основные преимущества.

Сравнения и опечатки

Строки могут содержать любые последовательности символов, даже бессмысленные. Если где-то в сравнении была допущена опечатка, найти её будет непросто. Перечислимые типы же, напротив, являются идентификаторами, и компилятор пожалуется на несоответствие. Рассмотрим в качестве примера маленькую функцию:

В нашем примере помеченная строка никогда не будет достигнута, поскольку messageType никогда не будет равен "waring". Я сделал эту опечатку, но мой напарник был достаточно внимателен, чтобы её заметить. В противном случае мне пришлось бы тратить время на отладку. Используй я перечислимый тип, IDE и компилятор сразу бы указали мне на ошибку.

Соответствие типов

Вернёмся к предыдущей функции. Вызовем её:

Упс. Мы только что хотели напечатать сообщение типа "Something bad happened!" с текстом "error". Если бы тип сообщения был перечислимым, такой проблемы бы не возникло.

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

Switch/case

В C++ мы не можем использовать строки в условном операторе switch. Вместо этого приходится нагромождать if/else. А вот enum’ы — можем!  Кроме того, мы сможем получать предупреждения компилятора или статического анализатора, если забудем про перечислимый тип.

Производительность

Я не рекомендую гнаться исключительно за производительностью. Но в нашем случае, кроме производительности, мы получим ещё и более поддерживаемый код, поэтому стоит разобраться чуть подробнее.

Сравнения перечислимых типов обычно быстрее, чем сравнения строк. Размер перечислимого типа равен размеру целого типа а строки могут быть весьма большими. Кроме того, оператор switch может быть траслирован в jump-таблицы, что может быть эффективнее if/else.

Перечислимые типы не обязательно более производительны, чем строки. Тем не менее, они явно не хуже, и это всё, о чём стоит беспокоиться.

Заключение

Если у вас есть фиксированный набор строк, используемый в качестве входных или выходных данных, его однозначно стоит конвертировать в enum.

Перевод статьи «Strings vs. Enumerators»