Jak działają różne warianty wyliczenia w języku TypeScript?

116

W języku TypeScript można zdefiniować wyliczenie na kilka różnych sposobów:

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

Jeśli spróbuję użyć wartości z Gammaw czasie wykonywania, Gammapojawia się błąd, ponieważ nie została zdefiniowana, ale tak nie jest w przypadku Deltalub Alpha? Co oznacza constlub declareoznacza w deklaracjach tutaj?

Jest też preserveConstEnumsflaga kompilatora - jak to współdziała z nimi?

Ryan Cavanaugh
źródło
1
Właśnie napisałem artykuł o tym , chociaż ma to więcej wspólnego z porównaniem const do non const
enums

Odpowiedzi:

247

Istnieją cztery różne aspekty wyliczeń w języku TypeScript, o których należy pamiętać. Najpierw kilka definicji:

„obiekt wyszukiwania”

Jeśli napiszesz to wyliczenie:

enum Foo { X, Y }

TypeScript wyemituje następujący obiekt:

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

Nazywam to obiektem wyszukiwania . Jego cel jest dwojaki: służy jako odwzorowanie ciągów znaków na liczby , np. Podczas pisania Foo.Xlub Foo['X'], oraz służy jako odwzorowanie liczb na łańcuchy . To odwrotne odwzorowanie jest przydatne do debugowania lub rejestrowania - często będziesz mieć wartość 0lub 1i chcesz uzyskać odpowiedni ciąg "X"lub "Y".

„deklaracja” lub „ otoczenie

W języku TypeScript można „zadeklarować” rzeczy, o których kompilator powinien wiedzieć, ale w rzeczywistości nie emitować kodu. Jest to przydatne, gdy masz biblioteki, takie jak jQuery, które definiują obiekt (np. $), O którym chcesz wpisać informacje, ale nie potrzebujesz żadnego kodu utworzonego przez kompilator. Specyfikacja i inna dokumentacja odnosi się do deklaracji sporządzonych w ten sposób jako znajdujących się w kontekście „otoczenia”; należy zauważyć, że wszystkie deklaracje w .d.tspliku są „otoczone” (albo wymagają jawnego declaremodyfikatora, albo mają go niejawnie, w zależności od typu deklaracji).

„inlining”

Ze względu na wydajność i rozmiar kodu często preferowane jest zastąpienie odniesienia do elementu członkowskiego wyliczenia jego liczbowym odpowiednikiem podczas kompilacji:

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

Specyfikacja nazywa to zastąpienie , nazwę to inlining, ponieważ brzmi fajniej. Czasami nie chcesz, aby elementy członkowskie wyliczenia były wstawiane, na przykład ponieważ wartość wyliczenia może ulec zmianie w przyszłej wersji interfejsu API.


Enum, jak to działa?

Podzielmy to na każdy aspekt wyliczenia. Niestety, każda z tych czterech sekcji będzie odnosić się do terminów ze wszystkich pozostałych, więc prawdopodobnie będziesz musiał przeczytać tę całość więcej niż raz.

obliczone a nieobliczone (stała)

Składowe wyliczenia mogą być obliczane lub nie. Specyfikacja wywołuje nieobliczone elementy składowe stałymi , ale będę je nazywać nieobliczonymi, aby uniknąć pomyłki z const .

Obliczane członkiem enum jest ten, którego wartość nie jest znany w czasie kompilacji. Oczywiście nie można wstawiać odwołań do obliczonych członków. Odwrotnie, niż wyliczona element wyliczenia jest po których wartość jest znana w czasie kompilacji. Odwołania do nieobliczonych elementów członkowskich są zawsze wstawiane.

Które elementy członkowskie wyliczenia są obliczane, a które nie? Po pierwsze, wszystkie elementy constwyliczenia są stałe (tj. Nieobliczane), jak sama nazwa wskazuje. W przypadku wyliczenia innego niż stała, zależy to od tego, czy patrzysz na wyliczenie otoczenia (deklarowanie), czy wyliczenie inne niż otoczenia.

Element członkowski declare enum(tj. Wyliczenie otoczenia) jest stały wtedy i tylko wtedy, gdy ma inicjator. W przeciwnym razie jest obliczany. Zauważ, że w a declare enumdozwolone są tylko inicjatory numeryczne. Przykład:

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

Na koniec, elementy składowe niezadeklarowanych wyliczeń innych niż stałe są zawsze traktowane jako obliczone. Jednak ich wyrażenia inicjujące są zredukowane do stałych, jeśli są obliczalne w czasie kompilacji. Oznacza to, że nie będące stałymi składowymi wyliczenia nigdy nie są wstawiane (to zachowanie zostało zmienione w języku TypeScript 1.5, zobacz „Zmiany w języku TypeScript” u dołu)

const vs non-const

konst

Deklaracja wyliczenia może mieć constmodyfikator. Jeśli wyliczenie to const, wszystkie odwołania do jego członków są wstawione.

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

Wyliczenia const nie generują obiektu wyszukiwania podczas kompilacji. Z tego powodu błędem jest odwoływanie się Foodo powyższego kodu, z wyjątkiem odniesienia do elementu członkowskiego. Żaden Fooobiekt nie będzie obecny w czasie wykonywania.

non-const

Jeśli deklaracja wyliczenia nie ma constmodyfikatora, odwołania do jej elementów członkowskich są wstawiane tylko wtedy, gdy element członkowski nie jest obliczany. Wyliczenie inne niż stała, niezadeklarowane spowoduje utworzenie obiektu wyszukiwania.

deklarować (otoczenia) vs deklarować

Ważnym wstępem jest to, że declarew TypeScript ma bardzo specyficzne znaczenie: ten obiekt istnieje gdzie indziej . Służy do opisywania istniejących obiektów. Używanie declaredo definiowania obiektów, które w rzeczywistości nie istnieją, może mieć złe konsekwencje; zbadamy je później.

ogłosić

A declare enumnie wyemituje obiektu wyszukiwania. Odwołania do jego elementów członkowskich są wstawiane, jeśli te elementy członkowskie są obliczane (patrz powyżej na temat obliczonych i nieobliczonych).

Należy zauważyć, że dozwolone declare enum inne formy odwołań do a , np. Ten kod nie jest błędem kompilacji, ale zakończy się niepowodzeniem w czasie wykonywania:

// 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

Ten błąd należy do kategorii „Nie kłam kompilatorowi”. Jeśli nie masz obiektu nazwanego Foow czasie wykonywania, nie pisz declare enum Foo!

A declare const enumnie różni się od a const enum, z wyjątkiem przypadku --preserveConstEnums (patrz poniżej).

nie deklarować

Niezadeklarowane wyliczenie tworzy obiekt wyszukiwania, jeśli nie jest const. Inlining opisano powyżej.

--preserveConstEnums flaga

Ta flaga ma dokładnie jeden efekt: niezadeklarowane wyliczenia const będą emitować obiekt wyszukiwania. Nie ma to wpływu na podszewkę. Jest to przydatne do debugowania.


Powszechne błędy

Najczęstszym błędem jest użycie a, declare enumgdy zwykły enumlub const enumbyłby bardziej odpowiedni. Typowa forma jest taka:

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]);
}

Pamiętaj o złotej zasadzie: nigdy declarerzeczy, które nie istnieją . Użyj, const enumjeśli chcesz zawsze wstawiać lub enumjeśli chcesz, aby obiekt wyszukiwania.


Zmiany w TypeScript

Pomiędzy TypeScript 1.4 i 1.5 nastąpiła zmiana w zachowaniu (patrz https://github.com/Microsoft/TypeScript/issues/2183 ), aby wszystkie składowe niezadeklarowanych wyliczeń innych niż const były traktowane jako obliczone, nawet jeśli są jawnie zainicjowane literałem. To, że tak powiem, „rozłupać dziecko”, czyniąc zachowanie inlining bardziej przewidywalnym i wyraźniej oddzielając koncepcję const enumod zwykłej enum. Przed tą zmianą nieobliczane elementy składowe wyliczeń innych niż const były wstawiane bardziej agresywnie.

Ryan Cavanaugh
źródło
6
Naprawdę niesamowita odpowiedź. To wyjaśniło mi wiele rzeczy, nie tylko wyliczenia.
Clark
1
Chciałbym móc głosować na ciebie więcej niż raz ... Nie wiedziałem o tej przełomowej zmianie. W prawidłowej wersji semantycznej można to uznać za uderzenie w wersję główną: - /
mfeineis
Bardzo pomocne porównanie różnych enumtypów, dziękuję!
Marius Schulz,
@Ryan to bardzo pomocne, dzięki! Teraz potrzebujemy tylko Web Essentials 2015, aby wygenerować odpowiednie constdla zadeklarowanych typów wyliczeń.
styfle
19
Ta odpowiedź wydaje się być bardzo szczegółowa, wyjaśniając sytuację w 1.4, a na samym końcu mówi: „ale 1.5 zmieniło wszystko i teraz jest znacznie prostsze”. Zakładając, że dobrze rozumiem, co ta organizacja będzie się coraz bardziej niestosowne jak ta odpowiedź wiekiem: ja zalecamy umieszczenie prostszy, obecną sytuację najpierw , a dopiero potem mówiąc: „ale jeśli korzystasz 1.4 lub wcześniej, rzeczy są trochę bardziej skomplikowane ”.
KRyan,
33

Dzieje się tu kilka rzeczy. Chodźmy na każdy przypadek.

enum

enum Cheese { Brie, Cheddar }

Po pierwsze, zwykły stary wyliczenie. Po skompilowaniu do JavaScript wyemituje to tablicę przeglądową.

Tabela przeglądowa wygląda następująco:

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

Następnie, gdy masz Cheese.Briew TypeScript, emituje Cheese.Briew JavaScript, który szacuje na 0. Cheese[0]Emituje Cheese[0]i faktycznie ocenia do "Brie".

const enum

const enum Bread { Rye, Wheat }

W rzeczywistości żaden kod nie jest do tego emitowany! Jego wartości są zapisane. Poniższe emitują samą wartość 0 w JavaScript:

Bread.Rye
Bread['Rye']

const enums 'inlining może być przydatne ze względu na wydajność.

Ale co z tym Bread[0]? Spowoduje to błąd w czasie wykonywania i Twój kompilator powinien go złapać. Nie ma tabeli wyszukiwania, a kompilator nie jest tutaj wbudowany.

Zauważ, że w powyższym przypadku flaga --preserveConstEnums spowoduje, że Bread wyemituje tablicę przeglądową. Jednak jego wartości będą nadal zapisane.

zadeklaruj wyliczenie

Podobnie jak w przypadku innych zastosowań programu declare, declarenie emituje żadnego kodu i oczekuje, że właściwy kod zdefiniujesz w innym miejscu. To nie emituje tabeli przeglądowej:

declare enum Wine { Red, Wine }

Wine.Redemituje Wine.Redw JavaScript, ale nie będzie żadnej tabeli wyszukiwania Wine, do której można by się odwołać, więc jest to błąd, chyba że zdefiniowałeś go gdzie indziej.

deklaruj const enum

To nie emituje tabeli przeglądowej:

declare const enum Fruit { Apple, Pear }

Ale działa w linii! Fruit.Appleemituje 0. Ale znowu Fruit[0]wyświetli się błąd w czasie wykonywania, ponieważ nie jest wstawiony i nie ma tabeli odnośników.

Napisałem to na tym placu zabaw. Polecam grać tam, aby zrozumieć, który TypeScript emituje który JavaScript.

Kat
źródło
1
Zalecam zaktualizowanie tej odpowiedzi: Począwszy od Typescript 3.3.3, Bread[0]zgłasza błąd kompilatora: „Do elementu członkowskiego const enum można uzyskać dostęp tylko za pomocą literału ciągu”.
chharvey
1
Hm ... czy to się różni od tego, co mówi odpowiedź? „Ale co z Bread [0]? To wyświetli się błąd w czasie wykonywania i Twój kompilator powinien go złapać. Nie ma tabeli wyszukiwania, a kompilator nie jest tutaj wbudowany”.
Kat