Rozdział 13. Obiekty, struktury i referencje w C#

30 listopada 2021
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

Wcześniej pokazałem Wam, jak przechowywać bloki informacji o wybranym elemencie w strukturze. Struktury są przydatne, ale podczas pisania dużych programów pojawiają się różne inne problemy:

  • Chcemy utworzyć taki element w programie, który nigdy nie może mieć nieprawidłowego stanu, np. nie możemy dopuścić, aby ktoś utworzył konto bankowe o pustym lub niepoprawnym numerze.
  • Chcemy móc podzielić duży system na osobne komponenty, które można rozwijać i wymieniać na inne o takiej samej funkcji. Na przykład delegujemy jeden zespół programistów do prac nad kontami, inny do sekcji czeków, jeszcze inny do kart kredytowych itd.
  • Chcemy, aby tworzenie nowych typów kont było jak najłatwiejsze. Jeśli na przykład bank postanowi stworzyć konto depozytowe o wysokim oprocentowaniu, to powinniśmy móc do tego celu wykorzystać istniejące konto depozytowe.

Aby zrealizować te wszystkie cele, musimy zacząć stosować w naszym programie zasady programowania obiektowego. Na początku tego rozdziału powinno znajdować się ostrzeżenie w rodzaju „niektóre zawarte tu treści mogą uszkodzić Ci mózg”. Koniecznie weź sobie do serca następujące informacje:

  • Obiekty nie dodają do programów żadnych nowych zachowań — wszystkiego, co jest potrzebne do pisania programów, nauczyliśmy się w rozdziałach poświęconych instrukcjom, pętlom, warunkom i tablicom.
  • Obiekty to narzędzie służące do projektowania struktury programu. Za ich pomocą opisujemy ogólną konstrukcję systemu. W razie potrzeby program oparty na strukturze obiektowej zawsze można poprawić.

Każdy program, jaki kiedykolwiek stworzono, można napisać przy użyciu wyłącznie tych technologii, które poznaliście do tej pory. Jednak praca z obiektami jest o wiele przyjemniejsza. Dlatego, czy Wam się to podoba, czy nie, teraz będziecie musieli się z nimi zapoznać.

Obiekty i struktury

W języku C# obiekty i struktury są bardzo podobne do siebie. Jedne i drugie mogą zawierać dane i metody. Dzieli je jednak pewna bardzo ważna różnica. Struktury to konstrukcje wartościowe, a obiekty to konstrukcje referencyjne.

Rozróżnienie to jest bardzo ważne, ponieważ ma duży wpływ na sposób posługiwania się każdym rodzajem konstrukcji.

Tworzenie i używanie struktur

Spójrz na poniższy kod:

struct AccountStruct
{
    public string Name ;
};

class StructsAndObjectsDemo
{
    public static void Main ()
    {
        AccountStruct RobsAccountStruct;
        RobsAccountStruct.Name = "Rob";
        Console.WriteLine ( RobsAccountStruct.Name );
    }
}

Przykład. Przykładowa struktura reprezentująca konto

To jest bardzo prosta implementacja konta bankowego, która zawiera tylko imię właściciela. Metoda Main tworzy zmienną strukturalną o nazwie RobsAccountStruct.

Następnie we własności Name tej zmiennej zapisuje łańcuch "Rob". Gdybyśmy teraz uruchomili ten program, to zapewne zgodnie z Waszymi oczekiwaniami wydrukowałby imię Rob. Gdyby ta struktura zawierała jeszcze inne elementy dotyczące konta bankowego, to one również byłoby w niej przechowywane i można by było się do nich odwoływać w taki sam sposób.

Tworzenie i używanie egzemplarza (obiektu) klasy

Wystarczy drobna zmiana w kodzie źródłowym, aby zamienić konto bankowe w klasę:

class Account
{
    public string Name ;
};

class StructsAndObjectsDemo
{
    public static void Main ()
    {
        Account RobsAccount;
        RobsAccount.Name = "Rob";
        Console.WriteLine (RobsAccount.Name);
    }
}

Przykład. Klasa Account, która nie przejdzie kompilacji

Teraz informacje o koncie są przechowywane w klasie. Klasa reprezentująca konto ma nazwę Account. Sęk w tym, że podczas kompilacji tego programu zobaczymy następujący komunikat:

ObjectDemo.cs(12,3): error CS0165: Use of unassigned local variable ' RobsAccount'

O co chodzi? Aby to zrozumieć, musisz dokładnie wiedzieć, co się dzieje w poszczególnych wierszach.

Account RobsAccount;

To wygląda jak deklaracja zmiennej o nazwie RobsAccount. A jednak w przypadku obiektów pozory mylą.

Kiedy komputer wykona ten wiersz kodu programu, zostanie utworzona referencja o nazwie RobsAccount. Takie referencje mogą odwoływać się do egzemplarzy klasy Account. Możesz o nich myśleć, jak o metkach do bagażu zgodnie z analogią, że je też można do czegoś przywiązać za pomocą sznurka. Kiedy masz w ręku metkę, to po sznurku dojdziesz do przedmiotu, z którym jest połączona.

Jednak utworzenie referencji nie sprawia, że dostajemy to, do czego się ona odnosi. Kompilator o tym wie, w związku z czym zgłosi błąd, gdy „zobaczy” poniższy wiersz kodu:

RobsAccount.Name = "Rob";

W tym wyrażeniu próbujemy znaleźć to, co jest powiązane z tą metką i ustawić własność name na "Rob". Tylko że ta metka na razie nie jest z niczym połączona, więc program w miejscu tego wyrażenia się wykrzaczy. W efekcie kompilator informuje nas, że próbujemy użyć referencji, która do niczego się nie odwołuje, w związku z czym postanowił nas uraczyć błędem dotyczącym niezdefiniowanej zmiennej.

Ten problem rozwiązujemy przez utworzenie obiektu klasy i połączenie go z naszą metką. W tym celu wystarczy dodać wiersz kodu do naszego programu:

class Account
{
    public string Name ;
};

class StructsAndObjectsDemo
{
    public static void Main ()
    {
        Account RobsAccount;
        RobsAccount = new Account();
        RobsAccount.Name = "Rob";
        Console.WriteLine (RobsAccount.Name);
    }
}

Przykład 28. Kompilacja klasy Account

Dodany przeze mnie wiersz tworzy nowy obiekt klasy Account i wiąże z nim referencję

RobsAccount.

Słowo kluczowe new już pokazywałem. Posłużyło nam do tworzenia tablic. Nic dziwnego, skoro tablice są zaimplementowane jako obiekty, które tworzy się za pomocą tego słowa. Słowo kluczowe new tworzy obiekt. Obiekt jest konkretnym egzemplarzem klasy. Powtórzę to bardzo wyraźnie:

Obiekt jest konkretnym egzemplarzem klasy.

Powtórzyłem to zdanie, ponieważ jest ono niezmiernie ważne. Klasa zawiera instrukcje określające, co interpreter C# musi i może zrobić. Słowo kluczowe new nakazuje mu utworzyć egzemplarz danej klasy na podstawie zawartych w niej instrukcji. Zwróć uwagę, że powyżej nazwałem obiekt Account, nie RobsAccount. Wynika to z tego, że egzemplarz obiektu nie ma identyfikatora RobsAccount. Jest w tej chwili jedynie połączony z RobsAccount.

Referencje

Musimy przyzwyczaić się do faktu, że jeśli chcemy używać obiektów, to będą nam towarzyszyć referencje. To nierozłączna para, która zawsze występuje razem. Struktury też są przydatne, ale jeśli zależy Ci na programowaniu obiektowym pełną gębą, to musisz używać obiektów, a tymi możesz w pełni zarządzać za pomocą prowadzących do nich referencji. W praktyce to nie jest nic strasznego, ponieważ referencję praktycznie przez cały czas można traktować tak, jakby była prawdziwym obiektem. Trzeba tylko pamiętać, że referencja nie jest tożsama z obiektem. To tylko metka, która jest powiązana z egzemplarzem…

Wiele referencji do jednego obiektu

Do wyjaśnienia tej kwestii posłużę się kolejnym przykładem. Spójrz na poniższy kod:

Account RobsAccount;
RobsAccount = new Account();
RobsAccount.Name = "Rob";
Console.WriteLine (RobsAccount.Name);
Account Temp;
Temp = RobsAccount;
Temp.Name = "Jim";
Console.WriteLine (RobsAccount.Name);

Przykład 29. Wiele referencji

Pytanie brzmi: jaki będzie wynik drugiego wywołania metody WriteLine? Łatwiej będzie odpowiedzieć, gdy narysujemy sobie schemat:

Obie metki odnoszą się do tego samego egzemplarza klasy Account. To znaczy, że wszelkie zmiany w obiekcie, do którego odnosi się referencja Temp będą widoczne także w obiekcie, do którego odnosi się referencja RobsAccount, ponieważ to jeden i ten sam obiekt. Zatem program wydrukowałby słowo Jim, ponieważ takie imię jest zapisane w obiekcie wskazywanym przez referencję RobsAccount.

To sugeruje, że korzystanie z obiektów i referencji może się wiązać z pewnymi trudnościami. Liczba referencji do jednego obiektu jest nieograniczona, a więc trzeba pamiętać, że zmiana obiektu wskazywanego przez jedną referencję spowoduje analogiczną zmianę z punktu widzenia innych referencji.

Brak referencji do obiektu

Na dobicie zastanowimy się nad sytuacją, gdy do obiektu nie odnosi się żadna referencja:

Account RobsAccount;
RobsAccount = new Account();
RobsAccount.Name = "Rob";
Console.WriteLine(RobsAccount.Name);
RobsAccount = new Account();
RobsAccount.Name = "Jim";
Console.WriteLine(RobsAccount.Name);

Przykład 30. Brak referencji do obiektu

Ten kod tworzy obiekt konta, ustawia jego własność name na Rob, a następnie tworzy drugi obiekt konta. Referencja RobsAccount zostaje przestawiona na nowy element, którego własności name została nadana wartość Jim. Pytanie brzmi: co się stanie z pierwszym obiektem? To też łatwiej wytłumaczyć z pomocą schematu:

Pierwszy obiekt „wisi” sobie w próżni, ponieważ nic do niego się nie odwołuje. Jeśli chodzi o możliwość użycia zawartych w tym obiekcie danych, to tak, jakby ich nie było. Implementacja języka C# ma specjalny proces zwany „śmieciarką” (garbage collector), którego zadaniem jest znajdowanie i usuwanie takich bezużytecznych obiektów. Pamiętaj, że kompilator nie powstrzyma Cię przed takim „porzuceniem” egzemplarza klasy.

Musisz też pamiętać, że podobna sytuacja ma miejsce, kiedy referencja do obiektu znajdzie się poza zakresem dostępności:

{
    Account localVar;
    localVar = new Account();
}

Zmienna localVar jest dostępna lokalnie w obrębie bloku. Kiedy program skończy go wykonywać, zostaje ona porzucona, a to oznacza, że znika jedyna referencja do konta, więc śmieciarka ma kolejne zadanie do wykonania.

Złota myśl programisty: Unikaj śmieciarki

Choć czasami dobrze jest zwolnić niepotrzebne obiekty, trzeba pamiętać, że na tworzenie i usuwanie obiektów jest zużywana moc obliczeniowa komputera. Kiedy posługuję się obiektami, zawsze uważam na to, ile ich tworzę i niszczę. To, że ich usuwanie odbywa się automatycznie, wcale nie znaczy, że należy nadużywać tej funkcji.

Po co w ogóle są te referencje?

Na razie referencje mogą nie wydawać się Wam niczym szczególnym. Można odnieść wrażenie, że tylko utrudniają tworzenie i używanie obiektów oraz mogą być źródłem poważnych nieporozumień. Po więc zawracać sobie nimi głowę?

Aby odpowiedzieć na to pytanie, poznajmy wyspę na Pacyfiku o nazwie Yap. Obowiązująca na niej waluta jest oparta na 3,5-metrowych kamieniach ważących po kilkaset kilogramów. Wartość „monety” w walucie Yap jest bezpośrednio powiązana z liczbą mężczyzn, którzy zginęli na morzu podczas sprowadzania kamienia na wyspę. Kiedy płacisz komuś jedną z tych monet, to nie dajesz jej fizycznie, tylko mówisz — „Moneta na drodze na szczycie wzgórze jest teraz twoja”. Innymi słowy mieszkańcy wyspy posługują się referencjami do obiektów, których nie chcą przenosić.

Z tego samego powodu używamy referencji w naszych programach. Wyobraź sobie bank prowadzący wiele kont. Gdybyśmy chcieli je posortować w kolejności alfabetycznej wg nazwisk klientów, to musielibyśmy je wszystkie poprzenosić.

Sortowanie przez przenoszenie obiektów

Gdybyśmy konta przechowywali w postaci tablicy struktur, to mielibyśmy mnóstwo pracy z samym utrzymaniem odpowiedniej kolejności. Poza tym bank może chcieć porządkować informacje na różne inne sposoby. Na przykład może zastosować sortowanie według nazwiska klienta i numeru konta. Bez referencji to byłoby niemożliwe. Dzięki nim wystarczy, że utworzymy kilka tablic takich odwołań i każdą z nich posortujemy w wybrany sposób.

Sortowanie przy użyciu referencji

Jeśli będziemy sortować tylko referencje, to nie będziemy musieli przenosić dużych porcji danych. Nie będzie potrzeby przenoszenia żadnych obiektów, aby dodać nowe, ponieważ wystarczy tylko poprzesuwać referencje.

Referencje i struktury danych

Nasza sortowana lista referencji jest całkiem w porządku, ale nadal zmusza nas do przenoszenia mnóstwa elementów, jeśli chcemy coś do niej dodać. Aby rozwiązać ten problem, i przy okazji przyspieszyć wyszukiwanie, możemy zapisać nasze dane w formie struktury zwanej drzewem.

Sortowanie przy użyciu drzewa

W powyższym drzewie każdy węzeł ma po dwie referencje. Jedna odnosi się do węzła, który jest „jaśniejszy”, a druga — do węzła, który jest „ciemniejszy”. Jeśli chcę posortować listę elementów, to po prostu schodzę w dół drzewa po jego „lżejszej” stronie jak najniżej, aż dojdę do najlżejszego elementu. Następnie przechodzę o jeden wyżej (który musi być drugim najlżejszym elementem).

Idę jeszcze wyżej (to jest następny najlżejszy element). Potem przechodzę na ciemną stronę (Luke) i powtarzam proces. Najfajniejsze w tym jest to, że bardzo łatwo można dodawać kolejne elementy. Wystarczy znaleźć miejsce, gdzie powinien zawisnąć kolejny element i przyłączyć tam jego referencję.

Operacja wyszukiwania też jest bardzo szybko. Oglądam dany węzeł i podejmuję decyzję, w którą stronę iść dalej, aż znajdę to, czego szukam lub dowiem się, że w wybranym kierunku nie ma referencji, co oznacza, że takiego elementu w strukturze nie ma.

Złota myśl programisty: Struktury danych są bardzo ważne

To nie jest książka o strukturach danych, tylko o programowaniu. Nie przejmuj się, jeśli jeszcze nie łapiesz dokładnie, o co chodzi z tymi drzewami. Zapamiętajcie tylko, że referencje są bardzo ważnym składnikiem budowy takich struktur i to Wam na razie wystarczy. Kiedyś jednak nadejdzie takie czas, że będziecie musieli ogarnąć ten temat i nauczyć się tworzyć struktury przy użyciu takich technik.

Znaczenie referencji

Kluczowe znaczenie ma fakt, że każdy obiekt oprócz danych może także zawierać referencje do innych obiektów. Do tego aspektu obiektów jeszcze wrócimy w późniejszym czasie. Na razie wystarczy zapamiętać, że referencja i obiekt to dwa osobne byty.

Zapiski bankiera: Referencje i konta

Dla banku posiadającego tysiące klientów referencje odgrywają kluczową rolę w zarządzaniu przechowywanymi danymi. Konta będą przechowywane w pamięci komputera i, ze względu na rozmiar każdego z nich oraz ich liczbę, przenoszenie ich w celu posortowania nie będzie wchodziło w grę.

To znaczy, że jedynym sposobem na wykonywanie tego typu operacji jest pozostawienie samych kont w spokoju i utworzenie listy referencji do nich. Referencje to bardzo małe „metki”, za pomocą których można odnajdywać elementy przechowywane w pamięci. Listy referencji sortuje się bardzo łatwo i oczywiście można ich utworzyć dowolną liczbę. To znaczy, że dyrektorowi banku możemy przedstawić listy kont posortowane według nazwiska klienta i salda. A jeśli ten wymyśli jeszcze jakiś inny sposób przedstawienia informacji, to dzięki referencjom bez problemu spełnimy jego nowe żądanie.

Autor: Rob Miles

Tłumaczenie: Joanna Liana

Dyskusja

Twój adres e-mail nie zostanie opublikowany.