enum (enumeration) הוא טיפוס שהערכים שלו הם קבוצה קבועה של קבועים עם שמות: סטטוסים של הזמנה, ימים בשבוע, רמות לוג. מתחת לפני השטח כל שם הוא מספר שלם, אבל מערכת הטיפוסים מונעת בלבול בין OrderStatus לבין int רגיל או enum אחר.
הצהרה על enum ושימוש בו
כתבו את שמות האיברים בתוך סוגריים מסולסלים. כברירת מחדל הראשון הוא 0 וכל אחד שאחריו גדול באחד:
פלט:
Paid
On its way
True
2
ה-enum הוא טיפוס אמיתי: אי אפשר לקרוא בטעות למתודה שמקבלת OrderStatus עם 3 או עם LogLevel. enums הם טיפוסי ערך, ולכן הם אף פעם לא null ומושווים עם == לפי ערך.
ערכים מפורשים והטיפוס שבבסיס
אפשר להציב מספרים בעצמכם. זה חשוב בכל פעם שהמספר יוצא מהתוכנית שלכם (עמודה במסד נתונים, סטטוס HTTP, פורמט קובץ), כי אז מספור מחדש שובר נתונים מאוחסנים:
פלט:
404
Created
418
1
Byte
שני דברים לשים לב אליהם. המרה של int ל-enum אף פעם לא נכשלת: (HttpStatus)418 הוא ערך תקין שפשוט אין לו שם, והוא מודפס כמספר. והטיפוס שבבסיס יכול להיות כל טיפוס שלם (byte, short, long, ...), וזה חשוב רק בקוד שרגיש לאחסון; int היא ברירת המחדל וכמעט תמיד הבחירה הנכונה.
כשמוסיפים איברים בהמשך, הוסיפו אותם בסוף או תנו ערכים מפורשים. הכנסת Refunded בין Paid ל-Shipped משנה בשקט את המספר של כל איבר שאחריו.
enum למחרוזת
ToString() מחזירה את שם האיבר, וזה גם מה ש-Console.WriteLine ו-string interpolation משתמשים בו. מחרוזות פורמט משנות את הפלט:
פלט:
Warning
2
00000002
[Warning]
Error
Error
Needs attention
שמות איברים הם מזהים, ולכן הם לא יכולים להכיל רווחים והם לא מתורגמים. לטקסט שמוצג למשתמשים, מפו את הערכים בעצמכם, כמו ש-Label עושה, או עם Dictionary<LogLevel, string>. בחלק מבסיסי הקוד שמים attribute [Description("Needs attention")] על כל איבר וקוראים אותו עם reflection; הדף על reflection ו-attributes מראה איך החיפוש הזה עובד.
מחרוזת ל-enum: Parse ו-TryParse
Enum.Parse ממירה שם בחזרה לערך וזורקת ArgumentException אם שום דבר לא תואם. Enum.TryParse מחזירה false במקום, וזה מה שרוצים לכל קלט שאינו בשליטתכם:
פלט:
Large
Medium
Parse threw ArgumentException for Huge
small parsed=True value=Small defined=True
XL parsed=False value=Small defined=True
2 parsed=True value=Large defined=True
7 parsed=True value=7 defined=False
שתי השורות האחרונות הן המלכודת. שתי המתודות מקבלות מחרוזות מספריות, כך ש-"7" מתפענח בהצלחה ל-Size שאין לו שם. ו-TryParse שנכשלה מציבה בתוצאה 0, שכאן הוא Small, שנראה תקין. כשהטקסט מגיע מ-query string, מקובץ הגדרות או מטופס, בדקו תמיד גם את ערך ההחזרה וגם את Enum.IsDefined:
if (Enum.TryParse(input, true, out Size size) && Enum.IsDefined(typeof(Size), size))
{
// safe to use size
}
.NET Core 2.0 ואילך מוסיפות Enum.Parse<Size>("Large") גנרית שלא צריכה cast.
רשימת כל הערכים
Enum.GetValues מחזירה כל איבר, ממוין לפי ערך מספרי (בהשוואה כמספר ללא סימן, כך שאיברים שליליים באים אחרונים); Enum.GetNames מחזירה את השמות שלהם. כך ממלאים רשימה נפתחת או מאמתים מול כל האפשרויות:
פלט:
Free 0 EUR/month
Starter 9 EUR/month
Pro 29 EUR/month
Team 99 EUR/month
Free | Starter | Pro | Team
3 paid plans
Enum.GetValues(typeof(Plan)) מחזירה Array רגיל, ומכאן ה-Cast<Plan>() לפני LINQ. ב-.NET 5 ואילך, Enum.GetValues<Plan>() מחזירה ישירות Plan[] עם טיפוס.
Flags: שילוב ערכים
חלק מה-enums מתארים קבוצה של אפשרויות ולא בחירה אחת: הרשאות קבצים, הימים שבהם חנות פתוחה, ערוצי התראות. תנו לכל איבר ביט משלו (1, 2, 4, 8, ...), הוסיפו None = 0, וסמנו את ה-enum ב-[Flags]. אז ערכים משתלבים עם |:
פלט:
Read, Share
Editor, Share
True
False
Editor
3
Read, Delete
True
מה כל אופרטור עושה: | מדליק ביטים, & ~X מכבה אותם, ^ הופך אותם, ו-(value & X) != 0 או value.HasFlag(X) בודקים אותם. HasFlag(X) פירושו "כל הביטים של X דולקים", כך ש-HasFlag(None) נכון לכל ערך, ו-HasFlag(Editor) דורש גם Read וגם Write.
שימו לב לשורה השנייה: כששילוב עם שם מכסה חלק מהביטים הדולקים, ToString משתמשת בו, כך ש-Read | Write | Share מודפס כ-Editor, Share. זכרו את זה לפני שאתם מפענחים את הפלט של ToString עם משהו אחר מלבד Enum.Parse.
ה-attribute לא משנה את החשבון. הוא משנה את העיצוב: בלי [Flags], Read | Share מודפס כ-9, כי לאף איבר בודד אין את הערך הזה. איתו, ToString ו-Parse עובדות שתיהן עם הצורה המופרדת בפסיקים. האיברים עדיין חייבים להיות חזקות של שתיים; כתיבה של Read, Write, Delete עם המספור של ברירת המחדל (0, 1, 2) הופכת את Write | Delete ל-3, ערך חסר משמעות.
switch על enum
switch הוא הדרך הטבעית לפעול לפי enum. כללו ענף default, כי משתנה enum יכול להחזיק ערכים שאין להם שם:
switch (status)
{
case OrderStatus.Pending:
case OrderStatus.Paid:
return "Preparing";
case OrderStatus.Shipped:
return "On the way";
case OrderStatus.Delivered:
return "Delivered";
default:
return "Unknown";
}
מאז C# 8 ביטוי switch קצר יותר. בלי זרוע _ הקומפיילר מזהיר: CS8509 כשחסר איבר עם שם, ו-CS8524 כשכל השמות מטופלים אבל ערכים בלי שם כמו (OrderStatus)7 לא:
string text = status switch
{
OrderStatus.Pending or OrderStatus.Paid => "Preparing", // 'or' pattern: C# 9
OrderStatus.Shipped => "On the way",
OrderStatus.Delivered => "Delivered",
OrderStatus.Cancelled => "Cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status)),
};
ערכי ברירת מחדל וערכים לא מוגדרים
ערך ברירת המחדל של כל enum הוא 0, בין אם יש איבר עם הערך הזה ובין אם לא. שדות, איברי מערך ו-TryParse שנכשלה מפיקים אותו. תכננו בהתאם:
- הפכו את
0לאיבר "לא הוגדר" בעל משמעות (None,Unknown) ולא לבחירה אמיתית. אחרת שדה שלא אותחל נקרא בשקט כאפשרות האמיתית הראשונה. - אמתו מספרים שמגיעים מבחוץ עם
Enum.IsDefined. ב-enums עם[Flags],IsDefinedמחזירהfalseלשילובים שאין להם שם (Read | Share), אז בדקו את הביטים במקום:(value & ~Permissions.All) == 0עם איברAllשמכסה כל ביט.
טעויות נפוצות
- הסתמכות על
TryParseבלבד. מחרוזות מספריות מתפענחות, ופענוח שנכשל מחזיר0. הוסיפוEnum.IsDefined. - הסתמכות על מספור מרומז לערכים מאוחסנים. הכנסת איבר ממספרת מחדש את אלה שאחריו. הציבו ערכים מפורשים לכל enum שנשמר.
- Flags בלי חזקות של שתיים. המספור של ברירת המחדל (0, 1, 2, 3) גורם לחפיפה בביטים. השתמשו ב-1, 2, 4, 8, או ב-
1 << n. - הצגת
ToString()למשתמשים. שמות איברים הם מזהים בקוד. מפו ערכים לטקסט תצוגה. - בלי
defaultב-switch. enum יכול להחזיק ערכים מחוץ לאיברים עם השמות.
שאלות נפוצות
איך ממירים enum למחרוזת ב-C#?
קראו ל-ToString(): OrderStatus.Shipped.ToString() מחזירה "Shipped", ו-string interpolation עושה את אותו הדבר. ToString("D") נותנת את המספר במקום. לשם שידוע בזמן קומפילציה, nameof(OrderStatus.Shipped) הוא קבוע. לטקסט למשתמשים עם רווחים או תרגומים, מפו ערכים למחרוזות בעצמכם (עם switch או מילון) במקום להסתמך על שם האיבר.
איך ממירים מחרוזת ל-enum ב-C#?
השתמשו ב-Enum.TryParse<OrderStatus>(text, true, out var status), שמחזירה false במקום לזרוק כשהטקסט לא תואם אף איבר (ה-true הופך אותה ללא רגישה לאותיות גדולות וקטנות). Enum.Parse(typeof(OrderStatus), text) זורקת ArgumentException על קלט לא תקין. שתיהן מקבלות גם מחרוזות מספריות כמו "42", אז בדקו את התוצאה עם Enum.IsDefined כשהקלט מגיע ממשתמשים.
איך ממירים בין enum ל-int ב-C#?
המירו עם cast לכל כיוון: int code = (int)OrderStatus.Paid; ו-var status = (OrderStatus)2;. ההמרה מ-int אף פעם לא נכשלת, גם למספרים שאין להם איבר תואם; התוצאה היא ערך enum שמודפס כמספר. אמתו עם Enum.IsDefined(typeof(OrderStatus), value) כשהמספר מגיע מבחוץ.
איך עוברים בלולאה על כל הערכים של enum ב-C#?
foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus))) עובר על כל איבר לפי סדר הערכים המספריים. מאז .NET 5 יש גרסה גנרית, Enum.GetValues<OrderStatus>(), שלא צריכה cast. Enum.GetNames(typeof(OrderStatus)) מחזירה את השמות כמחרוזות.
מה [Flags] עושה על enum ב-C#?
הוא מסמן enum שהערכים שלו הם ביטים שנועדו להשתלב עם |, כמו Read | Write. תנו לכל איבר חזקה של שתיים (1, 2, 4, 8) ו-None = 0. ה-attribute גורם ל-ToString() להדפיס שילובים כ-"Read, Write" ומאפשר ל-Enum.Parse לקרוא את הפורמט הזה בחזרה. בדקו ביט עם HasFlag או עם (value & Permissions.Write) != 0.