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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *