Rozdział 18. Obiekty i metoda ToString

> Dodaj do ulubionych

Przyjęliśmy za coś oczywistego fakt, że obiekty mają magiczną zdolność drukowania się. Jeśli napiszę takie wyrażenie:

int i = 99; Console.WriteLine(i);

To na ekranie zobaczę taki wynik:

99

Wartość całkowitoliczbowa skądś wie, jak się wydrukować. Czas dowiedzieć się, skąd bierze się ta zdolność oraz jak nadać takie magiczne umiejętności swoim własnym obiektom.

Okazuje się, że takie cuda dostaje się dzięki byciu obiektem w C#. Wiemy, że obiekt może zawierać informacje i robić różne rzeczy. (W sumie to cały proces budowy programów polega na decydowaniu, co obiekty powinny robić, a następnie na nadawaniu im tych umiejętności). Ponadto wiecie już, że można rozszerzyć obiekt nadrzędny, aby utworzyć nowy obiekt, który ma wszystkie elementy rodzica plus dodatkowo własne. Teraz pokażę wam, jak te właściwości obiektów są wykorzystywane do implementacji samego języka C#.

Klasa Object

Kiedy tworzymy nową klasę, to nie jest ona zawieszona w próżni. Tak naprawdę jest podklasą klasy object. Innymi słowy, jeśli napiszę coś takiego:

public class Account {

to równie dobrze mogę napisać to:

public class Account : object {

Klasa object jest częścią języka C#, w którym wszystko jest potomkiem tej klasy. Ma to parę ważnych konsekwencji.

  • Każdy obiekt ma wszystkie funkcje klasy object.
  • Odwołanie do typu object może odnosić się do dowolnej klasy.

W tej chwili dla nas ważniejszy jest pierwszy z tych punktów. Oznacza on, że wszystkie klasy dziedziczą pewien zestaw zachowań po klasie najwyższego poziomu o nazwie object. Jeśli zajrzycie do wnętrza obiektu, to znajdziecie w nim metodę o nazwie ToString. Jej implementacja w obiekcie zwraca łańcuchową reprezentację typu tego obiektu.

Metoda ToString

System wie, że każdy obiekt ma metodę ToString, więc jeśli kiedykolwiek potrzebuje łańcuchowej wersji obiektu, wywołuje na nim tę metodę, aby otrzymać reprezentację tekstową.

Innymi słowy ten kod:

object o = new object(); Console.WriteLine(o);

spowodowałby wydrukowanie następującej informacji:

System.Object

Metodę ToString można też wywołać bezpośrednio:

[csharp]Console.WriteLine(o.ToString());[/csharp]

Wynik będzie identyczny jak za poprzednim razem.

Metoda ToString ma też tę zaletę, że jest wirtualna. To znaczy, że możemy ją przesłonić, aby robiła dokładnie to, co chcemy.

class Account
{
private string name; private decimal balance;

public override string ToString()
{
return "Name: " + name + " balance: " + balance;
}

public Account (string inName, decimal inBalance)
{
name = inName; balance = inBalance;
}
}

W powyższej klasie Account przesłoniłem metodę ToString oraz sprawiłem, aby drukowała nazwisko i saldo. To znaczy, że ten kod:

Account a = new Account("Rob", 25); Console.WriteLine(a);

Przykład kodu. Metoda ToString

spowodowałby wydrukowanie następującej informacji:

Name: Rob balance: 25

Z punktu widzenia etykiety, każdy kto projektuje klasę, powinien zaimplementować w niej metodę ToString zwracającą tekstową wersję swojej zawartości.

Uzyskiwanie tekstowego opisu obiektu nadrzędnego

Aby uzyskać tekstowy opis obiektu nadrzędnego, można wykorzystać mechanizm base. Czasami się to przydaje, gdy dodacie jakieś dane do klasy potomnej i będziecie chcieli wydrukować zawartość klasy nadrzędnej:

public override string ToString()
{
return base.ToString() + " Rodzic: " + parentName;
}

Powyższa metoda pochodzi z klasy ChildAccount. Klasa ta rozszerza klasę CustomerAccount i ma dostęp do nazwy swojego rodzica. W powyższym kodzie została użyta metoda ToString z obiektu nadrzędnego, która pobrała nazwę rodzica, a następnie ją zwróciła. Zaletą tego jest to, że jeśli zmieni się zachowanie klasy nadrzędnej, np. zostaną dodane nowe składowe lub zmieni się format łańcucha, to nic nie będzie trzeba zmieniać w klasie ChildAccount, ponieważ będzie ona po prostu używać zmodyfikowanej metody.

Obiekty i porównywanie

Wiecie już, że porównywanie referencji do obiektów nie daje takiego samego wyniku, jak porównywanie wartości Wiemy, że obiektom są nadawane etykiety, które odnoszą się do niemających nazwy elementów przechowywanych gdzieś w pamięci. Kiedy je porównamy, system po prostu sprawdzi, czy obie referencje odnoszą się do tej samej lokalizacji.

Jeśli chcecie się dowiedzieć, jakie mogą z tego wynikać problemy, to pokażę wam przykład implementacji gry komputerowej (na razie zostawiamy bank). W tej grze różne obiekty są umieszczone w określonych miejscach na ekranie. Ich położenie możemy określić za pomocą współrzędnych punktu x i y. Potem możemy sprawdzać, czy da obiekty się ze sobą zderzyły.

class Point
{
public int x; public int y;
}

To jest moja klasa reprezentująca punkt. Teraz mogę tworzyć egzemplarze klasy Point i używać ich do obsługi obiektów w grze:

Point spaceshipPosition = new Point(); spaceshipPosition.x = 1;
spaceshipPosition.y = 2;

Point missilePosition = new Point(); missilePosition.x = 1;
missilePosition.y = 2;

if ( spaceshipPosition == missilePosition )
{
Console.WriteLine("Bum");
}

Zwróćcie uwagę, że składowe x i y klasy Point są publiczne. Nie zależy mi jakoś specjalnie na ich ochronie, ale za to chcę, aby program działał jak najszybciej. Sęk w tym, że ten program nie zadziała. Nawet jeśli umieszczę statek kosmiczny i pocisk w tym samym miejscu na ekranie, napis Bum i tak nie zostanie wydrukowany.

Wynika to z tego, że choć oba obiekty Point przechowują te same dane, to nie znajdują się pod tym samym adresem w pamięci, w efekcie czego wynik porównywania będzie ujemny.

Złota myśl programisty: Stosuj odpowiednią metodę porównywania

Programista musi pamiętać, że jeśli chce sprawdzić, czy dwa obiekty zawierają takie same dane, to powinien użyć metody Equals, a nie operatora ==. Z użyciem niewłaściwej metody porównywania wiążą się niektóre najwredniejsze błędy, jakie przyszło mi poprawiać w mojej karierze. Jeśli znajdziecie jakiś błąd w wyniku porównywania obiektów zawierających takie same dane, to sprawdźcie, czy została użyta właściwa metoda porównywania.

Dodawanie własnej metody Equals

Rozwiązaniem naszego problemu jest stworzenie metody, która będzie porównywała dwa punkty pod kątem tego, czy odnoszą się do tego samego miejsca. W tym celu musimy przesłonić standardową metodę Equals i zastąpić ją własną wersją:


public override bool Equals(object obj)
{
Point p = (Point) obj;
if ( ( p.x == x ) && ( p.y == y ) )
{
 

}
else
{

}
}
return true;return false;

Tej metodzie Equals należy przekazać referencję do obiektu, który ma zostać porównany. Zwróćcie uwagę, że jest to referencja do obiektu. Pierwsze, co musimy zrobić, to utworzenie referencji do obiektu Point. Musimy to zrobić, ponieważ chcemy uzyskać dostęp do wartości x i y punktu (nie pobierzemy ich z obiektu).

Następnie obiekt jest rzutowany na typ Point i zostają porównane wartości x i y. Jeśli są takie same, metoda zwraca wartość true. To oznacza, że mogę napisać taki kod:

if ( missilePosition.Equals(spaceshipPosition) )
{
Console.WriteLine("Bum");
}

Przykład kodu 43. Metoda Equals

Teraz test da prawidłowy wynik, ponieważ metoda Equals porówna rzeczywistą zawartość dwóch punktów, a nie ich referencje.

Zauważcie, że przesłonięcie metody Equals nie było konieczne. Równie dobrze mogłem napisać całkiem nową metodę, np. o nazwie TakieSame. Jednak metoda Equals jest czasami używana przez metody biblioteki C# do sprawdzania, czy dwa obiekty zawierają takie same dane, więc przesłonięcie metody Equals sprawia, że moja klasa jest zgodna z wymogami także tych metod.

Złota myśl programisty: Operacje porównywania są bardzo ważne w testach

Nasz klient bankowy jest bardzo zadowolony, że żadne utworzone konto bankowe się nie powtarza. Gdyby w banku pojawiły się dwa identyczne konta, firma miałaby ogromne problemy. Skoro w systemie nic nie może się powtarzać, to możecie pomyśleć, że nie ma sensu tworzyć operacji porównującej obiekty Account. „Skoro system nie może zawierać dwóch takich samych obiektów, po co marnować czas na pisanie kodu sprawdzającego, czy obiekty są takie same?”

A jednak metoda porównująca jak najbardziej może się przydać. Kiedy napiszecie część programu zapisującą dane i je pobierającą, to będziecie bardzo zadowoleni z posiadania sposobu na sprawdzenie, czy to, co pobieranie jest identyczne z tym, co zapisaliście. W takiej sytuacji metoda porównywania jest bardzo ważna i uważam, że zawsze powinno się ją zaimplementować.

Zapiski bankiera: Dobrze jest mieć dobre maniery

Dodanie do tworzonych przez siebie klas metod Equals i ToString jest uważane za dobry zwyczaj. Dzięki temu wasze klasy będą pasować do pozostałych klas systemu C#. Ponadto dobrze jest prawidłowo posługiwać się słowem kluczowym this podczas pisania metod w klasach. Po rozpoczęciu prac nad systemem zarządzania kontami bankowymi powinniśmy zwołać zebranie wszystkich programistów, aby poinformować ich, że będziecie naciskać na przestrzeganie takich dobrych praktyk programistycznych. Jeśli poważnie podejdziecie do swojego zadania, to powinniście ustanowić normy określające, które z tych metod mają zostać zaimplementowane. Następnie powinniście poinformować wszystkich zaangażowanych w projekt programistów, czego się od nich oczekuje.

Obiekty i this

Na tym etapie powinniście już być przyzwyczajeni do tego, że można odwołać się do obiektu, aby dobrać się do jego składowych. Spójrzcie:

public class Counter
{
public int Data=0; public void Count ()
{
Data = Data + 1;
}
}

Powyższa klasa ma jedną składową do przechowywania danych i jedną metodę składową. Pierwsza składowa jest licznikiem. Każde wywołanie metody Count powoduje zwiększenie wartości licznika o jeden. Tej klasy mogę użyć na przykład tak:

Counter c = new Counter(); c.Count();
Console.WriteLine("Count: " + c.Data);

Wywołałem metodę i wydrukowałem dane. Wiemy, że w tym kontekście kropka nakazuje przejście do obiektu i użycie wskazanej składowej jego klasy.

this jako referencja do bieżącego egzemplarza

Słowo kluczowe this oznacza referencję do aktualnie używanego egzemplarza klasy. Kiedy metoda klasy pobiera zmienną składową, kompilator automatycznie wstawia this. przed każdym takim użyciem. Innymi słowy, „prawidłowa” wersja klasy Counter wygląda następująco:

public class Counter
{
public int Data=0; public void Count ()
{
this.Data = this.Data + 1;
}
}

Sami możemy dodać this., jeśli chcemy wyraźnie zaznaczyć, że używamy składowej klasy, a nie lokalnej zmiennej.

Przekazywanie referencji do siebie do innych klas

Słowa kluczowego this używa się także w sytuacji, kiedy klasa egzemplarza musi dostarczyć referencję do samej siebie innej klasie, która chce z niej skorzystać. Nowe konto bankowe mogłoby zapisać się w banku przez przekazanie referencji do siebie do metody, która je zapisze:

bank.Store(this);

Kiedy zostanie wywołana metoda Store, zostanie jej przekazana referencja do aktualnie używanego obiektu konta. Metoda Store przyjmuje this i zapisuje obiekt. Oczywiście w tym przypadku nie przekazujemy konta do banku, tylko przekazujemy referencję do konta.

Nie przejmujcie się, jeśli kręci się wam od tego w głowie.

Zamieszanie z this

Uważam, że posługiwanie się słowem kluczowym this jest dobrym pomysłem. Dzięki temu inny programista czytający mój kod od razu wie, czy używam zmiennej lokalnej, czy składowej klasy. Trzeba tylko się do tego przyzwyczaić. Jeśli mam składową klasy zawierającej metody i chcę użyć jednej z tych metod, to napiszę taki kod:

this.account.SetName("Rob");

To znaczy „W tej klasie mam zmienną składową o nazwie account. Wywołaj metodę SetName na tej składowej, aby ustawić w niej imię na Rob”.

Moc łańcuchów i pojedynczych znaków

Typ łańcuchowy jest na tyle ważny, że warto poświęcić mu trochę więcej czasu. Pokażę wam parę jego cech, dzięki którym zaoszczędzicie sporo pracy. Są to metody, które można wywoływać na referencjach do łańcuchów. Metody te zwracają nowy łańcuch, ale przetworzony w określony przez was sposób. Możecie na przykład zmienić wielkość liter w tekście, usunąć spacje z początku i końca oraz wyszukiwać fragmenty łańcuchów.

Manipulacja łańcuchami

Łańcuchy są wyjątkowe. Czasami myślę o nich jak o nietoperzach. Można powiedzieć, że nietoperz to zarówno ptak, jak i ssak. Podobnie łańcuch jest zarówno obiektem (do którego można odnosić się za pomocą referencji), jak i wartością (można się do niego odnosić jak do wartości).

Ta dwoista natura ułatwia programistom wiele spraw. Powoduje jednak też, że podczas nauki programowania łańcuchy musimy traktować w specjalny sposób, ponieważ sprawiają wrażenie, jakby łamały niektóre znane nam zasady.

Koniecznie musicie wiedzieć, co się dzieje, kiedy dokonujecie przekształcenia łańcucha. Wynika to z tego, że choć są one obiektami, to nie zawsze zachowują się jak obiekty.

string s1="Rob"; string s2=s1;
s2 = "different"; Console.WriteLine(s1 + " " + s2);

Jeśli myślicie, że znacie obiekty i referencje, to powinniście się spodziewać, że s1 zmieni się w chwili zmiany s2. Druga z tych instrukcji ustawia referencję s2 na ten sam obiekt, co s1, więc zmiana s2 powinna spowodować zmianę obiektu, do którego odwołuje się także s1.

Tak jednak się nie stanie, ponieważ C# traktuje obiekty łańcuchowe w wyjątkowy sposób. To są tzw. obiekty niezmienne. Wiąże się to z tym, że programiści wolą, aby łańcuchy bardziej przypominały wartości.

Niezmienne łańcuchy

Chodzi o to, że kiedy spróbujecie zmienić łańcuch, system C# utworzy nowy łańcuch, a następnie ustawi referencję, którą „zmieniacie” na ten nowy obiekt. Innymi słowy, kiedy system natrafi na taki wiersz:

s2 = "different";

utworzy nowy łańcuch zawierający tekst „different” i ustawi na niego referencję s2. To, do czego odnosi się s1, pozostało niezmienione. W związku z tym, po przypisaniu s1 i s2 nie odnoszą się do tego samego obiektu. Takie zachowanie, polegające na tym, że każda próba zmiany obiektu powoduje utworzenie nowego, jest sposobem implementacji niezmienności.

Pewnie nasuwa się wam takie pytanie: „Po co to komu?”. Przypuszczalnie wystarczyłoby uczynić łańcuchy typami wartościowymi i zniknęłyby wszystkie kłopoty. Niestety nie. Wyobraźcie sobie, że zapisaliście duży dokument w pamięci komputera. Dzięki referencjom wystarczy nam tylko jeden egzemplarz łańcucha zawierający dane słowo. Wszystkie jego wystąpienia w tekście mogą odnosić się do tego jednego egzemplarza. To daje oszczędność pamięci i znacznie przyspiesza wyszukiwanie słów.

Porównywanie łańcuchów

Nietypowa natura łańcuchów sprawia także, że można je porównywać za pomocą metody porównującej o dowolnie zdefiniowanym zachowaniu:

if ( s1 == s2 )
{
Console.WriteLine("Taki sam");
}

Jeśli s1 i s2 będą prawidłowymi typami referencyjnymi, to ta operacja porównania zadziałałaby tylko, gdyby obie referencje odnosiły się do tego samego obiektu. Ale w C# porównywanie działa, kiedy obiekty zawierają ten sam tekst. Jeśl wolicie, możecie użyć metody porównującej:

if ( s1.Equals(s2) )
{
Console.WriteLine("Nadal taki sam");
}

Edytowanie łańcuchów

Pojedyncze znaki łańcucha można odczytywać za pomocą indeksów, jak w tablicy:

char firstCh = name[0];

Ta instrukcja ustawia zmienną znakową firstCh na pierwszy znak łańcucha. Nie ma jednak możliwości zmiany znaków:

name[0] = 'R';

Ten kod spowodowałby błąd kompilacji, ponieważ łańcuchy są niezmienne.

Sekwencję znaków z łańcucha można pobrać za pomocą metody SubString:

string s1="Rob"; s1=s1.Substring(1,2);

Pierwszym parametrem jest pozycja początkowa, a drugim —liczba znaków do skopiowania. Wynikiem tej instrukcji byłoby zapisanie łańcucha "ob" w s1 (pamiętajcie, że indeksowanie łańcuchów zaczyna się od 0).

Drugi parametr można opuścić i wówczas zostaną skopiowane wszystkie znaki do samego końca łańcucha:

string s1="Miles"; s1=s1.Substring(2);

W zmiennej s1 zostałby zapisany łańcuch "les".

Długość łańcucha

Wszystkie powyższe operacje zakończyłyby się błędem, gdybyście spróbowali wykonać czynność sięgającą dalej niż wynosi długość łańcucha. Pod tym względem łańcuchy są takie same jak tablice. Można nawet korzystać z tych samych metod, aby sprawdzić ich długość:

Console.WriteLine ( "Długość: " + s1.Length);

Własność Length podaje liczbę znaków w łańcuchu.

Wielkość liter

Na podstawie obiektów typu łańcuchowego można tworzyć nowe zmodyfikowane wersje. W tym celu należy wywołać wybraną metodę na referencji do odpowiedniego obiektu:

s1=s1.ToUpper();

Metoda ToUpper zwraca łańcuch z wszystkimi literami zamienionymi na wielkie. Żadne inne znaki w łańcuchu nie zostają zmienione. Istnieje też metoda ToLower o przeciwnym działaniu.

Usuwanie białych znaków i opróżnianie łańcuchów

Kolejną przydatną metodą jest Trim. Usuwa ona wszystkie spacje z początku i końca łańcucha.

s1=s1.Trim();

Jest przydatna na wypadek gdyby użytkownik wpisał np. " Rob " zamiast "Rob". Jeśli nie pozbędziesz się niepotrzebnych spacji z początku i końca, wynik porównania tych dwóch imion będzie negatywny. Istnieją też metody TrimStart i TrimEnd usuwające tylko spacje z początku lub z końca łańcucha. Jeśli takiej metodzie przekażecie łańcuch zawierający tylko spacje, powstanie pusty łańcuch (o długości zero).

Polecenia dotyczące znaków

Klasa char także udostępnia kilka bardzo przydatnych metod, za pomocą których można sprawdzić wartości indywidualnych znaków. Są to metody statyczne, które wywołuje się na klasie znaków i za pomocą których można sprawdzać znaki na różne sposoby:

char.IsDigit(ch)zwraca prawdę, jeśli znak jest cyfrą (od 0 do 9)
char.IsLetter(ch)zwraca prawdę, jeśli znak jest literą (od a do z lub od A do Z)
char.IsLetterOrDigit(ch) zwraca prawdę, jeśli znak jest literą lub cyfrą
char.IsLower(ch)zwraca prawdę, jeśli znak jest małą literą
char.IsUpper(ch)zwraca prawdę, jeśli znak jest wielką literą
char.IsPunctuation(ch)zwraca prawdę, jeśli znak jest znakiem interpunkcyjnym
char.IsWhiteSpace(ch)zwraca prawdę, jeśli znak jest spacją, tabulatorem lub znakiem nowego wiersza

Możecie ich używać do wyszukiwania konkretnych znaków w łańcuchach.

Autor: Rob Miles

Tłumaczenie: Łukasz Piwko