13.2 — Wyliczenia bez zakresu

C++ zawiera wiele przydatnych podstawowych i złożonych typów danych (które przedstawiliśmy na lekcjach 4.1 -- Wprowadzenie do podstawowych typów danych i 12.1 — Wprowadzenie do złożonych typów danych). Jednak te typy nie zawsze są wystarczające do tego, co chcemy robić.

Załóżmy na przykład, że piszesz program, który musi śledzić, czy jabłko jest czerwone, żółte czy zielone, albo jakiego koloru jest koszula (z wstępnie ustawionej listy kolorów). Gdyby dostępne były tylko podstawowe typy, jak można to zrobić?

Możesz przechowywać kolor jako wartość całkowitą, używając pewnego rodzaju ukrytego mapowania (0 = czerwony, 1 = zielony, 2 = niebieski):

int main()
{
    int appleColor{ 0 }; // my apple is red
    int shirtColor{ 1 }; // my shirt is green

    return 0;
}

Ale nie jest to wcale intuicyjne i już omawialiśmy, dlaczego magiczne liczby są złe (5.2 -- Literały). Możemy pozbyć się magicznych liczb, używając stałych symbolicznych:

constexpr int red{ 0 };
constexpr int green{ 1 };
constexpr int blue{ 2 };

int main()
{
    int appleColor{ red };
    int shirtColor{ green };

    return 0;
}

Chociaż jest to trochę lepsze do odczytu, programiście nadal pozostaje do wywnioskowania, że appleColor i shirtColor (które są typu int) mają przechowywać jedną z wartości zdefiniowanych w zestawie stałych symbolicznych kolorów (które prawdopodobnie są zdefiniowane gdzie indziej, prawdopodobnie w osobnym pliku).

Możemy uczynić ten program trochę bardziej przejrzystym używając aliasu typu:

using Color = int; // define a type alias named Color

// The following color values should be used for a Color
constexpr Color red{ 0 };
constexpr Color green{ 1 };
constexpr Color blue{ 2 };

int main()
{
    Color appleColor{ red };
    Color shirtColor{ green };

    return 0;
}

Jesteśmy coraz bliżej. Ktoś czytający ten kod nadal musi zrozumieć, że te stałe symboliczne koloru mają być używane ze zmiennymi typu Color, ale przynajmniej typ ma teraz unikalną nazwę, więc ktoś wyszukujący Color będzie mógł znaleźć zestaw powiązanych stałych symbolicznych.

Jednakże Color to tylko alias dla int, nadal mamy problem, że nic nie wymusza prawidłowego użycia tych symbolicznych symboli kolorów stałe. Nadal możemy zrobić coś takiego:

Color eyeColor{ 8 }; // syntactically valid, semantically meaningless

Ponadto, jeśli debugujemy którąkolwiek z tych zmiennych w naszym debugerze, zobaczymy tylko całkowitą wartość koloru (np. 0), a nie znaczenie symboliczne (red), co może utrudnić stwierdzenie, czy nasz program jest poprawny.

Na szczęście możemy zrobić lepiej.

Jako inspirację rozważ typ bool . To, co sprawia, że bool jest szczególnie interesujące, to fakt, że ma on tylko dwie zdefiniowane wartości: true i false. Możemy użyć true lub false bezpośrednio (jako literałów) lub możemy utworzyć instancję obiektu bool i sprawić, że będzie on przechowywał jedną z tych wartości. Dodatkowo kompilator potrafi odróżnić bool od innych typów. Oznacza to, że możemy przeciążać funkcje i dostosowywać sposób, w jaki te funkcje zachowują się po przekazaniu. a bool wartości.

Gdybyśmy mieli możliwość zdefiniowania własnych typów niestandardowych, gdzie moglibyśmy zdefiniować zestaw nazwanych wartości powiązanych z tym typem, mielibyśmy doskonałe narzędzie do eleganckiego rozwiązania powyższego wyzwania…

Enumerations

An wyliczenie (zwany także typem wyliczeniowym lub wyliczeniem) to złożony typ danych, którego wartości są ograniczone do zbiór nazwanych stałych symbolicznych (zwanych enumeratorami).

C++ obsługuje dwa rodzaje wyliczeń: wyliczenia bez zakresu (które omówimy teraz) i wyliczenia z zakresem (które omówimy w dalszej części tego rozdziału).

Ponieważ wyliczenia są typami zdefiniowanymi w programie 13.1 -- Wprowadzenie do typów zdefiniowanych przez program (zdefiniowanych przez użytkownika), każde wyliczenie musi zostać w pełni zdefiniowane, zanim będziemy mogli z niego skorzystać (przejście do przodu deklaracja nie jest wystarczająca).

Wyliczenia bez zakresu

Wyliczenia bez zakresu są definiowane za pomocą enum słowo kluczowe.

Typy wyliczeniowe najlepiej uczyć się na przykładach, więc zdefiniujmy wyliczenie bez zakresu, które może przechowywać pewne wartości kolorów. Poniżej wyjaśnimy, jak to wszystko działa.

// Define a new unscoped enumeration named Color
enum Color
{
    // Here are the enumerators
    // These symbolic constants define all the possible values this type can hold
    // Each enumerator is separated by a comma, not a semicolon
    red,
    green,
    blue, // trailing comma optional but recommended
}; // the enum definition must end with a semicolon

int main()
{
    // Define a few variables of enumerated type Color
    Color apple { red };   // my apple is red
    Color shirt { green }; // my shirt is green
    Color cup { blue };    // my cup is blue

    Color socks { white }; // error: white is not an enumerator of Color
    Color hat { 2 };       // error: 2 is not an enumerator of Color

    return 0;
}

Zaczynamy nasz przykład od użycia słowa kluczowego enum , aby poinformować kompilator, że definiujemy wyliczenie bez zakresu, co oznacza, że nazwaliśmy Color.

W parze nawiasów klamrowych definiujemy moduły wyliczające dla typu Color : red, green, I blue. Te wyliczacze definiują konkretne wartości, do których ograniczony jest typ Color . Każdy moduł wyliczający musi być oddzielony przecinkiem (a nie średnikiem) — przecinek końcowy po ostatnim module wyliczającym jest opcjonalny, ale zalecany. spójność.

Najczęściej definiuje się jeden moduł wyliczający w jednej linii, lecz w prostych przypadkach (gdy liczba modułów wyliczających jest niewielka i nie są potrzebne żadne komentarze) można je wszystkie zdefiniować w jednej linii.

Definicja typu dla Color kończy się średnikiem. Teraz w pełni zdefiniowaliśmy, jaki typ wyliczeniowy Color Jest!

Wewnątrz main(), tworzymy instancję trzech zmiennych typu Color: apple jest inicjowany kolorem red, shirt jest inicjowany kolorem green, I cup jest inicjowany kolorem blue. Dla każdego z tych obiektów przydzielana jest pamięć. Należy zauważyć, że inicjator typu wyliczeniowego musi być jednym ze zdefiniowanych modułów wyliczających dla tego typu. Zmienne socks i hat powodują błędy kompilacji, ponieważ inicjatory white i 2 nie są rachmistrzami Color.

Liczniki są domyślnie constexpr.

Przypomnienie

Aby szybko podsumować nazewnictwo:

  • An wyliczenie lub typem wyliczeniowym jest typem zdefiniowanym przez program (np. Color).
  • An licznik jest konkretną nazwaną wartością należącą do wyliczenia (np. red).

Nazewnictwo wyliczeń i enumeratorów

Zgodnie z konwencją nazwy typów wyliczeniowych zaczynają się od dużej litery (jak wszystkie typy zdefiniowane w programie).

Ostrzeżenie

Wyliczenia nie muszą być nazwane, ale we współczesnym C++ należy unikać wyliczeń bez nazw.

Licznikom należy nadać nazwy. Niestety nie ma wspólnej konwencji nazewnictwa nazw modułów wyliczających. Typowe opcje obejmują rozpoczynanie od małych liter (np. czerwony), wielkie litery (Red), wielkie litery (RED), wszystkie wielkie litery z przedrostkiem (COLOR_RED) lub poprzedzone literą „k” i zakończone wielkimi literami (kColorRed).

Współczesne wytyczne dotyczące języka C++ zazwyczaj zalecają unikanie konwencji nazewnictwa wielkich liter, ponieważ wielkie litery są zwykle używane w makrach preprocesora i mogą powodować konflikty. Zalecamy również unikanie konwencji zaczynających się od dużej litery, ponieważ nazwy rozpoczynające się od dużej litery są zazwyczaj zarezerwowane dla typów zdefiniowanych w programie.

Najlepsza praktyka

Nazwij wyliczone typy zaczynając od dużej litery. Nazwij swoje liczniki zaczynając od małej litery.

Typy wyliczone są różnymi typami

Każdy utworzony typ wyliczeniowy jest uważany za odrębny typ, co oznacza, że ​​kompilator może odróżnić go od innych typów (w przeciwieństwie do typedefs lub aliasów typów, które są uważane za nieróżniące się od typów, które aliasują).

Ponieważ typy wyliczeniowe są różne, modułów wyliczających zdefiniowanych jako część jednego typu wyliczeniowego nie można używać z obiektami innego typu wyliczeniowego:

enum Pet
{
    cat,
    dog,
    pig,
    whale,
};

enum Color
{
    black,
    red,
    blue,
};

int main()
{
    Pet myPet { black }; // compile error: black is not an enumerator of Pet
    Color shirt { pig }; // compile error: pig is not an enumerator of Color

    return 0;
}

Prawdopodobnie i tak nie chciałeś koszulki ze świnią.

Wykorzystanie wyliczeń

Ponieważ moduły wyliczające mają charakter opisowy, są przydatne do ulepszania dokumentacji kodu i czytelności. Typów wyliczeniowych najlepiej używać, gdy masz niewielki zestaw powiązanych stałych, a obiekty muszą przechowywać tylko jedną z tych wartości na raz.

Powszechnie definiowane wyliczenia obejmują dni tygodnia, główne kierunki i kolory w talii kart:

enum DaysOfWeek
{
    sunday,
    monday,
    tuesday,
    wednesday,
    thursday,
    friday,
    saturday,
};

enum CardinalDirections
{
    north,
    east,
    south,
    west,
};

enum CardSuits
{
    clubs,
    diamonds,
    hearts,
    spades,
};

Czasami funkcje zwracają kod stanu wywołującemu, aby wskazać, czy funkcja została wykonana pomyślnie, czy napotkała błąd. Tradycyjnie do przedstawienia różnych możliwych kodów błędów używano małych liczb ujemnych. Na przykład:

int readFileContents()
{
    if (!openFile())
        return -1;
    if (!readFile())
        return -2;
    if (!parseFile())
        return -3;

    return 0; // success
}

Jednak używanie takich magicznych liczb nie jest zbyt opisowe. Lepszą metodą byłoby użycie typu wyliczeniowego:

enum FileReadResult
{
    readResultSuccess,
    readResultErrorFileOpen,
    readResultErrorFileRead,
    readResultErrorFileParse,
};

FileReadResult readFileContents()
{
    if (!openFile())
        return readResultErrorFileOpen;
    if (!readFile())
        return readResultErrorFileRead;
    if (!parseFile())
        return readResultErrorFileParse;

    return readResultSuccess;
}

Następnie osoba wywołująca może przetestować wartość zwracaną przez funkcję względem odpowiedniego modułu wyliczającego, co jest łatwiejsze do zrozumienia niż testowanie wyniku zwrotu dla określonej wartości całkowitej.

if (readFileContents() == readResultSuccess)
{
    // do something
}
else
{
    // print error message
}

Wyliczone typy można również dobrze wykorzystać w grach, aby zidentyfikować różne typy przedmiotów, potworów lub terenu. Zasadniczo wszystko, co jest małym zestawem powiązanych obiektów.

Na przykład:

enum ItemType
{
	sword,
	torch,
	potion,
};

int main()
{
	ItemType holding{ torch };

	return 0;
}

Wyliczone typy mogą również służyć jako przydatne parametry funkcji, gdy użytkownik musi dokonać wyboru pomiędzy dwiema lub więcej opcjami:

enum SortOrder
{
    alphabetical,
    alphabeticalReverse,
    numerical,
};

void sortData(SortOrder order)
{
    switch (order)
    {
        case alphabetical:
            // sort data in forwards alphabetical order
            break;
        case alphabeticalReverse:
            // sort data in backwards alphabetical order
            break;
        case numerical:
            // sort data numerically
            break;
    }
}

W wielu językach do definiowania wartości logicznych używa się wyliczeń — w końcu wartość logiczna jest po prostu wyliczeniem z dwoma modułami wyliczającymi: false i true! Jednakże w C++ true i false są definiowane jako słowa kluczowe, a nie moduły wyliczające.

Ponieważ wyliczenia są małe i niedrogie w kopiowaniu, dobrze jest przekazywać je (i zwracać) według wartości.

W lekcji O.1 — Flagi bitowe i manipulacja bitami za pomocą std::bitset omawialiśmy flagi bitowe. Wyliczeń można także używać do definiowania kolekcji powiązanych pozycji flag bitowych do użycia z std::bitset:

#include <bitset>
#include <iostream>

namespace Flags
{
    enum State
    {
        isHungry,
        isSad,
        isMad,
        isHappy,
        isLaughing,
        isAsleep,
        isDead,
        isCrying,
    };
}

int main()
{
    std::bitset<8> me{};
    me.set(Flags::isHappy);
    me.set(Flags::isLaughing);

    std::cout << std::boolalpha; // print bool as true/false

    // Query a few states (we use the any() function to see if any bits remain set)
    std::cout << "I am happy? " << me.test(Flags::isHappy) << '\n';
    std::cout << "I am laughing? " << me.test(Flags::isLaughing) << '\n';

    return 0;
}

Jeśli zastanawiasz się, w jaki sposób możemy użyć modułu wyliczającego, gdy oczekiwana jest wartość całkowita, moduły wyliczające bez zakresu zostaną niejawnie przekonwertowane na wartości całkowite. Zbadamy to szczegółowo w następnej lekcji (13.3 — Konwersje całkowe modułu wyliczającego bez zakresu).

Zakres wyliczeń bez zakresu

Wyliczenia bez zakresu są nazywane w taki sposób, ponieważ umieszczają nazwy modułów wyliczających w tym samym zakresie, co sama definicja wyliczenia (w przeciwieństwie do tworzenia nowego obszaru zasięgu, jak ma to miejsce w przypadku przestrzeni nazw).

Na przykład, biorąc pod uwagę ten program:

enum Color // this enum is defined in the global namespace
{
    red, // so red is put into the global namespace
    green,
    blue, 
};

int main()
{
    Color apple { red }; // my apple is red

    return 0;
}

Klasa Color wyliczenie jest zdefiniowane w zasięgu globalnym. Dlatego wszystkie nazwy wyliczeń (red, green, I blue) również wchodzą w zakres globalny. Zanieczyszcza to zakres globalny i znacznie zwiększa ryzyko kolizji nazw.

Jedną z konsekwencji jest to, że nazwa modułu wyliczającego nie może być używana w wielu wyliczeniach w tym samym zakresie:

enum Color
{
    red,
    green,
    blue, // blue is put into the global namespace
};

enum Feeling
{
    happy,
    tired,
    blue, // error: naming collision with the above blue
};

int main()
{
    Color apple { red }; // my apple is red
    Feeling me { happy }; // I'm happy right now (even though my program doesn't compile)

    return 0;
}

W powyższym przykładzie oba wyliczenia bez zakresu (Color i Feeling) umieszczają wyliczenia w tym samym zakresie. name blue do zakresu globalnego. Prowadzi to do kolizji nazewnictwa i późniejszego błędu kompilacji.

Wyliczenia bez zakresu udostępniają również nazwany region zakresu dla swoich modułów wyliczających (podobnie jak przestrzeń nazw działa jako nazwany region zakresu dla nazw zadeklarowanych wewnątrz). Oznacza to, że możemy uzyskać dostęp do modułów wyliczających wyliczenia bez zakresu w następujący sposób:

enum Color
{
    red,
    green,
    blue, // blue is put into the global namespace
};

int main()
{
    Color apple { red }; // okay, accessing enumerator from global namespace
    Color raspberry { Color::red }; // also okay, accessing enumerator from scope of Color

    return 0;
}

Najczęściej dostęp do modułów wyliczających bez zakresu uzyskuje się bez użycia metody). operator rozpoznawania zakresu.

Unikanie kolizji nazewnictwa modułów wyliczających

Istnieje sporo typowych sposobów zapobiegania kolizjom nazewnictwa modułów wyliczających bez zakresu.

Jedną z opcji jest poprzedzenie każdego modułu wyliczającego nazwą samego wyliczenia:

enum Color
{
    color_red,
    color_blue,
    color_green,
};

enum Feeling
{
    feeling_happy,
    feeling_tired,
    feeling_blue, // no longer has a naming collision with color_blue
};

int main()
{
    Color paint { color_blue };
    Feeling me { feeling_blue };

    return 0;
}

To nadal zanieczyszcza przestrzeń nazw, ale zmniejsza ryzyko kolizji nazewnictwa poprzez wydłużenie i zwiększenie nazw unikatowy.

Lepszą opcją jest umieszczenie typu wyliczeniowego w czymś, co zapewnia oddzielny obszar zasięgu, na przykład w przestrzeni nazw:

namespace Color
{
    // The names Color, red, blue, and green are defined inside namespace Color
    enum Color
    {
        red,
        green,
        blue,
    };
}

namespace Feeling
{
    enum Feeling
    {
        happy,
        tired,
        blue, // Feeling::blue doesn't collide with Color::blue
    };
}

int main()
{
    Color::Color paint{ Color::blue };
    Feeling::Feeling me{ Feeling::blue };

    return 0;
}

Oznacza to, że musimy teraz poprzedzić nasze nazwy wyliczeń i modułów wyliczających nazwą obszaru o zasięgu.

Dla zaawansowanych czytelników

Klasy zapewniają również obszar zasięgu i często umieszcza się typy wyliczeniowe powiązane z klasą w obszarze zasięgu klasy lekcja 15.3 -- Typy zagnieżdżone (typy elementów).

Powiązaną opcją jest użycie wyliczenia o określonym zakresie (które definiuje własny obszar zasięgu) Wkrótce omówimy wyliczenia o określonym zakresie (13.6 -- Wyliczenia o ograniczonym zakresie (klasy wyliczeniowe)).

Najlepsza praktyka

Preferuj umieszczanie wyliczeń w nazwanym obszarze zakresu (takim jak przestrzeń nazw lub klasa), aby moduły wyliczające nie zanieczyszczały globalnej przestrzeni nazw.

Alternatywnie, jeśli wyliczenie jest używane tylko w treści pojedynczej funkcji, wyliczenie powinno być zdefiniowane wewnątrz funkcji. Ogranicza to zakres wyliczenia i jego modułów wyliczających tylko do tej funkcji. Wyliczacze takiego wyliczenia będą śledzić wyliczenia o identycznych nazwach zdefiniowane w zakresie globalnym.

Porównanie z modułami wyliczającymi

Możemy użyć operatorów równości (operator== i operator!=), aby sprawdzić, czy wyliczenie ma wartość określonego modułu wyliczającego lub not.

#include <iostream>

enum Color
{
    red,
    green,
    blue,
};

int main()
{
    Color shirt{ blue };

    if (shirt == blue) // if the shirt is blue
        std::cout << "Your shirt is blue!";
    else
        std::cout << "Your shirt is not blue!";

    return 0;
}

W powyższym przykładzie używamy instrukcji if, aby sprawdzić, czy shirt jest równy modułowi wyliczającemu blue. Dzięki temu możemy warunkować zachowanie naszego programu w oparciu o moduł wyliczający, który zawiera nasze wyliczenie.

Wykorzystamy to częściej w następnej lekcji.

Czas quizu

Pytanie nr 1

Zdefiniuj nieograniczony typ wyliczeniowy o nazwie MonsterType, aby wybierać pomiędzy następującymi rasami potworów: ork, goblin, troll, ogr i szkielet.

Pokaż rozwiązanie

Pytanie nr 2

Umieść wyliczenie MonsterType w przestrzeni nazw. Następnie utwórz funkcję main() i utwórz instancję trolla. Program powinien się skompilować.

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
519 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze