Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 50 z 58

Enum i struct — nazwane stany oraz proste wartości

Zamiast pamiętać, że 2 oznacza oddany sprzęt, użyjemy nazwy. Poznamy też mały typ reprezentujący punkt planszy.

C# enum Flags Enum.TryParse struct typ wartościowy 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, dlaczego liczba i napis są złym zapisem stanu, i wskażesz, co się psuje w każdym z nich.
  • Zadeklarujesz typ wyliczeniowy, także z własnymi numerami pozycji i mniejszym typem w tle.
  • Zamienisz wartość wyliczeniową na liczbę i tekst oraz w drugą stronę, ze sprawdzeniem poprawności.
  • Napiszesz switch po wartości wyliczeniowej i uzasadnisz obecność gałęzi _.
  • Przejdziesz wszystkie pozycje pętlą i wczytasz wartość od użytkownika przez Enum.TryParse.
  • Zbudujesz zestaw uprawnień na enum z [Flags] i sprawdzisz je metodą HasFlag.
  • Zadeklarujesz prostą strukturę i wskażesz trzy rzeczy, których struct nie może.
  • Wyjaśnisz, dlaczego przypisanie struktury tworzy kopię, a przypisanie obiektu klasy — nie.
TEORIA

Liczba albo napis — i oba źle

Wypożyczalnia sprzętu z lekcji 48 musi pamiętać, w jakim stanie jest każda pozycja: dostępna, wypożyczona, w naprawie albo wycofana. Pierwszy pomysł to liczba:

stan jako liczba
class Sprzet
{
    public string Nazwa { get; }
    public int Stan { get; set; }      // 0 - dostepny, 1 - wypozyczony, 2 - w naprawie...

    public Sprzet(string nazwa)
    {
        Nazwa = nazwa;
        Stan = 0;
    }
}

Sprzet s = new Sprzet("Laptop Dell");
s.Stan = 2;

if (s.Stan == 1)
{
    Console.WriteLine("nie mozna wypozyczyc");
}

Program działa i jest nieczytelny. Za trzy tygodnie nikt — Ty też nie — nie będzie pamiętał, czy 2 oznaczało naprawę, czy wycofanie. Komentarz przy deklaracji pomaga, ale nie pojawia się w tych trzydziestu miejscach, gdzie potem porównujesz liczby.

Drugi pomysł to napis:

stan jako napis
public string Stan { get; set; }

s.Stan = "w naprawie";

if (s.Stan == "wnaprawie")      // literowka - warunek nigdy nie bedzie prawdziwy
{
    // ...
}

Czytelniej, ale teraz łatwo o literówkę, a kompilator nie ma jak jej zauważyć — "wnaprawie" to poprawny napis. Do tego "W naprawie", "w naprawie" i "W NAPRAWIE" to trzy różne wartości.

Zapis stanuCzytelny?Kompilator pilnuje literówek?Da się wypisać wprost?
liczba intnienienie
napis stringtaknietak
const inttakczęściowonie
typ wyliczeniowytaktaktak

Czego szukamy

Chcemy typu, który ma z góry ustaloną, zamkniętą listę wartości — takich, że kompilator odrzuci wszystko poza nią, a wypisanie da nazwę, nie liczbę. W C# nazywa się to typem wyliczeniowym, słowo kluczowe enum.

TEORIA

Zamknięta lista wartości

Deklaracja jest krótsza od wszystkiego, co pisałeś w tym bloku:

definicja typu
enum Stan
{
    Dostepny,
    Wypozyczony,
    WNaprawie,
    Wycofany
}

Od tej chwili Stan jest pełnoprawnym typem, takim jak int czy string:

Program.cs
Stan s = Stan.WNaprawie;

Console.WriteLine(s);                        // WNaprawie - nazwa, nie liczba
Console.WriteLine($"stan: {s}");             // dziala w interpolacji

if (s == Stan.Wypozyczony)
{
    Console.WriteLine("ktos ma to u siebie");
}

// s = 5;                                    // BLAD CS0029
// s = Stan.Zniszczony;                      // BLAD CS0117 - nie ma takiej nazwy
wynik w konsoli
WNaprawie
stan: WNaprawie

Dwa zakomentowane wiersze to dokładnie to, czego brakowało w wersji z liczbą i napisem: kompilator odrzuca każdą wartość, której nie ma na liście, i wyłapuje literówkę w nazwie stanu.

Za nazwami stoją liczby

Każda pozycja w enum ma wartość liczbową. Domyślnie pierwsza dostaje 0 i dalej po jednym:

Program.cs
Console.WriteLine((int)Stan.Dostepny);      // 0
Console.WriteLine((int)Stan.Wypozyczony);   // 1
Console.WriteLine((int)Stan.WNaprawie);     // 2
Console.WriteLine((int)Stan.Wycofany);      // 3

Numery można też podać samemu — przydaje się, gdy liczby mają znaczenie poza programem, na przykład w pliku albo w bazie:

definicje typów
enum Priorytet
{
    Niski = 1,
    Sredni = 5,
    Wysoki = 10
}

enum KodBledu : byte      // mniejszy typ w tle, gdy wartosci sa male
{
    Brak = 0,
    BrakPliku = 4,
    BrakDostepu = 5
}

Pozycja o wartości 0 jest szczególna

Pole typu enum w nowym obiekcie ma wartość 0 — nawet jeśli żadna pozycja nie ma takiej liczby. Dlatego pierwsza pozycja powinna być tą, która ma sens jako stan początkowy: Dostepny, Nieznany, Brak. W typie Priorytet powyżej brak pozycji o wartości 0, więc Priorytet p; w polu klasy da wartość, która nie ma nazwy i wypisze się jako 0.

Zamiana w obie strony

Program.cs
Stan s = Stan.WNaprawie;

int liczba = (int)s;                  // enum  -> int
Stan znowu = (Stan)liczba;            // int   -> enum
string nazwa = s.ToString();          // enum  -> tekst

Console.WriteLine($"{liczba} / {znowu} / {nazwa}");

Stan dziwny = (Stan)99;               // UWAGA: to sie kompiluje i dziala!
Console.WriteLine(dziwny);            // 99 - nie ma takiej nazwy
Console.WriteLine(Enum.IsDefined(dziwny));   // False
wynik w konsoli
2 / WNaprawie / WNaprawie
99
False

Jedna dziura w szczelności

Rzutowanie (Stan)99 nie jest błędem — kompilator zakłada, że wiesz, co robisz. Dlatego przy wartościach pochodzących z zewnątrz (plik, konsola, baza) zawsze sprawdzaj je metodą Enum.IsDefined albo wczytuj przez Enum.TryParse. Wewnątrz programu, gdzie przypisujesz tylko nazwy, problemu nie ma.

TEORIA

Pole klasy, switch i wczytywanie

enum najczęściej trafia do pola klasy. Wtedy obiekt pilnuje swoich stanów sam:

definicja typu
class Sprzet
{
    public string Nazwa { get; }
    public Stan Stan { get; private set; }

    public Sprzet(string nazwa)
    {
        Nazwa = nazwa;
        Stan = Stan.Dostepny;
    }

    public bool Wypozycz(string komu)
    {
        if (Stan != Stan.Dostepny)
        {
            Console.WriteLine($"{Nazwa}: nie mozna wypozyczyc, stan {Stan}");
            return false;
        }
        Stan = Stan.Wypozyczony;
        Console.WriteLine($"{Nazwa}: wypozyczono dla {komu}");
        return true;
    }

    public void OddajDoNaprawy()
    {
        Stan = Stan.WNaprawie;
    }
}

switch po wartości wyliczeniowej

To najczęstsze użycie enum — czytelne rozgałęzienie bez ani jednej liczby:

Program.cs
static string Komunikat(Stan s)
{
    return s switch
    {
        Stan.Dostepny    => "mozna wypozyczyc",
        Stan.Wypozyczony => "u kogos w domu",
        Stan.WNaprawie   => "w serwisie",
        Stan.Wycofany    => "zdjete z inwentarza",
        _                => "stan nieznany"
    };
}

Po co gałąź _

Kompilator nie wymaga wymienienia wszystkich pozycji, bo — jak widziałeś — do zmiennej typu enum da się wcisnąć liczbę spoza listy. Dlatego gałąź _ zostaw zawsze. Zyskujesz też drugą rzecz: gdy dopiszesz nową pozycję do enum i zapomnisz o tym switch, program nie wysypie się wyjątkiem, tylko zwróci „stan nieznany”.

Przejście po wszystkich pozycjach

Program.cs
foreach (Stan s in Enum.GetValues<Stan>())
{
    Console.WriteLine($"{(int)s,2}  {s,-12}  {Komunikat(s)}");
}
wynik w konsoli
 0  Dostepny      mozna wypozyczyc
 1  Wypozyczony   u kogos w domu
 2  WNaprawie     w serwisie
 3  Wycofany      zdjete z inwentarza

Wczytanie od użytkownika

Convert.ToInt32 tu nie wystarczy — potrzebna jest metoda, która rozumie nazwy:

Program.cs
Console.Write("Podaj stan (Dostepny/Wypozyczony/WNaprawie/Wycofany): ");
string? wpisane = Console.ReadLine();

if (Enum.TryParse<Stan>(wpisane, true, out Stan wynikStanu))
{
    Console.WriteLine($"rozpoznano: {wynikStanu}");
}
else
{
    Console.WriteLine("nie rozpoznano stanu");
}

Drugi argument true znaczy „nie zwracaj uwagi na wielkość liter”, więc wnaprawie też zadziała. Metoda zwraca bool — dokładnie tak jak int.TryParse z lekcji 12.

Pułapka TryParse

Enum.TryParse<Stan>("7", out s) zwraca true i ustawia (Stan)7, choć nie ma takiej pozycji — bo metoda przyjmuje także zapis liczbowy. Jeśli ma to znaczenie, dodaj po udanym rozpoznaniu sprawdzenie Enum.IsDefined(s).

TEORIA

Zestaw wartości w jednej zmiennej

Zwykły enum trzyma jedną wartość z listy. Czasem potrzebny jest zestaw: uczeń może mieć dostęp do czytania i zapisu jednocześnie. Do tego służy enum z atrybutem [Flags] i wartościami będącymi kolejnymi potęgami dwójki.

definicja typu
[Flags]
enum Dostep
{
    Brak      = 0,
    Czytanie  = 1,      // 0001
    Zapis     = 2,      // 0010
    Usuwanie  = 4,      // 0100
    Drukowanie = 8      // 1000
}

Zestaw składa się operatorem |, a sprawdza metodą HasFlag — operatory bitowe znasz z lekcji 10:

Program.cs
Dostep uczen = Dostep.Czytanie | Dostep.Drukowanie;

Console.WriteLine(uczen);                          // Czytanie, Drukowanie
Console.WriteLine((int)uczen);                     // 9  (1 + 8)
Console.WriteLine(uczen.HasFlag(Dostep.Zapis));    // False
Console.WriteLine(uczen.HasFlag(Dostep.Czytanie)); // True

uczen = uczen | Dostep.Zapis;                      // dodanie uprawnienia
Console.WriteLine(uczen);                          // Czytanie, Zapis, Drukowanie

uczen = uczen & ~Dostep.Drukowanie;                // odebranie uprawnienia
Console.WriteLine(uczen);                          // Czytanie, Zapis
wynik w konsoli
Czytanie, Drukowanie
9
False
True
Czytanie, Zapis, Drukowanie
Czytanie, Zapis
OperacjaZapisZnaczenie
dodaj uprawnienied = d | Dostep.Zapis;zapala bit
odbierz uprawnienied = d & ~Dostep.Zapis;gasi bit
sprawdź uprawnienied.HasFlag(Dostep.Zapis)zwraca bool
przełącz uprawnienied = d ^ Dostep.Zapis;odwraca bit
brak uprawnieńd == Dostep.Brakporównanie z pozycją o wartości 0

Wartości muszą być potęgami dwójki

Gdyby Usuwanie miało wartość 3 zamiast 4, zestaw Czytanie | Zapis (1 + 2 = 3) byłby nieodróżnialny od Usuwanie. Dlatego w enum z [Flags] kolejne pozycje to 1, 2, 4, 8, 16, 32… Sam [Flags] tego nie pilnuje — wpływa tylko na to, jak działa ToString().

TEORIA

Mała wartość zamiast obiektu

Drugi typ tej lekcji rozwiązuje inny problem. Pozycja pionka na planszy to dwie liczby — X i Y. Klasa by tu działała, ale jest jak młot na muchę: obiekt klasy zawsze leży osobno w pamięci, a zmienna trzyma tylko odwołanie do niego (lekcja 43).

Do małych, prostych wartości C# ma struct. Deklaracja wygląda prawie jak klasa:

definicja typu
struct Punkt
{
    public int X { get; }
    public int Y { get; }

    public Punkt(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}
Program.cs
Punkt a = new Punkt(3, 4);
Punkt b = a;                   // KOPIA calej wartosci, nie drugie odwolanie

Console.WriteLine(a);          // (3, 4)
Console.WriteLine(b);          // (3, 4)
Console.WriteLine(a.Equals(b));// True - porownuje pola, nie adresy

Punkt zero = default;          // (0, 0) - pola dostaja wartosci domyslne
Console.WriteLine(zero);
wynik w konsoli
(3, 4)
(3, 4)
True
(0, 0)

To najważniejsza różnica i wrócimy do niej dokładnie w lekcji 50: struct jest typem wartościowym. Przypisanie kopiuje całą zawartość, tak jak przy int. W klasie przypisanie dałoby drugą nazwę na ten sam obiekt.

Czego struct nie może

Elementclassstruct
dziedziczenie po innym typietaknie (CS0527 — tylko interfejsy)
może być klasą bazowątaknie
implementowanie interfejsówtaktak
virtual i abstracttaknie
przesłonięcie ToString()taktak
konstruktor bez parametrówtaktak, od C# 10 — ale default go pomija
wartość nulltaknie — potrzebne Punkt?
zachowanie przy przypisaniudrugie odwołaniekopia

Znasz już wiele struktur

Prawie wszystko, czym posługujesz się od pierwszych lekcji, to struktury: int, double, bool, char, decimal, DateTime. Dlatego int a = b; kopiuje liczbę, a nie tworzy drugiej nazwy na tę samą. Wyjątkiem jest string — to klasa, choć zachowuje się podobnie dzięki niezmienności (lekcja 08).

Kiedy struct, a kiedy class

Reguła praktyczna: struct tylko wtedy, gdy typ opisuje jedną małą wartość (do kilkunastu bajtów), jest niezmienny i ma sens porównywania po zawartości. Pozycja, kolor, zakres dat, kwota z walutą. Wszystko inne — class. Jeśli się wahasz, wybierz class: to jest właściwa domyślna odpowiedź. Lekcja 50 pokazuje, co dokładnie idzie nie tak przy złym wyborze.

PRZYKŁAD

Magazyn wypożyczalni

Magazyn wypożyczalni: trzy typy wyliczeniowe (zwykły, z własnymi numerami i z [Flags]), jedna struktura na numer półki i klasa, która ich używa.

Program.cs — pełny plik
// Program.cs - pelny plik
List<Sprzet> magazyn = new List<Sprzet>
{
    new Sprzet("Laptop Dell", Kategoria.Komputer, new Punkt(1, 2)),
    new Sprzet("Projektor Epson", Kategoria.Prezentacja, new Punkt(1, 5)),
    new Sprzet("Drukarka HP", Kategoria.Komputer, new Punkt(3, 1)),
    new Sprzet("Mikroskop", Kategoria.Laboratorium, new Punkt(4, 4))
};

Console.WriteLine("=== stan magazynu ===");
Pokaz(magazyn);

Console.WriteLine();
Console.WriteLine("=== operacje ===");
magazyn[0].Wypozycz("Kasia Nowak");
magazyn[0].Wypozycz("Marek Lis");          // juz wypozyczony
magazyn[1].OddajDoNaprawy();
magazyn[3].Wycofaj();

Console.WriteLine();
Console.WriteLine("=== stan po operacjach ===");
Pokaz(magazyn);

Console.WriteLine();
Console.WriteLine("=== zestawienie po stanach ===");
foreach (Stan s in Enum.GetValues<Stan>())
{
    int ile = 0;
    foreach (Sprzet sp in magazyn)
    {
        if (sp.StanSprzetu == s)
        {
            ile++;
        }
    }
    Console.WriteLine($"{s,-12} {ile}  ({Komunikat(s)})");
}

Console.WriteLine();
Console.WriteLine("=== uprawnienia ===");
Dostep uczen = Dostep.Czytanie;
Dostep nauczyciel = Dostep.Czytanie | Dostep.Zapis | Dostep.Usuwanie;

Sprawdz("uczen", uczen);
Sprawdz("nauczyciel", nauczyciel);

Console.WriteLine();
Console.WriteLine("=== polki sa kopiowane, nie wspoldzielone ===");
Punkt polka = magazyn[0].Polka;
Punkt kopia = polka;
Console.WriteLine($"polka {polka}, kopia {kopia}, rowne: {polka.Equals(kopia)}");

static void Pokaz(List<Sprzet> lista)
{
    foreach (Sprzet s in lista)
    {
        Console.WriteLine($"{s.Nazwa,-18} {s.KategoriaSprzetu,-13} polka {s.Polka,-8} {s.StanSprzetu}");
    }
}

static string Komunikat(Stan s)
{
    return s switch
    {
        Stan.Dostepny    => "mozna wypozyczyc",
        Stan.Wypozyczony => "u kogos w domu",
        Stan.WNaprawie   => "w serwisie",
        Stan.Wycofany    => "zdjete z inwentarza",
        _                => "stan nieznany"
    };
}

static void Sprawdz(string kto, Dostep d)
{
    Console.WriteLine($"{kto,-11} {d,-30} zapis: {d.HasFlag(Dostep.Zapis)}");
}


enum Stan
{
    Dostepny,
    Wypozyczony,
    WNaprawie,
    Wycofany
}

enum Kategoria
{
    Inne = 0,
    Komputer = 1,
    Prezentacja = 2,
    Laboratorium = 3
}

[Flags]
enum Dostep
{
    Brak = 0,
    Czytanie = 1,
    Zapis = 2,
    Usuwanie = 4
}

struct Punkt
{
    public int Rzad { get; }
    public int Miejsce { get; }

    public Punkt(int rzad, int miejsce)
    {
        Rzad = rzad;
        Miejsce = miejsce;
    }

    public override string ToString()
    {
        return $"{Rzad}/{Miejsce}";
    }
}

class Sprzet
{
    public string Nazwa { get; }
    public Kategoria KategoriaSprzetu { get; }
    public Punkt Polka { get; }
    public Stan StanSprzetu { get; private set; }

    public Sprzet(string nazwa, Kategoria kategoria, Punkt polka)
    {
        Nazwa = nazwa;
        KategoriaSprzetu = kategoria;
        Polka = polka;
        StanSprzetu = Stan.Dostepny;
    }

    public bool Wypozycz(string komu)
    {
        if (StanSprzetu != Stan.Dostepny)
        {
            Console.WriteLine($"{Nazwa}: odmowa, stan {StanSprzetu}");
            return false;
        }
        StanSprzetu = Stan.Wypozyczony;
        Console.WriteLine($"{Nazwa}: wypozyczono dla {komu}");
        return true;
    }

    public void OddajDoNaprawy()
    {
        StanSprzetu = Stan.WNaprawie;
        Console.WriteLine($"{Nazwa}: oddane do serwisu");
    }

    public void Wycofaj()
    {
        StanSprzetu = Stan.Wycofany;
        Console.WriteLine($"{Nazwa}: wycofane z inwentarza");
    }
}
wynik w konsoli
=== stan magazynu ===
Laptop Dell        Komputer      polka 1/2      Dostepny
Projektor Epson    Prezentacja   polka 1/5      Dostepny
Drukarka HP        Komputer      polka 3/1      Dostepny
Mikroskop          Laboratorium  polka 4/4      Dostepny

=== operacje ===
Laptop Dell: wypozyczono dla Kasia Nowak
Laptop Dell: odmowa, stan Wypozyczony
Projektor Epson: oddane do serwisu
Mikroskop: wycofane z inwentarza

=== stan po operacjach ===
Laptop Dell        Komputer      polka 1/2      Wypozyczony
Projektor Epson    Prezentacja   polka 1/5      WNaprawie
Drukarka HP        Komputer      polka 3/1      Dostepny
Mikroskop          Laboratorium  polka 4/4      Wycofany

=== zestawienie po stanach ===
Dostepny     1  (mozna wypozyczyc)
Wypozyczony  1  (u kogos w domu)
WNaprawie    1  (w serwisie)
Wycofany     1  (zdjete z inwentarza)

=== uprawnienia ===
uczen       Czytanie                       zapis: False
nauczyciel  Czytanie, Zapis, Usuwanie      zapis: True

=== polki sa kopiowane, nie wspoldzielone ===
polka 1/2, kopia 1/2, rowne: True

Co dzieje się po kolei

  • Pole StanSprzetu ma private set, więc stan zmienia się wyłącznie metodami. Dzięki temu nikt z zewnątrz nie ustawi sprzętowi stanu Wypozyczony, pomijając sprawdzenie.
  • Drugie wywołanie Wypozycz na tym samym laptopie kończy się odmową, bo warunek porównuje wartości wyliczeniowe, a nie liczby — i czyta się jak zdanie.
  • Wypisanie s.StanSprzetu daje nazwę stanu, bez żadnej tabeli tłumaczącej numery.
  • Enum.GetValues<Stan>() zwraca wszystkie cztery pozycje, więc zestawienie obejmuje też stany, których nikt nie użył — przy liczeniu ręcznym łatwo takie pominąć.
  • Kategoria ma jawnie nadane numery i pozycję Inne = 0. Gdyby kategoria pochodziła z pliku, wartość nierozpoznana trafiłaby właśnie tam.
  • Dostep z [Flags] trzyma kilka uprawnień w jednej zmiennej. ToString() wypisuje je po przecinku, a HasFlag sprawdza jedno z nich.
  • Punkt jest strukturą: Punkt kopia = polka; tworzy niezależną kopię, a Equals porównuje pola, nie adresy — dlatego wynik jest True. Przy klasie dostałbyś False, o ile nie przesłoniłbyś Equals.

Sprawdź trzy rzeczy

  1. Dopisz do enum Stan pozycję Zgubiony i uruchom program bez zmiany metody Komunikat. Zestawienie pokaże nowy stan z opisem „stan nieznany” — to zaleta gałęzi _. Potem dopisz gałąź i porównaj.
  2. Zmień struct Punkt na class Punkt. Ostatni wiersz wypisze rowne: False, bo Equals klasy porównuje odwołania. Nic innego się nie zepsuje — na razie.
  3. Dopisz Console.WriteLine((Stan)7);. Zobaczysz 7 zamiast nazwy — to ta jedna dziura w szczelności typu wyliczeniowego.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
enum Nazwa { A, B }typ o zamkniętej liście wartościPozycje numerowane od 0. Kompilator odrzuca wartości spoza listy.
A = 5 w enumwłasny numer pozycjiGdy liczby mają znaczenie poza programem.
enum Nazwa : bytemniejszy typ w tleDomyślnie int.
(int)wartoscwyliczenie na liczbęZawsze się udaje.
(Stan)liczbaliczba na wyliczenieNie sprawdza, czy taka pozycja istnieje.
.ToString()nazwa pozycjiWywoływana też przez WriteLine i interpolację.
Enum.GetValues<T>()tablica wszystkich pozycjiDo pętli po stanach i do menu.
Enum.TryParse<T>rozpoznanie z tekstuDrugi argument true ignoruje wielkość liter.
Enum.IsDefined(w)sprawdzenie, czy wartość jest na liścieKonieczne przy danych z zewnątrz.
[Flags]wyliczenie jako zestawWartości: 1, 2, 4, 8… Składanie przez |.
.HasFlag(x)sprawdzenie jednej flagiZwraca bool.
struct Nazwa { }typ wartościowyPrzypisanie kopiuje zawartość. Bez dziedziczenia.
default dla strukturywszystkie pola zeroweStruktura nie może być null.
Punkt?struktura, która może być pustaZapis z lekcji 34.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Stan jako int z komentarzemKomentarz jest w jednym miejscu, a porównania w dwudziestu. Po miesiącu nikt nie wie, co znaczy 2.
Stan jako stringLiterówka nie jest błędem kompilacji. "w naprawie" i "W naprawie" to różne wartości.
Brak pozycji o wartości 0Pole typu enum w nowym obiekcie i tak dostanie 0, a ta wartość nie będzie mieć nazwy.
(Stan)99 bez sprawdzeniaKompiluje się i działa. Zmienna ma wartość bez nazwy, a switch wpadnie do _. Użyj Enum.IsDefined.
switch po enum bez gałęzi _CS8509 — kompilator ostrzega, że nie wszystkie wartości są obsłużone, i ma rację.
Nowa pozycja w enum bez przejrzenia wszystkich switchProgram działa, ale w jednym miejscu zachowa się domyślnie. Szukaj po nazwie typu, gdzie jest używany.
[Flags] z wartościami 1, 2, 3Zestaw 1 + 2 jest nieodróżnialny od pozycji 3. Muszą być potęgi dwójki.
Enum.TryParse bez IsDefinedTekst "7" zostanie przyjęty jako wartość liczbowa, choć nie ma takiej pozycji.
struct z dziedziczeniemCS0527. Struktura może implementować interfejsy, ale nie ma klasy bazowej.
struct ustawiany na nullCS0037. Struktura zawsze ma wartość; jeśli potrzebujesz braku, napisz Punkt?.
struct na coś dużego i zmiennegoKopiowanie przy każdym przypisaniu i przekazaniu do metody. Do tego prowadzi do błędów opisanych w lekcji 50 — tam zobaczysz dokładnie jakich.
ZADANIA

Zadania

ZAD 1Pierwszy typ wyliczeniowy★☆☆

Napisz typ wyliczeniowy DzienTygodnia z siedmioma pozycjami od poniedziałku do niedzieli. W programie utwórz zmienną tego typu, przypisz jej środę i wypisz: samą wartość, jej numer po rzutowaniu na int oraz wynik porównania z sobotą. Następnie pętlą po Enum.GetValues<DzienTygodnia>() wypisz wszystkie dni z numerami. Na koniec spróbuj napisać DzienTygodnia d = 3; i zapisz w komentarzu numer błędu.

ZAD 2Wyliczenie w klasie i switch★☆☆

Napisz typ wyliczeniowy Ocena z pozycjami Niedostateczny = 1 do Celujacy = 6. Napisz klasę Sprawdzian z właściwościami Przedmiot i Wynik typu Ocena. Napisz metodę static string Slownie(Ocena o), która wyrażeniem switch zwraca komentarz do każdej oceny, z gałęzią _ na końcu. Utwórz tablicę pięciu sprawdzianów, wypisz je w kolumnach wraz z komentarzem i policz średnią ocen, rzutując wartości na int.

ZAD 3Wczytanie i sprawdzenie★★☆

Do zadania 2 dopisz wczytywanie oceny od użytkownika. Program ma w pętli prosić o ocenę do momentu podania poprawnej: wykorzystaj Enum.TryParse<Ocena> z ignorowaniem wielkości liter, a po udanym rozpoznaniu dodatkowo sprawdź Enum.IsDefined. Sprawdź, co się dzieje przy wpisaniu celujacy, CELUJACY, 4, 9 oraz pustego wiersza, i zapisz wyniki wszystkich pięciu prób w komentarzu. Wyjaśnij, dlaczego samo TryParse nie wystarczyło.

ZAD 4Uprawnienia na flagach★★☆

Napisz typ wyliczeniowy Uprawnienia z atrybutem [Flags] i pozycjami Brak = 0, Czytanie, Zapis, Usuwanie, Administracja o właściwych wartościach. Napisz klasę Konto z właściwościami Login i Prawa oraz metodami Dodaj(Uprawnienia u), Odbierz(Uprawnienia u) i bool Moze(Uprawnienia u). W programie utwórz trzy konta: uczeń (tylko czytanie), nauczyciel (czytanie i zapis) i administrator (wszystko). Wypisz tabelkę z loginem, pełnym zestawem praw i czterema kolumnami tak/nie dla poszczególnych uprawnień. Potem odbierz nauczycielowi zapis, dodaj uczniowi usuwanie i wypisz tabelkę ponownie.

ZAD 5Pierwsza struktura★★☆

Napisz strukturę Wymiar z właściwościami tylko do odczytu Szerokosc i Wysokosc (typ double), konstruktorem, wyliczaną właściwością Pole oraz przesłoniętym ToString() w formacie 3,0 x 4,0. W programie utwórz wymiar, przypisz go do drugiej zmiennej i wypisz oba oraz wynik Equals. Utwórz też zmienną przez default i wypisz ją. Następnie spróbuj: napisać struct Wymiar : object, przypisać null do zmiennej typu Wymiar oraz zadeklarować Wymiar? i przypisać jej null. Zapisz w komentarzu, które z trzech prób się udały i jakie numery błędów dały pozostałe.

ZAD 6Rezerwacja sali — pełny program★★★

Napisz program do rezerwacji sal na konsultacje. Zadeklaruj: typ wyliczeniowy StanRezerwacji (Wolna, Zajeta, Odwolana), typ wyliczeniowy TypSali z jawnymi numerami (Zwykla = 1, Komputerowa = 2, Laboratorium = 3, Inna = 0), typ wyliczeniowy z [Flags] o nazwie Wyposazenie (Brak, Rzutnik, Tablica, Komputery, Naglosnienie) oraz strukturę Termin z właściwościami Dzien (typ DzienTygodnia, własny enum) i Godzina (typ int), z przesłoniętym ToString(). Napisz klasę Rezerwacja z właściwościami Sala, TypSali, Wyposazenie, Termin, StanRezerwacji (z private set) i Nauczyciel, oraz metodami bool Zarezerwuj(string kto) (udaje się tylko dla stanu Wolna) i void Odwolaj(). W programie utwórz listę co najmniej sześciu rezerwacji, wykonaj kilka udanych i nieudanych rezerwacji, a następnie wypisz: pełną tabelkę rezerwacji, zestawienie liczby rezerwacji w każdym stanie (pętlą po Enum.GetValues), listę sal mających jednocześnie rzutnik i nagłośnienie (sprawdzenie dwóch flag) oraz listę sal wolnych w poniedziałek. Na końcu, w komentarzu, odpowiedz: dlaczego Termin jest strukturą, a Rezerwacja klasą, oraz co by się zmieniło, gdyby było odwrotnie.

PODSUMOWANIE

Co trzeba zapamiętać

  • enum to typ o zamkniętej liście wartości; kompilator odrzuca wszystko poza nią.
  • Za nazwami stoją liczby — domyślnie od 0, ale można je nadać samemu.
  • Pierwsza pozycja (wartość 0) powinna mieć sens jako stan początkowy, bo taką wartość dostaje nowe pole.
  • Rzutowanie liczby na enum nie jest sprawdzane — dane z zewnątrz weryfikuj przez Enum.IsDefined.
  • ToString() daje nazwę pozycji, dlatego wypisywanie stanów nie wymaga żadnej tabeli tłumaczeń.
  • W switch po enum zawsze zostawiaj gałąź _.
  • Enum.GetValues<T>() daje wszystkie pozycje — do pętli, menu i zestawień.
  • Enum.TryParse rozpoznaje tekst i zwraca bool, ale przyjmuje też zapis liczbowy.
  • [Flags] plus wartości 1, 2, 4, 8… pozwala trzymać zestaw wartości w jednej zmiennej.
  • struct to typ wartościowy: przypisanie kopiuje zawartość, a Equals porównuje pola.
  • Struktura nie dziedziczy, nie może być null i nie ma składowych virtual.
  • struct wybieraj tylko dla małych, niezmiennych wartości; w razie wątpliwości class.

Dokumentacja: Microsoft Learn — temat tej lekcji.

Postęp zapisuje się w tej przeglądarce.