Enum’ы (или перечисления) являются удобной возможностью языка, которая зачастую помогает сделать код более прозрачным, расширяемым и сопровождаемым. Хотелось бы поделиться некоторой хитростью, которой я пользуюсь для превращения enum’ов в более удобный инструмент. Давайте представим себе пример, в котором речь идет о перечислении птиц.
Для начала код:
typedef enum _BirdKind
{
kBirdKindCanary,
kBirdKindHen,
kBirdKindPenguin
} BirdKind;
//Первый элемент перечисления
#define bFirst kBirdKindCanary
// Последний элемент перечисления
#define bLast kBirdKindPenguin
// Элемент перечисления, который маркирует выход за пределы перечисления
#define bEnd (bLast + 1)
// Количество элементов в перечислении
#define bCount (bLast - bFirst + 1)
// Преобразование элемента перечисления в индекс, где элементу bFirst соответствует индекс 0.
// Если в ваших перечислениях элементы всегда начинаются с 0, можете не использовать этот макрос,
// а просто подставлять сам элемент перечисления в качестве индекса
#define bIndex(bk) (bk - bFirst)
// Получение следующего элемента
#define bNext(cur) ( ((cur >= bFirst) && (cur < bLast)) ? (BirdKind)(cur + 1) : bEnd)
Как можно заметить, речь идет о добавлении ряда макросов к определению enum’а, которые призваны упростить использования этого enum’а. Префикс ‘k’ у элементов перечисления означает ‘key’, я привык этим пользоваться, т.к. при взгляде на кусок кода, где есть элемент enum’а, визуально сразу становится понятно, чем является этот элемент. Префикс ‘b’ у макросов означает ‘bird’ — это отсылка к типу, к которому они относятся. Если у вас в приложении есть несколько enum’ов на букву ‘b’, можете использовать префикс ‘brd’. Если у вас существует проектный префикс (например, проект называется ‘Angry Birds’ и проектный префикс ‘ab’), можете использовать для макросов префикс ‘abb’, где ‘ab’ — это отсылка к проекту, а ‘b’ — отсылка к типу.
Я хочу обратить внимание на то, что не все enum’ы нуждаются в такого рода макросах. Если, скажем, enum используется всего в одном файле в небольшом количестве мест, то его совершенно не стоит сопровождать подобного рода макросами. Если же enum используется повсеместно в проекте, в различных классах и различными способами — использование макросов может быть очень даже оправдано.
В приведенном варианте реализации макросы bCount
, bIndex
, bNext
будут корректно работать только в том случае, если элементы перечисления идут друг за другом (т.е. их значения последовательны: 1, 2, 3 и т.д.). Если элементы перечисления изменяются по какому-то другому принципу (например, являются битовыми флагами), то данные макросы следует соответственно изменить.
Следует более подробно остановиться на макросе bNext. Важный момент, касающийся его, — это наличие условного оператора. Плюс от использования условного оператора таков, что мы получаем возможность определить bEnd как угодно, например как -1
или как MAX_INT
, или как-то еще (бывают ситуации, когда определение bEnd
как (bLast + 1)
невозможно, по причине использования (bLast + 1)
для других целей. Также условный оператор позволяет из bNext(cur)
возвращать bEnd
для всех значений cur
, не входящих в диапазон значений enum’а. Однако, условный оператор может быть неприемлем в критичном по производительности месте. На этот случай можно отвязаться от произвольного определения bEnd
, решив что bEnd
это всегда (bLast + 1)
. И тогда можно сделать так:
#define bNext(cur) (BirdKind)(cur + 1)
Таким образом мы избавляемся от условного оператора, а вместе с ним и от некоторых его преимуществ, упомянутых выше.
Такой подход находит применение в циклах, итерирующихся по элементам enum’а:
for(BirdKind bk = bFirst; bk != bEnd; bk = bNext(bk))
{
//some code
}
В создании массивов и индексировании массивов, которые имеют дело с элементами этого enum’а
BOOL couldFlyFlags[bCount] = {YES, NO, NO};
if (YES == couldFlyFlags[bIndex(kBirdKindCanary)])
{
//some code
}
Во введении ограничений на значения перечислений. Такие ограничения имеют смысл, если есть вероятность передачи некорректного значения в качестве элемента перечисления:
void proceedBirdKind: (BirdKind) birdKind
{
assert((birdKind >= bFirst) && (birdKind <= bLast));
//some code
}
Позволяет удобно модифицировать enum. Если у нас появляется новая птица, которая должна попасть в конец перечисления, нам достаточно добавить ее и модифицировать bLast
. Неудобств при этом не возникнет, т.к. сам enum и дефайн bLast
находятся на расстоянии нескольких строчек кода. Чтобы оценить простоту этого решения, представьте себе ситуацию, когда макросы не используются для манипуляций enum’ом и в различных местах вашего кода встречаются циклы, вроде такого:
for(BirdKind bk = kBirdKindCanary; bk <= kBirdKindPenguin; bk = (BirdKind)(bk + 1))
{
//some code
}
При необходимости добавления еще одного элемента в конец enum’а, вам придется найти все подобные куски кода и внести исправления в них.
Существует довольно широко принятая практика использования перечислений в следующей манере:
typedef enum _BirdKind
{
kBirdKindCanary,
kBirdKindHen,
kBirdKindPenguin,
kBirdKindCount
} BirdKind;
При таком использовании перечислений мы получаем возможность писать циклы и обрабатывать массивы так:
BOOL couldFlyFlags[kBirdKindCount] = {YES, NO, NO};
for(BirdKind bk = kBirdKindCanary; bk != kBirdKindCount; bk = (BirdKind)(bk + 1))
{
if (YES == couldFlyFlags[bk])
{
//some code
}
//some code
}
В этом варианте я вижу один существенный недостаток, который состоит в том, что в состав перечисления птиц входит элемент kBirdKindCount
, который птицей не является и несет совершенно другой смысл. Не стоит упоминать о том, что два смысла всегда сложнее, чем один смысл. Я глубоко убежден, что не стоит усложнять код там, где можно без этого обойтись. В приведенном мною варианте использования enum’ов вместе с макросами, можно говорить об увеличении объема кода, но нельзя говорить об усложнении понимания кода. Если программист знаком с подобным приемом, то понять такой код ему будет совсем не сложно. А познакомиться с данным приемом, согласитесь, особого труда не составляет. Помимо прочего, в последнем варианте индексация по массиву будет производиться корректно лишь в том случае, если элементы enum’а будут начинаться с 0. В противном случае нужен будет преобразователь элемента к индексу (навроде упомянутого bIndex
)
Также хотелось бы отдельно отметить такой вариант использования enum’ов, как:
for(NSInteger bk = kBirdKindCanary; bk != kBirdKindCount; bk = bk + 1)
{
//some code
}
Плюс очевиден — нет операции приведения типа. Минус не так очевиден, но он есть — компилятор не сможет проверить соответствие типов, если внутри цикла bk будет передаваться в какой-нибудь метод как тип BirdKind
. Вам придется либо отключить опцию компилятора по проверке соответствия типов enum’ов, либо мириться с warning’ами, либо сделать приведение типа при передаче значения в метод, но тогда вы лишитесь упомянутого плюса. Я настоятельно рекомендую использовать преимущество проверки типов, т.к. это сокращает количество потенциальных ошибок в коде.
По сути, описанный мною метод работы с enum’ами (который с макросами) с помощью макросов абстрагирует вас в коде от таких вещей, как первый (bFirst
) и последний (bLast
) элементы, некорректный элемент (bEnd
), количество элементов (bCount
), механизм индексирования (bIndex
) и механизм итерирования (bNext
). Эти абстракции позволяют лучше понимать и модифицировать код.
У программистов иногда встречается такое искушение, как размещение enum’ов вместе с другими типами в одном файле. Иногда это бывает оправданным (например, в упомянутом выше случае, когда enum используется всего в одном файле). Если же enum используется повсеместно и вы решили дополнить его макросами, я рекомендую размещать такие enum’ы в отдельных файлах (для нашего примера это будет ‘BirdKind.h’), для того, чтобы не мешать определение самого enum’а и его макросов с другим кодом.
Чтобы приблизить все это к жизни, приведу пример из моего опыта, где описанный выше подход был полезен.
Речь пойдет о разработке игры, в которой существует игровое поле, разделенное на квадратные клетки. Из клетки можно переместиться в одну из четырех соседних клеток. Здесь мы имеем дело с четырьмя направлениями, которые мы можем перечислить как: kDirectionUp, kDirectionLeft, kDirectionRight, kDirectionDown
. Все это дело мы организуем в enum и сопровождаем соответствующими макросами(dFirst, dLast, dEnd, dCount, dIndex, dNext
). Теперь в коде, допустим, нам нужно для соседней клетки задать маску проходимости в соседние клетки. Для этого мы можем создать у объекта клетки поле _passibilityMask[dCount] и
легко работать с этой маской. Можно добавить клетке метод getNeighborInDirection:
, который будет возвращать объект соседней клетки по заданному направлению. Допустим, нам нужно для текущей клетки просмотреть всех соседей и подсветить тех из них, в которые мы можем перейти. Выглядеть это будет так:
for(Direction d = dFirst; d != dEnd; d = dNext(d))
{
if (YES == _passibilityMask[dIndex(d)])
{
Cell *neighbor = [self getNeighborInDirection: d];
[neighbor glowOn];
}
}
Читабельно, не так ли?
И что произойдет, если игровое поле решат переделать в поле из шестиугольников? Изменится количество и название направлений. Но изменять код не нужно будет, нужно будет лишь модифицировать хидер с описанием направлений.
В заключение хотелось бы отметить, что описанный подход не был почерпнут мною из каких либо источников — это исключительно результат многих и многих проб и ошибок. Я жду комментариев и предложений по улучшению. Если вам уже встречалось что-то подобное — дайте знать, может быть удастся избежать каких-то граблей, которые пока не видны, но грозят ударить больно.
Материал предоставлен компанией Softeq Development, FLLC
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: