קוד מלא במספרים חשופים הוא קוד שאף אחד לא יכול לקרוא. if (state == 2) לא אומר לכם כלום; if (state == STATE_RUNNING) אומר לכם הכול. enum הוא הדרך של C לתת שמות ראויים לקבוצה קטנה וקבועה של מספרים שלמים, ובניגוד לערימה של #define, הם מגיעים מקובצים בטיפוס אחד שהקומפיילר מכיר.
הגדרת enum
ההצהרה נותנת שם לתגית ומפרטת את הקבועים (enumerators) בתוך סוגריים מסולסלים:
שני דברים כדאי לשים לב אליהם מיד. ראשית, המספור מתחיל ב-0 וגדל באחד, כך ש-RED הוא 0, GREEN הוא 1, BLUE הוא 2. שנית, השמות של הקבועים גלויים בכל מקום בטווח העוטף: כותבים GREEN, לא Color.GREEN. הם חיים באותו מרחב שמות כמו המשתנים שלכם, ולכן לפי המוסכמה קוד C כותב אותם באותיות גדולות או מוסיף להם קידומת לפי הטיפוס (COLOR_RED) כדי למנוע התנגשויות.
הדפסת enum משתמשת ב-%d, כי ערך של enum הוא ערך שלם.
ערכים מפורשים
תנו לכל קבוע ערך קבוע מפורש, ואלה שאחריו ימשיכו משם:
הערכים לא חייבים להיות ייחודיים או מסודרים: שני קבועים יכולים לחלוק מספר, וזה לפעמים שימושי לכינויים (COLOR_DEFAULT = COLOR_BLACK). הם חייבים להיות קבועים שלמים בזמן קומפילציה, כך שאי אפשר לחשב אחד מהם ממשתנה.
ניב נפוץ משתמש בזה כדי לשמור את הספירה:
enum Suit {
CLUBS, DIAMONDS, HEARTS, SPADES,
SUIT_COUNT /* automatically 4 - stays correct if you add a suit */
};
SUIT_COUNT אינו צורה אמיתית של קלף; זה מספר הצורות, שהמספור האוטומטי שומר עליו מעודכן בחינם. לולאות וגדלי מערכים יכולים להשתמש בו והוא אף פעם לא מתיישן.
typedef enum
כמו ב-structs, השם המלא של הטיפוס ב-C הוא enum Color, ו-typedef משמיט את מילת המפתח:
מכיוון שהקבועים הם מספרים שלמים רגילים שמתחילים ב-0, הם משמשים גם כאינדקסים של מערכים: dx[DIR_EAST] עובד בלי שום המרה. זו התכונה השימושית ביותר של enum שמתחיל מאפס.
enum בתוך switch
השילוב של enum עם switch הוא המקום שבו הטיפוס מתחיל להחזיר את ההשקעה. קמפלו עם -Wall והקומפיילר יגיד לכם כששכחתם case.
הוסיפו STATE_FAILED ל-enum ו-GCC מזהיר: enumeration value 'STATE_FAILED' not handled in switch. האזהרה הזו היא הדבר הקרוב ביותר שיש ל-C לבדיקת שלמות, והיא סיבה טובה לכתוב את פקודות ה-switch האלה בלי default:: default משתיק את האזהרה ונותן למצבים חדשים לחמוק בלי טיפול. שימו את ברירת המחדל אחרי ה-switch במקום זאת, כמו ש-describe עושה.
המרת enum למחרוזת
C לא שומרת שמות בזמן ריצה; printf("%s", GREEN) לא יכול לעבוד כי GREEN הוא פשוט 1. שתי תבניות פותרות את זה.
גרסת ה-switch, שהקומפיילר בודק:
וגרסת טבלת החיפוש, קצרה יותר אבל לא נבדקת: אם המערך וה-enum מתרחקים זה מזה, שום דבר לא מזהיר:
האינדקסים הממוענים במערך ([RED] = "RED") לפחות שומרים את השמות מחוברים לתאים שלהם, כך ששינוי סדר ה-enum לא יכול לערבב את המחרוזות בשקט. בדיקת הגבולות חשובה כי שום דבר לא מונע ממי שקורא לפונקציה להעביר ערך מחוץ ל-enum; ראו את הסעיף הבא.
enum אינו אטום
משתנה enum הוא מספר שלם מתחת למכסה המנוע, ו-C לא שומרת על הטווח:
חשבון על קבועים עובד, השוואות עם int רגילים עובדות, והמרה לערך מחוץ לטווח מתקבלת. אז התייחסו ל-enum כאל תיעוד מצוין ורמז טוב לקומפיילר, לא כאל ערובה. בדקו כל ערך שמגיע מחוץ לתוכנית שלכם, מקובץ, מארגומנט או מהודעת רשת, לפני שאתם עושים עליו switch.
גודל ואחסון
ב-C17, כל קבוע חייב להיכנס ב-int: enum Huge { BIG = 3000000000 }; הוא הפרה של אילוץ שקומפיילר קפדני (-pedantic-errors) דוחה, למרות ש-gcc ו-clang מקבלים אותו כהרחבה במצב ברירת המחדל שלהם. לקבועים עצמם יש טיפוס int, בעוד שהגודל של טיפוס ה-enum תלוי מימוש (בפועל, int):
(C23 מסירה את ההגבלה: קבוע יכול לחרוג מהטווח של int, ואפשר אפילו לבחור את הטיפוס הבסיסי עם enum E : unsigned long { ... }. עד שהכלים שלכם יכוונו ל-C23, השאירו את הקבועים בתוך int.)
מכיוון שהבחירה המדויקת תלויה במימוש, לעולם אל תכתבו enum ישירות לקובץ בינארי או לחבילת רשת ותצפו שמכונה אחרת תקרא אותו בחזרה. המירו לטיפוס ברוחב קבוע כמו uint8_t או uint32_t מ-<stdint.h> בנקודת הגבול.
enum מול #define
שניהם נותנים שם למספר. ה-enum בדרך כלל עדיף:
enum | #define | |
|---|---|---|
| מי מטפל בו | הקומפיילר | קדם-המעבד |
| קיבוץ | ערכים קשורים חולקים טיפוס | כל קבוע עצמאי |
| מספור אוטומטי | כן | לא, ממספרים ביד |
| גלוי בדיבאגר | לרוב, לפי שם | לא, רק המספר |
אזהרת שלמות ב-switch | כן | לא |
| טווח | מכבד טווח של בלוק | טקסטואלי, עד #undef |
פנו ל-#define כשהקבוע אינו מספר שלם (מחרוזת, float) או כשהוא חייב להיות שמיש לקדם-המעבד עצמו, כמו בקומפילציה מותנית. לקבוצה של מצבים, דגלים או סוגים שלמים קשורים, השתמשו ב-enum.
מוסכמות שמות
enums מתנגשים בקלות כי השמות שלהם יושבים בטווח הרגיל. המוסכמות ששומרות על שפיות בבסיסי קוד C גדולים:
- הוסיפו לכל קבוע קידומת של הטיפוס:
COLOR_RED,STATE_IDLE,HTTP_OK.REDבלי קידומת בתוכנית גרפיקה ייפגש בסוף עם ה-REDשל מישהו אחר. - כתבו את הקבועים ב-SHOUT_CASE, כמו שקבועים אחרים נראים.
- תנו לטיפוס שם ביחיד ב-PascalCase או ב-
snake_case_t:Color,Stateאוcolor_t, באופן עקבי בכל הפרויקט. - הוסיפו זקיף
_COUNTבסוף כשתעברו בלולאה או תשתמשו ב-enum כאינדקס.
שאלות נפוצות
איך מגדירים enum ב-C?
enum Color { RED, GREEN, BLUE }; מגדיר טיפוס שהערכים שלו הם שלושת הקבועים עם השמות. כברירת מחדל הם ממוספרים מ-0, כך ש-RED הוא 0, GREEN הוא 1 ו-BLUE הוא 2. מצהירים על משתנה עם 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 שמחזירה מחרוזת מילולית לכל ערך, או מערך של מחרוזות שהאינדקס שלו הוא ה-enum. גרסת ה-switch בטוחה יותר, כי -Wall מזהיר כשלקבוע חדש אין case.
מה הגודל של enum ב-C?
תלוי מימוש, אבל בפועל sizeof(int), כלומר 4 בתים ברוב המערכות. הקומפיילר בוחר טיפוס שלם שיכול להכיל כל אחד מהקבועים. אל תניחו גודל מסוים כשאתם מתכננים מבנה של קובץ בינארי או חבילת רשת; השתמשו שם בטיפוס ברוחב קבוע מ-<stdint.h>.