איך פועלים המגוונים השונים של enum ב-TypeScript?


ל-TypeScript יש כמה דרכים שונות להגדיר enum:

enum Alpha { X, Y, Z }
const enum Beta { X, Y, Z }
declare enum Gamma { X, Y, Z }
declare const enum Delta { X, Y, Z }

אם אני מנסה להשתמש בערך מ-Gamma בזמן ריצה, אני מקבל שגיאה כי Gamma אינה מוגדרת, אבל זה לא המקרה עבור Delta או Alpha? מה המשמעות של const או declare בהגדרות כאן?

יש גם דגל קומפיילר preserveConstEnums -- איך זה משתלב עם אלה?

תשובה #1

ישנם ארבעה היבטים שונים של enums ב-TypeScript שצריך להיות מודעים להם. ראשית, כמה הגדרות:

"אובייקט חיפוש"

אם אתה כותב את ה־enum הזה:

enum Foo { X, Y }

TypeScript תייצא את האובייקט הבא:

var Foo;
(function (Foo) {
    Foo[Foo["X"] = 0] = "X";
    Foo[Foo["Y"] = 1] = "Y";
})(Foo || (Foo = {}));

אני אתייחס לזה כאל אובייקט החיפוש. המטרה שלו היא כפולה: לשמש כמיפוי מ-מחרוזות ל-מספרים, למשל כשכותבים Foo.X או Foo['X'], ולשמש כמיפוי מ-מספרים ל-מחרוזות. המיפוי ההפוך הזה שימושי לאבחון תקלות או למטרות רישום -- לעיתים קרובות יהיה לך את הערך 0 או 1 ותרצה לקבל את המחרוזת המתאימה "X" או "Y".

״declare״ או ״ambient״

ב-TypeScript, ניתן "להכריז" על דברים שהקומפילר צריך לדעת עליהם, אבל לא לייצר עבורם קוד בפועל. זה שימושי כאשר יש לך ספריות כמו jQuery שמגדירות אובייקט מסוים (למשל $) שברצונך לקבל עליו מידע טיפוסי, אך אינך זקוק ליצירת קוד על ידי הקומפילר. המפרט ומסמכים נוספים מתייחסים להכרזות שנעשו בדרך זו כהכרזות ב"סביבה סביבתית"; חשוב לציין שכל ההכרזות בקובץ .d.ts הן "סביבתיות" (או שמצריכות את המפעיל declare במפורש או שיש להן אותו באופן מרומז, בהתאם לסוג ההכרזה).

"הטמעה פנימית"

מסיבות של ביצועים וגודל קוד, לרוב עדיף שהפניה לחבר במניין תוחלף בערכה המספרית כאשר הוא מקומפלת:

enum Foo { X = 4 }
var y = Foo.X; // emits "var y = 4";

המפרט קורא לזה החלפה, אני אקרא לזה הטמעה כי זה נשמע יותר מגניב. לפעמים לא תרצה שחברי המניין יוטמעו, למשל בגלל שערך המניין עלול להשתנות בגרסה עתידית של ה-API.

אנאומים, איך הם פועלים?

בואו נפרק את זה לפי כל היבט של Enum. למרבה הצער, כל אחד מארבעת החלקים האלה יתייחס למונחים מכל השאר, אז סביר שתצטרכו לקרוא את כל זה יותר מפעם אחת.

חישוב לעומת לא חישוב (קבוע)

חברי Enum יכולים להיות מוחשבים או לא. התקן קורא לחברים שאינם מוחשבים קבועים, אבל אני אקרא להם לא מוחשבים כדי למנוע בלבול עם const.

חבר של רשימה מסומנת (enum) שהוא מחושב הוא כזה שערכו אינו ידוע בזמן הקומפילציה. הפניות לחברים מחושבים לא יכולות להיות מוטמעות, כמובן. לעומת זאת, חבר של רשימה מסומנת שאינו מחושב הוא כזה שערכו ידוע בזמן הקומפילציה. הפניות לחברים שאינם מחושבים מוטמעות תמיד.

אילו חברי enum מחושבים ואילו אינם מחושבים? קודם כל, כל חברי const enum הם קבועים (כלומר, אינם מחושבים), כפי שהשם מרמז. עבור enum שאינו const, זה תלוי אם אתה מסתכל על enum ambient (מוצהר) או על enum שאינו ambient.

חבר של declare enum (כלומר enum סביבתי) הוא קבוע אם ורק אם יש לו מאתחל. אחרת, הוא מחושב. שים לב שב-declare enum, מותרים רק מאתחלים מספריים. דוגמה:

declare enum Foo {
    X, // Computed
    Y = 2, // Non-computed
    Z, // Computed! Not 3! Careful!
    Q = 1 + 1 // Error
}

לבסוף, חברי אנומים שאינם מוצהרים כלא קבועים תמיד נחשבים כמחושבים. עם זאת, ביטויי האתחול שלהם מצטמצמים לערכים קבועים אם הם ניתנים לחישוב בזמן הקומפילציה. משמעות הדבר היא שחברי אנום לא קבועים אף פעם לא מושתלים בקוד (התנהגות זו השתנתה ב-TypeScript 1.5, ראו את "שינויים ב-TypeScript" בתחתית)

const מול non-const

קבוע

הצהרת Enum יכולה להכיל את המייצג const. אם Enum הוא const, כל ההפניות לחבריו מוטמעות בקוד.

const enum Foo { A = 4 }
var x = Foo.A; // emitted as "var x = 4;", always

מחרוזות const לא מייצרות אובייקט חיפוש כאשר הן מקומפלות. מסיבה זו, זהו שגיאה להתייחס ל-Foo בקוד שלמעלה אלא אם כן כחלק מהתייחסות לחבר. לא יהיה אובייקט Foo בזמן ריצה.

לא קבוע

אם הצהרה של enum אינה כוללת את המודיפייר const, הפניות לחבריה מוקמות במקום רק אם החבר אינו מחושב. enum שאינו const ואינו declare ייצור אובייקט חיפוש.

הצהרה (אמביינטית) מול לא הצהרה

הקדמה חשובה היא ש-declare ב-TypeScript יש משמעות מאוד ספציפית: העצם הזה קיים במקום אחר. זה לשם תיאור עצמים קיימים. שימוש ב-declare כדי להגדיר עצמים שאינם קיימים בפועל יכול להיות בעל השלכות רעות; נחקור את אלו בהמשך.

להצהיר

A declare enum לא יפיק אובייקט חיפוש. הפניות לחבריו מוטמעות אם אותם חברים מחושבים (ראו למעלה על מחושבים מול לא מחושבים).

חשוב לציין שסוגים אחרים של התייחסות ל-declare enum מותרים, לדוגמה הקוד הזה אינו טעות קומפילציה אבל ייפול בזמן ריצה:

// Note: Assume no other file has actually created a Foo var at runtime
declare enum Foo { Bar } 
var s = 'Bar';
var b = Foo[s]; // Fails

שגיאה זו נופלת תחת הקטגוריה של "אל תשקר לקומפיילר". אם אין לך אובייקט בשם Foo בזמן ריצה, אל תכתוב declare enum Foo!

A declare const enum אינו שונה מ-const enum, למעט במקרה של --preserveConstEnums (ראה למטה).

לא מוגדר

enum שאינו מוכרז מייצר אובייקט חיפוש אם הוא לא const. ההטמעה מתוארת למעלה.

--דגל preserveConstEnums

דגל זה משפיע בדיוק על דבר אחד: אנה-מוצהרים משתנים קבועים (const enums) יפיקו אובייקט חיפוש. ההטמעה לא מושפעת. זה שימושי לניפוי שגיאות.

שגיאות נפוצות

הטעות הנפוצה ביותר היא להשתמש ב-declare enum כאשר עדיף להשתמש ב-enum רגיל או const enum. הצורה הנפוצה היא זו:

module MyModule {
    // Claiming this enum exists with 'declare', but it doesn't...
    export declare enum Lies {
        Foo = 0,
        Bar = 1     
    }
    var x = Lies.Foo; // Depend on inlining
}

module SomeOtherCode {
    // x ends up as 'undefined' at runtime
    import x = MyModule.Lies;

    // Try to use lookup object, which ought to exist
    // runtime error, canot read property 0 of undefined
    console.log(x[x.Foo]);
}

זכור את הכלל הזהב: לעולם אל תכריז על דברים שלמעשה אינם קיימים. השתמש ב-const enum אם תמיד אתה רוצה שילוב במקום, או ב-enum אם אתה רוצה את אובייקט החיפוש.

שינויים ב-TypeScript

בין TypeScript 1.4 ל-1.5, היה שינוי בהתנהגות (ראה https://github.com/Microsoft/TypeScript/issues/2183) כדי לגרום לכל חברי האנומים שאינם declare ואינם const להיחשב כמחושבים, גם אם הם מאותחלים במפורש בערך מילולי. זה "פיצל את התינוק", כביכול, כאשר ההתנהגות של ההחדרה בגוף הקוד הפכה ליותר צפויה ומפרידה בצורה נקייה יותר את המושג של const enum מה enum הרגיל. לפני שינוי זה, חברים שאינם מחושבים של אנומים שאינם const הוכנסו בגוף הקוד באופן אגרסיבי יותר.

תשובה #2

יש כאן כמה דברים שמתרחשים. בוא נעבור מקרה במקרה.

אנום

enum Cheese { Brie, Cheddar }

ראשית, enum פשוט ישן. כאשר הוא מקומפל ל-JavaScript, זה יפיץ טבלת חיפוש.

טבלת החיפוש נראית ככה:

var Cheese;
(function (Cheese) {
    Cheese[Cheese["Brie"] = 0] = "Brie";
    Cheese[Cheese["Cheddar"] = 1] = "Cheddar";
})(Cheese || (Cheese = {}));

אז כשיש לך Cheese.Brie ב-TypeScript, זה מפיק Cheese.Brie ב-JavaScript שמוערך ל-0. Cheese[0] מפיק Cheese[0] ובפועל מוערך ל-"Brie".

מְנֻהֶה קְבוּעָה

const enum Bread { Rye, Wheat }

אין קוד שמופק בפועל עבור זה! הערכים שלו מוטמעים בקוד. הדברים הבאים מפיקים את הערך 0 עצמו ב-JavaScript:

Bread.Rye
Bread['Rye']

ההטבעה של const enums עשויה להיות שימושית מסיבות ביצועים.

אבל מה לגבי Bread[0]? זה יגרום לשגיאה בזמן ריצה והקומפיילר שלך צריך לתפוס את זה. אין טבלת חיפוש והקומפיילר לא ממזג כאן.

שים לב שבמקרה שלמעלה, הדגל --preserveConstEnums יגרום ל-Bread לפלוט טבלת חיפוש. עם זאת, הערכים שלה עדיין יהיו מוטבעים.

הכרזה על אנום

כמו בשימושים אחרים של declare, declare לא מייצר קוד ומצפה ממך שהקוד האמיתי מוגדר במקום אחר. זה לא יוצר טבלת חיפוש:

declare enum Wine { Red, Wine }

Wine.Red מפיק Wine.Red ב-JavaScript, אבל לא תהיה שום טבלת חיפוש של Wine להתייחס אליה ולכן זו תהיה שגיאה אלא אם הגדרת את זה במקום אחר.

להצהיר על enum קבוע

זה לא מפיק טבלת חיפוש:

declare const enum Fruit { Apple, Pear }

אבל זה כן מוטמע! Fruit.Apple מפיק 0. אבל שוב Fruit[0] יגרום לשגיאה בזמן ריצה כי זה לא מוטמע ואין טבלת חיפוש.

כתבתי את זה כאן ב-playground הזה. אני ממליץ לשחק שם כדי להבין איזה TypeScript מפיק איזה JavaScript.