Rozdział 12. Struktury C#

> Dodaj do ulubionych

Za pomocą struktury można uporządkować zbiór wartości w spójną paczkę gotową do wykorzystania przez jeden z elementów opracowywanego przez nas właśnie problemu. To bardzo ważne zastosowanie przydatne w wielu sytuacjach.

Czym jest struktura

Podczas pracy z danymi często trzeba przechowywać różne informacje na temat pewnych obiektów. Przyjazny bank zamówił system przechowywania kont, którego budowę możesz sobie ułatwić właśnie dzięki wykorzystaniu struktur. Jak każdy dobry programista, który uczył się z mojego kursu, rozpoczynasz pracę od następującej czynności:

  1. Ustalenie specyfikacji, tzn. uzyskanie na piśmie dokładnego opisu oczekiwań klienta.
  2. Negocjacja wygórowanego wynagrodzenia.
  3. Zastanowienie się nad sposobem przechowywania danych.

Przykładowa struktura w C#

Ze specyfikacji wiesz, że program ma przechowywać następujące informacje:

  • imię i nazwisko klienta — łańcuch
  • adres klienta — łańcuch
  • numer konta — liczba całkowita
  • saldo konta — liczba całkowita
  • limit debetu — liczba całkowita

Ludzie z Przyjaznego banku mówią Ci, że w tworzonym przez Ciebie magazynie będą przechowywane dane dotyczące maksymalnie 50 osób, więc po krótkim zastanowieniu tworzysz coś takiego:

const int MAX_CUST = 50;

AccountState [] states = new AccountState [MAX_CUST]; string [] names = new string [MAX_CUST];
string [] addresses = new string [MAX_CUST]; int [] accountNos = new int [MAX_CUST];
int [] balances = new int [MAX_CUST]; int [] overdraft = new int [MAX_CUST];

Są to osobne tablice do przechowywania poszczególnych elementów danych na temat każdego klienta. Gdybyśmy mieli posłużyć się bazą danych (którą w istocie właśnie piszemy), to pakiet danych każdego klienta nazywałby się rekordem, a jego poszczególne części, np. kwota debetu, nazywałyby się polami. W naszym programie przyjęliśmy zasadę, że element balance[0] zawiera saldo pierwszego klienta w naszej bazie danych, overdraft[0] zawiera kwotę debetu pierwszego klienta itd. (Przypominam, że indeksowanie tablic zaczyna się od 0).

System oparty na takiej bazie danych mógłby działać prawidłowo. Jednak byłoby miło, gdyby cały rekord udało się przedstawić w jakiejś bardziej zwięzłej postaci.

Tworzenie struktury w C#

W języku C# można tworzyć struktury. Struktura to zbiór zmiennych C#, które chcemy traktować jako jedną całość. W języku C# taki pakiet danych nazywa się strukturą, a jego elementy nazywają się polami. Aby stworzyć lepszą bazę danych dla naszego banku, powinniśmy utworzyć strukturę umożliwiającą zapisanie wszystkich informacji na temat każdego klienta:

struct Account
{
    public AccountState State;
    public string Name;
    public string Address;
    public int AccountNumber;
    public int Balance;
    public int Overdraft;
};

To jest definicja struktury o nazwie Account umożliwiająca zapisanie wszystkich wymaganych informacji o kliencie. Teraz możemy definiować takie zmienne:

Account RobsAccount;

Ta instrukcja tworzy zmienną typu Account o nazwie RobsAccount. Program może zapisywać informacje w jej elementach składowych.

Do wybranej składowej struktury odwołujemy się przez umieszczenie nazwy tej składowej po nazwie struktury zmiennej reprezentującej tę strukturę, rozdzielając je kropką, jak w poniższym przykładzie:

RobsAccount.Name="Rob";

To jest odwołanie do pola typu łańcuchowego Name w zmiennej typu strukturalnego o nazwie RobsAccount (czyli wartości pola Name w strukturze RobsAccount).

using System;

enum AccountState
{
    Nowe,
    Aktywne,
    WTrakcieKontroli,
    Zawieszone,
    Zamknięte
};

struct Account
{
    public AccountState State;
    public string Name;
    public string Address;
    public int AccountNumber;
    public int Balance;
    public int Overdraft;
};

class BankProgram
{
    public static void Main()
    {
        Account RobsAccount;
        RobsAccount.State = AccountState.Active;
        RobsAccount.Balance = 1000000;
    }
}

Przykład. Hojna struktura reprezentująca konto

Ten niemiernie hojny fragment kodu tworzy zmienną typu AccountStructure o nazwie RobsAccount, ustawia stan konta na Active oraz zapisuje na nim milion złotych (chociaż nic nie robi z tymi pieniędzmi).

Zwróć uwagę, że struktura i wyliczenie zostały zadeklarowane na zewnątrz klasy BankProgram. Utworzonej struktury mogę używać w taki sam sposób, jak typów wbudowanych, np. int czy float.

Struktury stają się szczególnie przydatne, kiedy zaczynamy zapisywać je w tablicach. W programie oceniającym przekonaliście się, że praca z pojedynczymi elementami jest nieefektywna, ale wszystko się zmienia, gdy użyjemy tablicy.

const int MAX_CUST = 100;
Account [] Bank = new Account [MAX_CUST];

Ta deklaracja tworzy tablicę klientów o nazwie Bank, w której można przechowywać dane wszystkich klientów. Zwróćcie uwagę, że sprytnie zdefiniowałem zmienną o nazwie MAX_CUST, której wartość aktualnie ustawiłem na 100. Reprezentuje ona maksymalną liczbę klientów, jaką może przechowywać nasz system bankowy.

Zmienne strukturalne można wzajemnie przypisywać jak zmienne każdego innego typu. W momencie przypisanie wszystkie wartości znajdujące się w strukturze źródłowej zostają skopiowane do struktury docelowej:

Bank[0] = RobsAccount;

Ta instrukcja powoduje skopiowanie informacji ze struktury RobsAccount do pierwszego elementu tablicy Bank.

To samo można także robić z elementami tablic struktur:

Bank [25].Name

To byłby łańcuch zawierający imię i nazwisko klienta w elemencie o indeksie 25.

Używanie struktury C#

Utworzonej struktury możemy użyć do przechowywania danych klientów naszego banku:

class AccountStructureArray
{
    public static void Main()
    {
        const int MAX_CUST = 100;
        Account[] Bank = new Account[MAX_CUST]; Bank[0].Name = "Rob";
        Bank[0].State = AccountState.Active;
        Bank[0].Balance = 1000000;
        Bank[1].Name = "Jim";
        Bank[1].State = AccountState.Frozen;
        Bank[1].Balance = 0;
    }
}

Przykład. Wstawianie informacji o kontach do tablic

Powyższy przykład pokazuje, jak to wygląda w praniu. Tworzy tablicę 100 rekordów klientów, po czym wstawia wartości do dwóch pierwszych elementów tablicy. Który właściciel konta ma najwięcej pieniędzy?

Ten program nie wczytuje danych wszystkich 100 klientów banku, tylko pokazuje, jak uzyskać dostęp do różnych pól w tablicy struktur.

Wartości początkowe w strukturach C#

Kiedy struktura zostaje utworzona jako zmienna lokalna (tzn. w bloku), to jej wartości są niezdefiniowane. To znaczy, że jeśli spróbujemy ich użyć gdzieś w programie, kompilator zgłosi błąd. To dokładanie taka sama sytuacja, jak gdybyśmy użyli w programie zmiennej przed nadaniem jej wartości. Innymi słowy:

Account RobsAccount;
Console.WriteLine ( "Imię i nazwisko : " + RobsAccount.Name );

Ten kod spowodowałby błąd kompilacji. Zadaniem programisty jest dopilnować, aby każdej zmiennej przed jej użyciem została przypisana jakaś wartość.

Typy strukturalne w wywołaniach metod C#

Parametry metod mogą być typu strukturalnego:

public void PrintAccount ( Account a )
{
    Console.WriteLine ("Imię i nazwisko: " + a.Name);
    Console.WriteLine ("Adres: " + a.Address);
    Console.WriteLine ("Saldo: " + a.Balance);
}

Ta metoda umożliwia szybki wydruk zawartości zmiennej reprezentującej konto:

PrintAccount (RobsAccount);

Aby metoda ta zadziałała, należy jej przekazać wartość struktury RobsAccount. To taka sama sytuacja, jak przekazywanie liczb całkowitych do metod, których używaliśmy wcześniej. W wywołaniach metod można także przekazywać elementy tablic (ponieważ element będący częścią tablicy wartości Account z definicji jest egzemplarzem Account).

class BankProgram
{

    public static void PrintAccount(Account a)
    {
        Console.WriteLine("Imię i nazwisko: " + a.Name);
        Console.WriteLine("Adres: " + a.Address);
        Console.WriteLine("Saldo: " + a.Balance);
    }

    public static void Main()
    {
        const int MAX_CUST = 100;
        Account[] Bank = new Account[MAX_CUST];
        Bank[0].Name = "Rob";
        Bank[0].Address = "Dom Roba";
        Bank[0].State = AccountState.Active;
        Bank[0].Balance = 1000000;
        PrintAccount(Bank[0]);
        Bank[1].Name = "Jim";
        Bank[1].Address = "Dom Jima";
        Bank[1].State = AccountState.Frozen;
        Bank[1].Balance = 0;
        PrintAccount(Bank[1]);
    }
}

Przykład. Wydruk wartości z tablicy Account

Utworzenie metody zwracającej wyniki w postaci struktury także jest możliwe. Moglibyśmy na przykład napisać metodę ReadAccount wczytującą i zwracającą konto.

Złota myśl programisty: Struktury są kluczowe

Projektanci systemów komercyjnych poświęcają bardzo dużo uwagi budowie struktur mających służyć do przechowywania danych. Stanowią one podstawowy blok konstrukcyjny programu, ponieważ zawierają wszystkie dane, na których opierają się pozostałe mechanizmy. Z tego względu konstrukcję struktur i warunki dotyczące tego, co można w nich przechowywać, można traktować jako ważny metaelement tworzonego systemu.

Projektowanie przy użyciu typów

Jak na pewno zauważyliście, dodałem wartość State do struktury Account. Ułatwia ona monitorowanie stanu konta za pomocą programu. Krótko mówiąc, utworzyliśmy typ, który może przechowywać kilka wartości (nasz typ wyliczeniowy AccountState), a następnie umieściliśmy go w innym typie, który zaprojektowaliśmy pod kątem przechowywania informacji o właścicielach kont bankowych. Tę taktykę można zastosować w celu podejścia do bardziej złożonych problemów. Informacje na temat tego, jakie dane powinny pozwalać przechowywać poszczególne typy, znajdziemy w notatkach z rozmów z klientem.

Jeśli na przykład chcemy stworzyć naprawdę imponujący program, taki na którym mucha nie siada, możemy zacząć od tego:

enum WindowState
{
    Quoted, Ordered, Manufactured, Shipped, Installed
};

struct Window
{
    public WindowState state;
    public double Width;
    public double Height;
    public string Description;
};

Ta struktura może przechowywać informacje o wybranym oknie w domu, a konkretnie jego wymiary, stan i jego opis. Dla domu zawierającego wiele okien moglibyśmy utworzyć tablicę struktur Window .

Złota myśl programisty: Obiekty powinny mieć określony stan

Powyższa klasa Account stanowi dobry przykład typu obiektów, które będą używany na różne sposoby w zależności od stanu. Na przykład nie będzie można pobrać środków z konta w stanie zamrożonym. Ulepszenie w postaci dodania stanu można zastosować praktycznie do każdego obiektu w systemie. Zamówienia mogą być puste, oczekujące, wysłane i odebrane. Kosmici mogą być uśpieni, w trakcie ataku lub zniszczeni. Przy tworzeniu nowego obiektu w systemie zawsze zastanów się, jakie stany może on przyjmować.

Autor: Rob Miles

Tłumaczenie: Joanna Liana