Rozdział 3. Typy danych C#

01 stycznia 2021
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

Z tego rozdziału dowiesz się, jak pisać programy przetwarzające dane. Poznasz typy danych C#. Sprawdzimy też, jak można przechowywać i pobierać wartości oraz jak na nich operować. Dzięki temu nasze programy będą w stanie przetwarzać dane.

3.1. Zmienne i dane

Postanowiliśmy, że w naszym kalkulatorze materiałów będziemy przechowywać szerokość i wysokość okien w zmiennych typu double. Nim postawimy kolejne kroki na drodze do kariery programistycznej, musimy się zastanowić co to oznacza i jakie inne typy danych C# możemy przechowywać w naszych programach.

Programy operują na danych. Język programowania musi umożliwiać przechowywanie przetwarzanych danych, w przeciwnym razie będzie bezużyteczny. Faktyczne znaczenie danych określa programista (patrz wyżej).

Zmienna to nazwana lokalizacja, w której możemy coś przechowywać. Można porównać ją do opatrzonego podpisem pudełka o określonym rozmiarze. Jego nazwę wybieramy w oparciu o docelową zawartość (w naszym programie użyliśmy sensownych nazw takich jak woodLength). Musimy także określić typ zmiennej (konkretny kształt i rozmiar pudełka) — do wyboru mamy jeden z typów dostępnych w C#. Typ zmiennej wchodzi w skład jej metadanych.

Programy zwierają również literały. Literał to po prostu wartość, której używamy w programie w jakimś celu. Dla każdego typu zmiennej w języku C# istnieje sposób na wyrażenie jego literałów.

3.2. Przechowywanie liczb w C#

Jeśli chodzi o wartości liczbowe, to możemy wyróżnić dwa rodzaje danych:

  • Wygodne, konkretne pojedyncze wartości, takie jak liczba pasących się owiec, zębów w kole zębatym, czy jabłek w koszyku. Nazywamy je liczbami całkowitymi.
  • Wredne wartości określające pomiary z życia wzięte, np. aktualną temperaturę, długość sznurka czy prędkość samochodu. To tak zwane liczby zmiennoprzecinkowe.

W pierwszym przypadku przechowujemy konkretną wartość — a konkretna liczba to liczba całkowita.

Pisząc specyfikację, należy rozważyć, jak precyzyjne mają być przechowywane wartości. Zbyt wysoka precyzja może spowolnić komputer, a zbyt niska może spowodować, że wykorzystane wartości będą nieprawidłowe.

W drugim przypadku nigdy nie jesteśmy w stanie dokładnie określić mierzonej wartości. Nawet jeśli zmierzymy kawałek sznurka z dokładnością do 100 miejsc po przecinku, to i tak nie otrzymamy dokładnej długości — zawsze można zrobić to bardziej precyzyjnie. Na tym polega istota liczb zmiennoprzecinkowych. Komputer to urządzenie cyfrowe, czyli operujące jedynie na układach bitów, które można traktować jako liczby. Wiemy, że komputery działają na zasadzie zerojedynkowej, dlatego też mają problemy z przechowywaniem wartości zmiennoprzecinkowych. By je obsłużyć, komputer przechowuje je z ograniczoną dokładnością, która — mamy nadzieję — będzie wystarczająca (a zwykle jest).

Kiedy więc chcemy coś przechować, to musimy poinformować komputer czy będzie to wartość zmiennoprzecinkowa, czy całkowitoliczbowa. Musimy także wziąć pod uwagę zakres ewentualnych wartości, jakie będziemy przechowywać i na tej podstawie wybrać odpowiedni typ danych.

By poinformować język C# o tworzonej zmiennej, wystarczy ją zadeklarować. Deklaracja określa jednocześnie typ danych, które zamierzamy przechowywać. Możecie sobie wyobrazić, że C# tworzy pudełko o określonym kształcie, zaprojektowane tak, by przechowywać rzeczy określonego typu. Pudełko oznaczone jest metadanymi (i znowu to słowo), dzięki czemu system wie, co można w tym pudełku przechowywać i jak z niego korzystać.

3.2.1. Przechowywanie wartości całkowitoliczbowych

Liczby całkowite to z perspektywy komputera najłatwiejszy do przechowania typ wartości. Każdej wartości odpowiada konkretny układ bitów. Jedyny problem stanowi zakres — im większa wartość, tym większa jest wymagana do jej reprezentacji liczba bitów.

W C# dostępnych jest szereg typów, których wybór zależy od zakresu wartości, jakie zamierzamy przechowywać:

sbyte 8 bitów Od -128 do 127
byte 8 bitów Od 0 do 255
short 16 bitów Od -32768 do 32767
int 32 bity Od -2 147 483 648 do 2 147 483 647
uint 32 bity Od 0 do 4 294 967 295
long 64 bity Od -9 223 372 036 854 775 808 do 9 223 372 036 854 775 807
ulong 64 bity Od 0 do 18 446 744 073 709 551 615
char 16 bitów Od 0 do 65 535

Warto zauważyć, że te typy danych C# obsługują o jedną wartość ujemną więcej niż dodatnią. To dlatego, że liczby te przechowywane są w systemie uzupełnienia dwójkowego.

W C# standardowy typ całkowitoliczbowy — int — może przechowywać przerażająco wielkie liczby, z zakresu od -2147483648 do 2147483647. Jeśli chcielibyście przechowywać jeszcze większe liczby całkowite (choć nie mam pojęcia po co), to int ma swoją dłuższą wersję — long.

Przykładem zmiennej całkowitoliczbowej może być zmienna przechowująca liczbę pasących się owiec: int numberOfSheep;.

W ten sposób utworzyliśmy zmienną, która może zarejestrować ponad dwa tysiące milionów owiec! Ponadto program może manipulować „ujemnymi owcami”, co pewnie jest bez znaczenia (chyba że prowadzicie wypożyczalnię owiec). Pamiętajcie, że język nie bierze tego pod uwagę. Jeśli chcecie mieć pewność, że liczba owiec nigdy nie przekroczy 1 000 i nie będzie ujemna, to musicie to sami określić.

Podczas edycji kodu źródłowego w Visual Studio (lub innym edytorze stosującym kolorowanie składni) zobaczycie, że nazwy wybudowanych typów w C# (np. int czy float) są wyświetlane na niebiesko, tak jak powyżej.

Złota myśl programisty: umiesz liczyć — licz na siebie

Musicie być także świadomi, że program nie zawsze wykryje, że przekroczyliście ustalony dla zmiennej zakres. Jeśli umieszczę wartość 255 w zmiennej typu byte, to wszystko będzie w porządku, ponieważ 255 to największa wartość jaką może przechowywać ta zmienna. Jednak jeśli zwiększę przechowywaną wartość o 1, to system może tego nie wykryć. W konsekwencji wartość może się wyzerować, co mogłoby przysporzyć programowi sporych problemów.

3.2.2. Wartości literałów całkowitoliczbowych

Literał całkowitoliczbowy wyrażany jest jako sekwencja cyfr bez przecinka dziesiętnego:

23

To całkowitoliczbowa wartość 23. Mógłbym wykorzystać ją w programie w następujący sposób:

numberOfSheep = 23;

Jeśli korzystam z jednego z „krótszych” typów, to kompilator potraktuje wartość literału jako ten typ:

sbyte tinyVal = 127;

W powyższej instrukcji 127 to literał typu sbyte, a nie literał całkowitoliczbowy. Oznacza to, że jeśli zrobię coś głupiego:

sbyte tinyVal = 128;

(najwyższa wartość jaką może przechowywać zmienna sbyte wynosi 127), to kompilator wykryje moją pomyłkę i program nie zostanie skompilowany.

3.2.3. Przechowywanie wartości zmiennoprzecinkowych

Mówiąc o wartościach zmiennoprzecinkowych, zwykle mamy na myśli liczby inne od całkowitych. Liczba zmiennoprzecinkowa składa się z przecinka dziesiętnego i części ułamkowej. Zależnie od wartości przecinek dziesiętny zmienia swoją pozycję — stąd też nazwa liczby zmiennoprzecinkowe.

W C# dostępne jest pudełko przechowujące tego rodzaju wartości. To zmienna typu float, obsługująca liczby z przedziału od 1.5E-45 do 3.4E48 z dokładnością do jedynie 7 miejsc po przecinku (czyli wypada gorzej od większości kalkulatorów kieszonkowych).

Jeśli zależy wam na większej precyzji (co oczywiście przełoży się na większe zużycie pamięci i wolniejszą pracę komputera), możecie skorzystać ze zmiennej double (double to skrót od double precision, z ang. podwójna precyzja). Obciąża ona pamięć, lecz pozwala przechowywać wartości z przedziału od 5.0E-324 do 1.7E308 z dokładnością do 15 miejsc po przecinku.

W zmiennej float można by na przykład przechowywać średnią cenę gałki lodów:

float averageIceCreamPriceInPence;

Zmienną double można natomiast wykorzystać do przechowywania podanej w calach szerokości wszechświata:

double univWidthInInches;

Jeśli natomiast zależy wam na najwyższej możliwej precyzji dla nieco mniejszego zakresu liczb, skorzystajcie z typu decimal. Zajmuje on dwa razy więcej pamięci co zmienna double, lecz przechowuje wartości z dokładnością do 28–29 miejsc po przecinku. Typ decimal wykorzystuje się na potrzeby obliczeń finansowych, w których liczby nie są tak duże, lecz muszą być wyrażone bardzo precyzyjnie.

decimal robsOverdraft;

3.2.4. wartości literałów zmiennoprzecinkowych

Liczby zmiennoprzecinkowe można przechowywać na dwa sposoby: w zmiennej typu float lub double. Gdy umieszczamy w programie literały, kompilator chce wiedzieć czy ma być to wartość zmiennoprzecinkowa (mniejsze pudełko), czy o podwójnej precyzji (większe pudełko).

By wyrazić literał typu float, po liczbie rzeczywistej dodajemy znak f:

2.5f

W przypadku literału typu double f pomijamy:

3.5

Wartości float i double można także wyrazić za pomocą wykładnika potęgi:

9.4605284E15

To literał o podwójnej precyzji, wyrażający liczbę metrów w roku świetlnym. Jeśli dodamy na końcu f, otrzymamy literał zmiennoprzecinkowy.

W przypadku liczb zmiennoprzecinkowych kompilator jest bardziej wymagający niż wobec liczb całkowitych — nie można ich łączyć w dowolny sposób. Gdy zmieniamy wartość o podwójnej precyzji na zwykłą zmienną zmiennoprzecinkową, to staje się ona mniej precyzyjna. Trzeba więc dodatkowo potwierdzić, że jesteśmy świadomi konsekwencji takiej operacji. W tym celu mamy do dyspozycji tak zwane rzutowanie, które jeszcze omówimy szczegółowo.

Złota myśl programisty: nie ma to jak proste zmienne

Przekonacie się, że wielu programistów, w tym i ja, korzysta zwykle tylko ze zmiennych całkowitoliczbowych (int) i zmiennoprzecinkowych (float). Choć może się wydawać, że to marnotrawstwo zasobów (nie będziemy raczej liczyć dwóch tysięcy milionów owiec), to dzięki stosowaniu tych typów programy są bardziej zrozumiałe.

3.3. Przechowywanie tekstu

Czasami dane, które chcemy przechowywać są tekstem. Może to być pojedynczy znak, ale też i łańcuch. W C# dostępne są zmienne dla obu takich typów informacji.

3.3.1. Zmienne typu char

Zmienna typu char przechowuje pojedynczy znak. Znak jest tym, co pojawia się na ekranie, gdy naciśniemy klawisz na klawiaturze. C# korzysta z zestawu o nazwie Unicode, który obsługuje ponad 65 000 różnych znaków z wielu alfabetów.

W zmiennej char można na przykład przechowywać wciśnięty przez użytkownika klawisz Command:

char commandKey;

3.3.2. Wartości literałów znakowych

Znak umieszczamy w pojedynczym cudzysłowie:

'A'

Taki zapis oznacza „znak A”. Coś takiego otrzyma nasz program, jeśli poprosimy go o odczytanie z klawiatury znaku, który został wprowadzony przez użytkownika poprzez jednoczesne naciśnięcie klawiszy Shift i A. Jeśli będziecie pracować w edytorze obsługującym kolorowanie składni, to literał znakowy będzie wyświetlany na czerwono.

3.3.3. Sekwencje specjalne

Tu dochodzimy do pytania „Jak wyrazić znak ' (pojedynczy cudzysłów)?”.Można to zrobić za pomocą sekwencji specjalnej. To sekwencja znaków rozpoczynająca się od specjalnego znaku ucieczki. W tym kontekście ucieczka oznacza, że porzucamy nudne konwencje i zamiast dosłownej interpretacji otrzymujemy coś ekstra. Znak ucieczki to / (lewy ukośnik). Oto zestawienie możliwych sekwencji specjalnych:

Znak	Nazwa sekwencji specjalnej
\' 	pojedynczy cudzysłów
\" 	podwójny cudzysłów
\\ 	lewy ukośnik
\0 	znak końca danych
\a 	sygnał dźwiękowy
\b 	backspace
\f 	wysunięcie strony
\n 	znak nowego wiersza
\r 	znak powrotu karetki
\t 	tabulacja pozioma
\v 	tabulacja pionowa

Sekwencje te dają różne rezultaty, w zależności od urządzenia, do którego zostaną przesłane. Niektóre systemy po otrzymaniu znaku alertu wydadzą dźwięk „biiip”. Inne zaś wyczyszczą ekran, jeśli prześlemy im znak nowej strony.

Z wymienionych sekwencji można korzystać następująco:

char beep = '\a';

Znak a musi być wpisany z małej litery.

3.3.4. Kody znaków

Ustaliliśmy już, że komputery nie manipulują tak naprawdę faktycznymi literami, lecz liczbami. Język C# korzysta z kodów liczbowych standardu Unicode, które odpowiadają poszczególnym znakom. Możemy więc wyrazić literał znakowy w postaci wartości z zestawu znaków Unicode. Dobra wiadomość jest taka, że daje on nam mam dostęp do szerokiej gamy znaków (jeśli tylko znamy ich kody). Jest też i zła wiadomość: wartość należy wyrazić w systemie szesnastkowym, co nastręcza nieco więcej trudności niż zapis w postaci liczby dziesiętnej. Co jednak najistotniejsze, nie trzeba tego robić zbyt często, o ile w ogóle zajdzie taka potrzeba.

Mimo wszystko przeanalizujmy przykład. Wiem, że wartość Unicode dla (wielkiej) litery A to 65. W systemie szesnastkowym (podstawa 16) będą to cztery szesnastki i jedna jedynka (czyli 41), zatem w programie zapiszę ten znak następująco:

char wielkieA = '\x0041';

Zwróćcie uwagę, że przed dwoma cyframi szesnastkowymi umieściłem początkowe zera.

3.3.5. Zmienne łańcuchowe

Zmienna łańcuchowa to pudełko przechowujące napisy. W C# łańcuch może być bardzo krótki, tak jak np. „Rob”, albo bardzo długi, jak „Wojna i pokój” (mam tu na myśli całą książkę, nie tytuł). Zmienna łańcuchowa przechowuje wiersz tekstu. Jednak dzięki istnieniu specjalnych znaków oznaczających przejście do nowego wiersza (patrz wyżej) nic nie stoi na przeszkodzie, by pojedynczy łańcuch zawierał kilka wierszy.

Zmienna łańcuchowa może na przykład przechowywać wpisany przez użytkownika tekst:

string commandLine;

3.3.6. Wartości literałów łańcuchowych

Wartość literału łańcuchowego umieszczamy w cudzysłowie:

"to łańcuch"

Łańcuch może zawierać wymienione wyżej sekwencje specjalne:

"\x0041BCDE\a"

Zostałoby to wyświetlone jako:

ABCDE

Jeśli chcemy wyrazić tekst bez zastosowania znaków ucieczki i innych dziwolągów, to informujemy kompilator, że nasz łańcuch ma zostać odczytany dosłownie. W tym celu poprzedzamy literał znakiem @:

@"\x0041BCDE\a"

Łańcuch ten zostanie wyświetlony jako:

\x0041BCDE\a

Przydaje się to jeśli chcemy np. wyrazić ścieżkę do pliku i tym podobne rzeczy. Za pomocą znaku @ możemy wykonać jeszcze jedną sztuczkę — dzięki niemu literał może objąć kilka wierszy:

@"W niżach 
mógł zjeść truflę
koń bądź psy"

W ten sposób wyraziliśmy literał rozciągający się na trzy wiersze. Łańcuch zapisywany jest z zachowaniem podziału na wiersze.

3.4. Przechowywanie stanu przy pomocy zmiennych logicznych

Zmienna typu logicznego (bool) to pudełko przechowujące informację o tym czy coś jest prawdziwe, czy fałszywe. Czasami niczego więcej nam nie potrzeba. Jeśli chcemy zapamiętać czy subskrypcja została opłacona, to nie ma sensu marnować przestrzeni na zmienną mogącą przechowywać wiele możliwych wartości. Zamiast tego wystarczy, że będziemy przechowywać stan true (prawda) lub false (fałsz). Tylko te dwie wartości dostępne są w typie logicznym.

Przykładowa zmienna typu bool może przechowywać stan połączenia z siecią:

bool połączenieOK;

3.4.1. Wartości literałów logicznych

Literały te wyrażamy po prostu jako prawdę (true) albo fałsz (false):

połączenieOK = true;

Złota myśl programisty: przemyśl wybór zmiennej

Dobór odpowiedniego typu zmiennych sam w sobie stanowi umiejętność. Osobiście korzystam ze zmiennych zmiennoprzecinkowych tylko w ostateczności. Prowadzą one do braku precyzji, co nie przypada mi go gustu. Sądzę, że z odrobiną pomyślunku mogę całkiem wygodnie pracować na zmiennych całkowitoliczbowych. Przykład: zamiast pisać, że cena produktu wynosi 1,5 funta (co wymaga użycia typu float), zapisuję ją jako 150 pensów.

Ponadto wybierając sposób przechowywania danych, warto mieć na uwadze ich źródło. W jednym z powyższych przykładów użyłem zmiennej typu double do przechowywania szerokości i wysokości okna. To bardzo głupia decyzja. Nie uwzględnia wymaganego stopnia precyzji ani dokładności z jaką da się zmierzyć okno. Zakładam, że mój klient nie będzie w stanie określić wymiarów okna z dokładnością większą niż do 1 mm, dlatego nie ma sensu korzystać z bardziej precyzyjnych zmiennych.

Ponownie widzimy tu znaczenie metadanych: jeśli zastanawiamy się, czy przechowywać informacje w zmiennej zmiennoprzecinkowej, to musimy się najpierw dowiedzieć jak informacje te powstają. Dopiero wówczas możemy podjąć decyzję. Na przykład mogłoby się wydawać, że prędkość samochodu należy wyrazić jako wartość zmiennoprzecinkową. Gdy jednak dowiemy się, że czujnik prędkości dokonuje pomiarów z dokładnością do 1 kilometra na godzinę, to nasze zadanie staje się o wiele prostsze.

3.5. Identyfikatory

W C# identyfikator to nazwa, którą programista nadaje jakiemuś elementowi programu. Mówiąc o nazwach zmiennych, powinniśmy więc prawidłowo nazywać je identyfikatorami. Jak się przekonamy, identyfikatory tworzymy także w innych kontekstach. W języku C# obowiązują pewne zasady określające prawidłowy identyfikator:

  • wszystkie identyfikatory muszą rozpoczynać się od litery,
  • po literze mogą występować litery, cyfry bądź znak podkreślenia — „_”.

Rozróżniane są także wielkie i małe litery, np. Fred i fred to dwa różne identyfikatory.

Oto kilka przykładowych deklaracji, z których jedna jest nieprawidłowa (zastanówcie się która i dlaczego):

intfred;
float jim;
char 29taktoja;

Jedna ze złotych zasad programowania, oprócz tej głoszącej, że nie należy pisać na klawiaturze obróconej do góry nogami, mówi:

zawsze nadawaj zmiennym znaczące nazwy.

W końcu jak wynika z przeczytanych przeze mnie harlekinów, najlepsze związki to takie, które coś znaczą.

Jeśli chodzi o nazwy zmiennych, którymi się teraz zajmujemy, to wg konwencji C# należy w nich używać zarówno wielkich jak i małych liter, tak by każde słowo rozpoczynało się od wielkiej litery.

float średniaCenaLodówWPensach;

Notacja ta bywa nazywana wielbłądzią, zapewne ze względu na „garby” wynikające z użycia wielkich liter.

Złota myśl programisty: przemyśl wybór nazwy zmiennej

Wybieranie nazw zmiennych to kolejna umiejętność, nad którą musicie pracować. Nazwy muszą być wystarczająco długie, by wyrazić jakieś znaczenie, ale też nie za długie, tak by wiersze programu nie były zbyt skomplikowane. Nazwa średniaCenaLodówWPensach jest więc chyba przesadnie długa, ale da się to przeżyć. Pamiętajcie, że zawsze możecie manipulować układem kodu, tak by program prezentował się czytelnie:

średniaCenaLodówWPensach = 
obliczonaSumaWPensach / liczbaGałekLodów;

3.5.1. Przypisywanie wartości zmiennym

Skoro już utworzyliśmy zmienną, musimy dowiedzieć się, jak umieszczać w niej wartości i jak je wydobywać. W C# robimy to za pomocą instrukcji przypisania. Instrukcja ta składa się z dwóch części: rzeczy, którą chcemy przypisać i miejsca, w którym ją umieścić. Spójrzcie na poniższy przykład:

class Przypisanie 
{
    static void Main () 
    {
        int pierwsza, druga, trzecia; 
        pierwsza = 1; druga = 2; 
        trzecia = druga + pierwsza; 
    } 
}

Przykład kodu 02. Głupi przykład przypisania

Pierwsza część programu powinna być już wam dobrze znana. Wewnątrz funkcji Main zadeklarowaliśmy trzy zmienne o nazwach: pierwsza, druga, trzecia. Wszystkie z nich są całkowitoliczbowe.

Ostatnie trzy instrukcje odpowiedzialne są za przypisanie i nazywają się instrukcjami przypisania. Przypisanie nadaje wartość określonej zmiennej, która musi mieć sensowny typ (odpowiedzialność za podjęcie rozważnej decyzji spoczywa na nas, ponieważ — jak wiemy — kompilator nie wie i nie chce wiedzieć co robimy). Przypisana wartość to wyrażenie. Znajdujący się w środku znak równości wprowadza jedynie zamieszanie. Nie oznacza bowiem równości w matematycznym znaczeniu, dlatego porównuję go do mostka (patrz wyżej). Wartość znajdująca się po jego prawej stronie trafia do pudełka po lewej, co oznacza, że w kodzie:

2 = druga + 1;

czai się programistyczny chochlik, powodujący wiele nieprzyjemnych błędów.

3.5.2. Wyrażenia

Wyrażenie to coś, z czego można obliczyć wynik. Z wyniku możemy następnie korzystać w programie. Wyrażenia mogą być bardzo proste i składać się z pojedynczej wartości, bądź bardzo złożone i zawierać długie działanie. W ich skład wchodzą dwie rzeczy: operator i argument wyrażenia.

3.5.3. Argumenty wyrażenia

Argument wyrażenia to to, na czym działa operator. Zwykle są to literały bądź identyfikatory zmiennych. W powyższym programie pierwsza, druga i trzecia to identyfikatory, zaś 2 to literał. Literał to coś dosłownie wyrażonego w kodzie. Literały mają swoje typy, które przypisuje im kompilator.

3.5.4. Operatory

Operator wykonuje główne zadanie: określa operację, które ma zostać wykonana na argumencie wyrażenia. Większość operatorów operuje na dwóch argumentach, na jednym po obu stronach. W powyższym programie + to jedyny operator.

Oto kilka przykładowych wyrażeń:

2 + 3 * 4
-1 + 3 
(2 + 3) * 4

Wyrażenia te obliczane są przez C# od lewej, czyli tak, jakbyśmy to zrobili sami. Podobnie jak w matematyce, pierwszeństwo mają działania mnożenia i dzielenia. Po nich wykonywane jest dodawanie i odejmowanie.

C# nadaje każdemu operatorowi priorytet. Podczas obliczania wyrażenia szuka operatorów o najwyższym priorytecie i ich działania wykonuje jako pierwsze. Następnie szuka operatorów, które są drugie w kolejności — i tak dalej, aż do otrzymania ostatecznego wyniku. Oznacza to, że wynik pierwszego przykładowego wyrażenia wyniesie 14, nie 20.

Aby wymusić kolejność wykonywania obliczeń, należy umieścić priorytetowe działania w nawiasie, tak jak w ostatnim przykładzie. W nawiasie może znajdować się inna para nawiasów. Należy jedynie pamiętać, by liczba otwierających i zamykających nawiasów się zgadzała. Jako prosty człowiek zwykle staram się, by kod był bardzo czytelny, dlatego wszystko umieszczam w nawiasie.

Nie ma się jednak co zbytnio przejmować, jak to się mądrze nazywa, obliczaniem wartości wyrażenia — zwykle wszystko przebiega zgodnie z naszymi oczekiwaniami.

Poniżej zamieszczam listę operatorów z opisem ich działania i priorytetem, zaczynając od najwyższego.


- 	Minus jednoargumentowy, który w C# służy do oznaczania liczb ujemnych, np. -1. Słowo jednoargumentowy oznacza, że odnosi się on do jednego elementu. 
* 	Znak mnożenia. Zauważcie że jest to gwiazdka (*), a nie bardziej poprawny z matematycznego punktu widzenia, lecz mylący znak x. 
/ 	Znak dzielnia. Zapis jednej liczby nad drugą byłby na ekranie zbyt problematyczny.
+ 	Znak dodawania. 
- 	Znak odejmowania. Zauważcie, że jest to ten sam znak co minus jednoargumentowy. 

Nie jest to wyczerpująca lista, jednak póki co nam wystarczy. Ponieważ wymienione operatory działają na liczbach, to są często nazywane operatorami liczbowymi. Należy jednak pamiętać, że niektóre z nich (np. +) operują także na innych typach danych. Ponadto operatory mogą sprawić, że typ wartości ulegnie zmianie. Jak się przekonamy, bywa to kłopotliwe.

3.6. Zmiana typu danych

Gdy tylko próbuję zmienić typ wartości, kompilator C# zaczyna się bardzo interesować tym co robię. Martwi się, czy operacja którą mam zamiar wykonać nie spowoduje utraty danych.

Kompilator postrzega każdą operację w kategoriach rozszerzania i zawężania wartości.

3.6.1. Rozszerzanie i zawężanie wartości

Co do zasady, zawsze gdy spróbujemy zawęzić wartość, C# poprosi nas byśmy wyraźnie potwierdzili, że chcemy to zrobić. W przypadku rozszerzania takiego wymagania nie ma.

Pojęcia zawężania i rozszerzania wartości można zilustrować na przykładzie walizek. Wybierając się w podróż, pakuję się w walizkę. Jeśli zdecyduję się na małą, to może nie udać mi się upchnąć w niej całego bagażu i będę musiał zostawić w domu jedną koszulę. To właśnie zawężanie.

Jeśli jednak zamienię wybraną walizkę na większą, to nie ma problemu. Większa walizka pomieści wszystko co było w małej i jeszcze zostanie w niej trochę miejsca.

W kontekście C# „rozmiar” typu to przedział wartości (od najmniejszej do największej) i stopień precyzji (liczba miejsc po przecinku). Oznacza to, że poniższy kod:

Int i = 1; 
Float x = i;

jest prawidłowy, ponieważ typ zmiennoprzecinkowy przechowuje wszystkie wartości obsługiwane przez typ całkowitoliczbowy. Jeśli jednak napiszę coś takiego:

float x = 1; 
int i = x ;

to kompilator zacznie marudzić (mimo iż zmienna x przechowuje obecnie wartość całkowitoliczbową). Kompilator martwi się, ponieważ dokonując takiego przypisania, możemy utracić informacje i uznaje to za błąd.

Podobnie jest w przypadku wartości zmiennoprzecinkowych. Przykładowo poniższy kod:

Double d = 1.5; 
Float f = d;

również spowoduje błąd, bo kompilator wie, że zmienna double może przechowywać szerszy zakres wartości niż typ float.

3.6.2. Rzutowanie

Za pomocą rzutowania możemy zmusić C#, by traktował wartość jako należącą do konkretnego typu. Aby rzutować wartość do określonego typu, dodajemy nową instrukcję — przed wybraną wartością umieszczamy w nawiasie typ, do którego ma zostać rzutowana. Przykład:

Double d = 1.5; 
Float f = (float) d;

Za pośrednictwem powyższego kodu mówimy kompilatorowi, że nie przejmujemy się utratą danych jaka mogłaby nastąpić w wyniku tego przypisania. Jako twórcy programu przejmujemy odpowiedzialność za jego prawidłowe działanie. Można powiedzieć, że gdy decydujemy się na rzutowanie, to kompilator umywa ręce od ewentualnych konsekwencji. Jeśli w programie wystąpi błąd spowodowany utratą danych, to nie będzie to wina kompilatora.

Jak już widzieliśmy, każdy typ zmiennej obsługuje określony przedział wartości, który przypadku typu float jest znacznie większy niż w typie całkowitoliczbowym. Jeśli więc napiszemy coś takiego:

Int i; 
i = (int) 123456781234567890.999;

to rzutowanie musi skończyć się niepowodzeniem. Wartość umieszczona w zmiennej i będzie nieprawidłowa. W C# nie istnieje mechanizm sprawdzający tego typu błędy. Programista musi samodzielnie zadbać, aby nie wykroczyć poza zakres wartości obsługiwany przez zastosowany typ zmiennej — gdyby tak by się stało, to program by tego nie zauważył, lecz użytkownik z pewnością!

W C# nie traktowałbym tego jako błąd. Takie rzutowanie daje nam duże pole manewru, o ile wiemy co robimy…

Rzutowanie może spowodować utratę informacji także w inny sposób:

Int i; 
i = (int) 1.999;

W powyższym przykładzie rzutujemy wartość 1,999 (którą należałoby umieścić w zmiennej o podwójnej precyzji) do typu int. W wyniku tego procesu zignorowana zostaje część ułamkowa, przez co w zmiennej int ostatecznie znajdzie się wartość 1, mimo iż rzutowana liczba była bliższa 2. Należy pamiętać, że takie skrócenie liczby następuje zawsze w przypadku rzutowania wartości mającej część ułamkową (liczba dziesiętna, zmiennoprzecinkowa, zmiennoprzecinkowa o podwójnej precyzji) do typu, który części ułamkowych nie obsługuje.

3.6.3. Rzutowanie a literały

Jak już powiedzieliśmy, w programie możemy umieścić literały. To po prostu wartości, z których będziemy korzystać w działaniach. Na przykład w naszym kalkulatorze materiałów do produkcji okien musieliśmy pomnożyć długość drewna przez 3,25, aby zamienić metry na stopy (na jeden metr przypada ok. 3,25 stopy).

C# uważnie przygląda się wykorzystywanym w obliczeniach wartościom, dlatego przypisania tego typu:

Int i;
i = 3.4 / "głupek";

zostaną słusznie potraktowane z pogardą. Kompilator wie, że dzielenie wartości 3,4 przez łańcuch "głupek" jest niemądre. Spójrzmy jednak na inny przykład:

float x;
x = 3.4;

Choć na pierwszy rzut oka powyższy kod jest w pełni prawidłowy, to zawiera błąd. Liczba 3,4 wyrażona jako literał to wartość zmiennoprzecinkowa o podwójnej precyzji, natomiast zmienna x jest typu zmiennoprzecinkowego. Jeśli chcemy umieścić literał zmiennoprzecinkowy w zmiennej zmiennoprzecinkowej, to możemy zastosować rzutowanie:

Float x; 
x = (float) 3.4;

Dzięki temu literał zmiennoprzecinkowy o podwójnej precyzji zostanie rzutowany do typu zmiennoprzecinkowego, a przypisanie zakończy się powodzeniem.

By ułatwić programistom życie, twórcy C# dodali nowy sposób na wyrażanie literałów zmiennoprzecinkowych. Wystarczy umieścić bezpośrednio po wartości literę f, a będzie ona traktowana jako typ zmiennoprzecinkowy. W związku z tym kompilacja kodu:

Float x; 
x = 3.4f;

przebiegnie pomyślnie.

3.7. Typy danych C# w wyrażeniach

Gdy posługujemy się jakimś operatorem, C# decyduje jakiego typu ma być wynik działania. Co do zasady, jeśli dwa argumenty wyrażenia są liczbami całkowitymi, to i wynik powinien być całkowitoliczbowy. Analogicznie, w przypadku dwóch argumentów zmiennoprzecinkowych wartość wynikowa powinna być typu zmiennoprzecinkowego. Może to być kłopotliwe. Przykład:

1/2 
1/2.0

Wydawać by się mogło, że wynik obu tych działań będzie taki sam. Nic bardziej mylnego. Kompilator sądzi, że pierwsze wyrażenie, zawierające jedynie liczby całkowite, powinno zwrócić wynik całkowitoliczbowy. Musiałoby to więc być zero (część ułamkowa zawsze zostaje ucięta). W przypadku drugiego wyrażenia, ze względu na zawartą w nim liczbę zmiennoprzecinkową, kompilator zdecyduje, że wynik powinien być liczbą zmiennoprzecinkową o podwójnej precyzji, czyli dokładniejszą wersją wynikowej wartości 0,5.

Zachowanie operatora zależy od kontekstu użycia. Jak zobaczymy w dalszej części książki, operator +, wykonujący zwykle działanie matematyczne, można zastosować do połączenia łańcuchów. Przykładowo wynikiem wyrażenia "Ro" + "b" będzie "Rob".

Jeśli chcemy zyskać całkowitą kontrolę nad konkretnym rodzajem operatora, to w naszym programie musimy zawrzeć instrukcje rzutowania, dzięki którym nadamy operatorowi odpowiedni kontekst.

using System; 
class CastDemo
{
    static void Main () 
    {
        int i = 3, j = 2; 
        float fraction; 
        fraction = (float) i / (float) j; 
        Console.WriteLine ( "ułamek : " + fraction ); 
    } 
}

Przykład kodu 03. Rzutowanie

Znajdująca się w powyższym kodzie instrukcja rzutowania (float) mówi kompilatorowi, by traktował wartości przechowywane w zmiennych całkowitoliczbowych jako wartości zmiennoprzecinkowe — dzięki temu zamiast 1 wyświetlony zostanie wynik 1,5.

Złota myśl programisty: rzutowanie poprawia czytelność kodu

Mam w zwyczaju stosować rzutowanie nawet jeśli nie jest ono konieczne, ponieważ w ten sposób program staje się czytelniejszy. Nie wpływa ono na wynik działania, lecz informuje osobę czytającą kod o moich zamiarach.

3.8. Programy i wzorce działania w C#

Czas wrócić do naszego kalkulatora i przeanalizować jego działanie. Kod wykonujący podstawowe zadanie sprowadza się zaledwie do kilku wierszy, w których wczytujemy dane, zapisujemy je w zmiennej o odpowiednim typie, a następnie wykorzystujemy przechowywane wartości do obliczenia wyniku, który chce otrzymać użytkownik:

string widthString = Console.ReadLine(); 
double width = double.Parse(widthString);
string heightString = Console.ReadLine();
int height = double.Parse(heightString);
woodLength = 2 * ( width + height ) * 3.25;
glassArea = 2 * ( width * height );
Console.WriteLine ( "Długośćdrewna: " + woodLength + " stopy" ); 
Console.WriteLine("Powierzchniaszyby: " + glassArea + " m kw." );

Warto tutaj zauważyć pewien wzorzec działania programu, z którego możemy wielokrotnie korzystać w przyszłości.

W ramach przykładu wyobraźmy sobie, że mamy znajomego, który jest właścicielem apteki. Nasz zaprzyjaźniony farmaceuta potrzebuje programu, który obliczy całkowity koszt zakupu tabletek oraz liczbę potrzebnych opakowań na podstawie podanej ceny i liczby medykamentów. Zamawiane przez znajomego tabletki są zawsze sprzedawane w butelkach o maksymalnej pojemności 100 tabletek.

Wystarczy zatem w bardzo łatwy sposób zmodyfikować nasz obecny kalkulator. Jedyna trudność polega na obliczeniu liczby potrzebnych butelek. Jeśli po prostu podzielimy liczbę tabletek przez 100, to wynik przeprowadzonego dzielenia całkowitoliczbowego będzie błędny (w przypadku gdy liczba tabletek będzie mniejsza od 100 program poinformuje nas, że potrzeba 0 butelek).

Problem ten możemy rozwiązać poprzez dodanie 99 do liczby tabletek przed wykonaniem dzielenia. Dzięki temu zaokrąglona liczba potrzebnych butelek zawsze będzie większa od 0. Odpowiedzialna za obliczenia część naszego programu wygląda zatem następująco:

int bottleCount = ((tabletCount + 99) / 100);
int salePrice = bottleCount * pricePerBottle;

Teraz wystarczy tylko dodać pozostały kod:

Pamiętajcie, że w wynik dzielenia całkowitoliczbowego jest zawsze zaokrąglany do dołu, a część ułamkowa zostaje ucięta.

string pricePerBottleString = Console.ReadLine(); 
int pricePerBottle = int.Parse(pricePerBottleString);
string tabletCountString = Console.ReadLine(); 
int tabletCount = int.Parse(tabletCountString);
intbottleCount = ((tabletCount + 99) / 100) ;
intsalePrice = bottleCount * pricePerBottle;
Console.WriteLine ( "Liczba butelek wynosi " + bottleCount ); 
Console.WriteLine( "Całkowity koszt wynosi " + salePrice );

Co ciekawe, program dla farmaceuty stanowi tak naprawdę wariant programu dla kolegi szklarza. Oba wpisują się w pewien wzorzec (wczytują dane, przetwarzają je, wyświetlają wynik), który jest typowy dla wielu aplikacji. Jeśli zatem zostaniecie kiedyś poproszeni o napisanie programu, który ma wczytywać dane, obliczać na ich podstawie odpowiedź i zwracać wynik, to możecie skorzystać z tego wzorca.

Sztuka programowania polega między innymi na umiejętności dopasowania do problemu odpowiedniego wzorca, który umożliwi jego rozwiązanie.

Autor: Rob Miles

Tłumaczenie: Joanna Liana

Dyskusja

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *