الشيفرة المليئة بالأعداد المجرّدة شيفرة لا يستطيع أحد قراءتها. فـ if (state == 2) لا تخبرك شيئًا؛ وif (state == STATE_RUNNING) تخبرك كل شيء. وenum هي طريقة C لمنح مجموعة صغيرة ثابتة من الأعداد الصحيحة أسماءً لائقة - وخلافًا لكومة من #define، تأتي مجمّعة في نوع واحد يعرفه المترجم.
تعريف enum
يسمّي التصريح وسمًا ويسرد المعدودات بين قوسين معقوفين:
أمران يجدر ملاحظتهما فورًا. أولًا، يبدأ الترقيم من 0 ويزيد بواحد، فتكون RED صفرًا وGREEN واحدًا وBLUE اثنين. ثانيًا، أسماء المعدودات مرئية في كل مكان في النطاق المحيط - فتكتب GREEN، لا Color.GREEN. وهي تعيش في فضاء الأسماء نفسه الذي تعيش فيه متغيّراتك، ولهذا تصرخ بها شيفرة C عرفًا أو تسبقها بالنوع (COLOR_RED) تجنّبًا للتصادمات.
وطباعة enum تستخدم %d، لأن قيمة التعداد هي قيمة عددية صحيحة.
القيم الصريحة
أسنِد لأي معدود ثابتًا صريحًا، فتواصل التي بعده من هناك:
ولا يلزم أن تكون القيم فريدة ولا مرتّبة - فقد يتشارك معدودان عددًا واحدًا، وهو أمر مفيد أحيانًا للأسماء البديلة (COLOR_DEFAULT = COLOR_BLACK). ويجب أن تكون ثوابت عددية صحيحة وقت الترجمة، فلا يمكنك حساب واحدة من متغيّر.
وثمّة صيغة شائعة تستخدم هذا لتسجيل العدد:
enum Suit {
CLUBS, DIAMONDS, HEARTS, SPADES,
SUIT_COUNT /* 4 تلقائيًا - ويبقى صحيحًا إن أضفت نوعًا */
};
SUIT_COUNT ليست نوعًا حقيقيًا من الورق؛ بل عددها، ويصونه الترقيم التلقائي مجانًا. ويمكن للحلقات وأحجام المصفوفات استخدامه دون أن يتقادم أبدًا.
typedef enum
كما هي الحال مع البنى، اسم النوع الكامل في C هو enum Color، وtypedef يُسقط الكلمة المفتاحية:
ولأن المعدودات أعداد صحيحة عادية تبدأ من 0، فإنها تصلح فهارس مصفوفات كذلك - فـ dx[DIR_EAST] تعمل دون أي تحويل. وتلك أنفع خاصية في تعداد يبدأ من الصفر.
التعدادات في switch
اقتران enum بـ switch هو حيث يبدأ النوع بردّ ثمنه. ترجم بـ -Wall فيخبرك المترجم حين تنسى حالة.
أضف STATE_FAILED إلى التعداد فيحذّر GCC: enumeration value 'STATE_FAILED' not handled in switch. وذلك التحذير أقرب ما لدى C إلى فحص الشمول، وهو سبب قوي لكتابة هذه الجمل بلا حالة default: - فـ default تُسكت التحذير وتدع الحالات الجديدة تمرّ دون معالجة. ضع الاحتياطي بعد switch بدلًا من ذلك، كما تفعل describe.
تحويل enum إلى سلسلة نصية
لا تحتفظ C بأي أسماء أثناء التشغيل؛ فـ printf("%s", GREEN) لا يمكن أن تعمل لأن GREEN ليست سوى 1. ونمطان يحلّان ذلك.
نسخة switch، التي يفحصها المترجم:
ونسخة جدول البحث، أقصر لكنها غير مفحوصة - فإن انحرفت المصفوفة والتعداد عن بعضهما، لا شيء يحذّر:
وفهارس المصفوفة المعيَّنة ([RED] = "RED") تُبقي الأسماء على الأقل مرتبطة بمواضعها، فلا يمكن لإعادة ترتيب التعداد أن تخلط السلاسل بصمت. وفحص الحدود مهمّ لأن لا شيء يمنع مستدعيًا من تمرير قيمة خارج التعداد - انظر القسم التالي.
التعدادات ليست محكمة الإغلاق
متغيّر التعداد عدد صحيح في العمق، ولا تراقب C المدى:
الحساب على المعدودات يعمل، والمقارنات مع أعداد صحيحة عادية تعمل، والتحويل إلى قيمة خارج المدى مقبول. فعامِل enum على أنه توثيق ممتاز وتلميح جيد للمترجم، لا ضمانة. وتحقّق من أي قيمة تصل من خارج برنامجك - ملف أو وسيط أو رسالة شبكة - قبل أن توزّعها بـ switch.
الحجم والتخزين
في C17، يجب أن يتّسع كل معدود في int - فـ enum Huge { BIG = 3000000000 }; مخالفة قيد يرفضها مترجم صارم (-pedantic-errors)، وإن قبلها gcc وclang توسعةً في وضعهما الافتراضي. والمعدودات نفسها من النوع int، بينما حجم نوع التعداد يحدّده التطبيق (وعمليًا هو int):
(وترفع C23 هذا القيد: فقد يتجاوز المعدود مدى int، بل يمكنك اختيار النوع الأساسي بـ enum E : unsigned long { ... }. وإلى أن تستهدف أدواتك C23، أبقِ المعدودات داخل int.)
ولأن الاختيار المضبوط يحدّده التطبيق، لا تكتب أبدًا enum مباشرةً في ملف ثنائي أو حزمة شبكة وتتوقّع أن يقرأها جهاز آخر. حوّل إلى نوع ثابت العرض مثل uint8_t أو uint32_t من <stdint.h> عند الحدّ.
enum مقابل #define
كلاهما يمنح اسمًا لعدد. والتعداد أفضل عمومًا:
enum | #define | |
|---|---|---|
| يعالجه | المترجم | المعالج الأولي |
| التجميع | القيم المترابطة تتشارك نوعًا | كل ثابت مستقلّ |
| الترقيم التلقائي | نعم | لا، ترقّم يدويًا |
| مرئي في المنقّح | غالبًا، بالاسم | لا، العدد فقط |
تحذير شمول switch | نعم | لا |
| النطاق | يحترم نطاق الكتلة | نصّي، حتى #undef |
الجأ إلى #define حين لا يكون الثابت عددًا صحيحًا (سلسلة نصية أو عدد عشري) أو حين يجب أن يكون صالحًا لاستخدام المعالج الأولي نفسه، كما في الترجمة الشرطية. أما لمجموعة من الحالات أو الرايات أو الأصناف الصحيحة المترابطة، فاستخدم enum.
أعراف التسمية
تتصادم التعدادات بسهولة لأن أسماءها تقع في النطاق العادي. والأعراف التي تُبقي قواعد شيفرة C الكبيرة عاقلة:
- اسبق كل معدود باسم النوع:
COLOR_REDوSTATE_IDLEوHTTP_OK. فـREDبلا بادئة في برنامج رسوميات ستلتقي في النهاية بـREDشخص آخر. - اكتب المعدودات بأحرف كبيرة، مطابقةً لشكل الثوابت الأخرى.
- سمِّ النوع بصيغة المفرد بـ PascalCase أو
snake_case_t:ColorأوStateأوcolor_t، باتّساق عبر المشروع. - أضف حارس
_COUNTأخيرًا حين تنوي المرور أو الفهرسة بالتعداد.
الأسئلة الشائعة
كيف تعرّف enum في لغة C؟
enum Color { RED, GREEN, BLUE }; يعرّف نوعًا قيمه هي الثوابت الثلاثة المسمّاة. وافتراضيًا تُرقَّم من 0، فتكون RED صفرًا وGREEN واحدًا وBLUE اثنين. عرّف متغيّرًا بـ enum Color c = GREEN;، أو أضف typedef لتكتب Color c فقط.
هل يمكن إسناد قيم محدّدة إلى enum في لغة C؟
نعم: enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };. ويمكنك ضبط بعضها وترك البقية تتبع - فأي اسم بلا قيمة صريحة يواصل من الذي قبله، فـ enum { A = 10, B, C }; يعطي B = 11 وC = 12.
كيف تحوّل enum إلى سلسلة نصية في لغة C؟
لا تملك C طريقة مدمجة؛ فأسماء المعدودات غير موجودة أثناء التشغيل. والنهج المعتاد دالة صغيرة فيها switch تُعيد قيمة نصية حرفية لكل قيمة، أو مصفوفة من السلاسل مفهرسة بالتعداد. ونسخة switch أأمن لأن -Wall تحذّر حين لا يكون لمعدود جديد حالة.
ما حجم enum في لغة C؟
يحدّده التطبيق، لكنه عمليًا sizeof(int) - أي 4 بايتات على معظم الأنظمة. فالمترجم يختار نوعًا عدديًا قادرًا على حمل كل معدود. لا تفترض حجمًا معيّنًا عند ترتيب ملف ثنائي أو حزمة شبكة؛ استخدم نوعًا ثابت العرض من <stdint.h> هناك.