Spis treści Poprzednia strona: Data i czas Następna strona: Instrukcja warunkowa
Operatory i wyrażenia są prawie takie same, jak w matematyce. Dlaczego tak? Bo komputery służą do wykonywania obliczeń, a tym zajmuje się matematyka. To właśnie w matematyce mamy operacje arytmetyczne i wyrażenia algebraiczne. Weźmy operator + oznaczający operację dodawania. Używamy go na przykład tak: a + b, tzn. wstawiamy go pomiędzy dwa operandy: lewy reprezentowany przez zmienną a i prawy reprezentowany przez zmienną b. Czym tak naprawdę są operatory w matematyce? To są funkcje dwuargumentowe! Funkcja dodająca (nazwijmy ją na przykład p, jak plus) ma taki wzór: p(x, y) = x + y. W języku C# operatorom także odpowiadają funkcje, a dokładnie specjalne statyczne metody klas tworzone przy pomocy słowa kluczowego operator. Można te metody pisać samemu de facto zmieniając (rozszerzając) sposób działania operatorów w samym języku C#. Nazywamy to przeciążaniem operatora. Składnia nieco różni się od tego, co możesz znać z języka C++. Nieco inny jest też zbiór operatorów, które można przeciążać. Nie można w C# definiować operatorów złożonych z własnych symboli. Mam cichą nadzieję, że kolejne wersje języka C# rozwiążą ten problem. Skoro można używać Unicode i zestaw symboli jest istotnie duży, to dlaczego z tego nie skorzystać?
Operatory arytmetyczne
Oczywiście podstawą są operatory arytmetyczne: + (czytany jako plus, czyli dodawanie), – (czytany jako minus, czyli odejmowanie), * (czytany jako „razy”, gwiazdka, czyli mnożenie), / (czytany jako „przez”, prawy ukośnik [slash], czyli dzielenie). Nie ma specjalnego operatora potęgowania, jak na przykład ** w języku Fortran. Napisanie dwóch zmiennych obok siebie tak: ab to inna zmienna o nazwie ab, a nie mnożenie a przez b. Podobnie 2a jest błędem i powinno być 2*a.
Dzielenie dwóch liczb całkowitych (typy int i pokrewne) przy pomocy operatora dzielenia / powoduje zaokrąglenie wyniku do najbliższej liczby całkowitej . Uwaga! To jest zaokrąglenie w stronę zera [towards zero], a nie zaokrąglenie w dół do największej liczby całkowitej nie mniejszej od ilorazu (bo to by wtedy była funkcja podłogi [floor]).
Dla liczb całkowitych mamy jeszcze jeden dodatkowy operator % oznaczający resztę z dzielenia i nie ma on nic (no może i ma, ale niewiele) wspólnego z obliczaniem procentów. Reszta z dzielenia a przez b oznaczana jest a % b i jest równa dokładnie tyle samo, co a – a / b * a. Oznacza to, że zawsze ma taki sam znak, jak a. Reszta może być zatem ujemna kiedy a jest ujemne. W matematyce często zakłada się jednak inaczej, tzn. że reszta jest zawsze nieujemna. Należy o tym pamiętać, kiedy robimy jakieś obliczenia na komputerze i spodziewamy się uzyskać takie same wyniki, jak w matematyce. Zapomnij o tym (tzn. zawsze o tym pamiętaj). Jeśli chcemy aby reszta r była zawsze nieujemna, to musimy zmienić także definicję wyniku dzielenia, czyli ilorazu q. Jeśli będzie q = Math.Sign(b) * Math.Floor((double)a / Math.Abs(b)), to reszta r będzie równa a – qb (czyli tak, jak ma być) oraz r będzie większe lub równe od zera i mniejsze od wartości bezwzględnej z b – tak jak tego zwykle chce matematyka.
Operatory + oraz – można używać stawiając je przed pojedynczym operandem (czyli czymś, co ma wartość liczbową, na przykład zmienna, stała, literał lub całe wyrażenie w nawiasach). Działają wtedy jak operatory jednoargumentowe: minus zmienia znak liczby, a plus nie zmienia znaku liczby (czyli w zasadzie nic nie robi).
Kolejność wykonywania działań jest taka sama, jak w matematyce, czyli najpierw obliczamy operatory jednoargumentowe, potem mnożenie i dzielenie w kolejności od lewej do prawej, a na końcu dodawanie i odejmowanie w kolejności od lewej do prawej. Kolejność tę możemy zmieniać (znowu tak samo jak w matematyce) przy pomocy nawiasów. Jedyna różnica polega na tym, że w języku C# do tego celu możemy używać tylko ( i ), czyli nawiasów półokrągłych [parentheses]. Nawiasy kwadratowe służą do obsługi tablic, a klamrowe (już je widzieliśmy!) do zaznaczania początku i końca bloku kodu albo bloku definicji albo bloku inicjalizacji tablicy, kolekcji, właściwości obiektu. Nawiasy mogą zawierać w sobie inne nawiasy, tzw. nawiasy zagnieżdżone i wyrażenia tego typu mogą być nieco uciążliwe. Każdy nawias prawy (zamykający) musi być poprzedzony nawiasem lewym (otwierającym). Musimy pilnować żeby każdy otworzony nawias zamknąć tam, gdzie należy, bo inaczej program będzie błędny. „Dobre” edytory kodu źródłowego (na przykład takie jak wbudowane w VS albo w SD) podświetlają (albo zmieniają im kolor tła) pary odpowiadających sobie nawiasów wtedy, kiedy kursor (a dokładniej karetka [caret], czyli mrugająca pionowa kreseczka) znajdzie się przed znakiem otwierającym albo za znakiem zamykającym dany nawias. Oczywiście para nawiasów sama w sobie jest operatorem jednoargumentowym. Działa w ten sposób, że oblicza wartość wyrażenia między nawiasami i zwraca tę wartość nie zmieniając jej (tak, jak jednoargumentowy plus).
W języku C# (a także w C i C++) funkcjonują jeszcze dwa inne arytmetyczne operatory jednoargumentowe. Operator inkrementacji ++ zwiększa wartość zmiennej o jeden, a operator dekrementacji --
zmniejsza wartość zmiennej o jeden. Jeśli występują przed nazwą zmiennej, to zwracają do zewnętrznego wyrażenia wartość po zmianie, a jeśli wystąpią po nazwie zmiennej, to zwracają do zewnętrznego wyrażenia wartość przed zmianą. Popatrzmy na przykłady:
int a, b = 5; a = 10 * b++; // a będzie równe 50, b będzie równe 6 a = 10 * ++b; // a będzie równe 70, b będzie równe 7 b = 100 - a--; // b będzie równe 30, a będzie równe 69 b = 100 + --a; // b będzie równe 168, a będzie równe 68
Operatory ++ i --
dotyczą one tylko zmiennych (właściwości i indeksery składniowo zachowują się tak samo jak zmienne) i nie można ich stosować do literałów ani do wartości złożonych wyrażeń. Edytor VS przypomina nam o tym, jeśli popełnimy błąd:
Operatory bitowe
Liczby w pamięci komputerowej są przechowywane w systemie binarnym (dwójkowym), czyli przy pomocy cyfr 0 i 1. Jest to system pozycyjny, tak samo jak system dziesiętny, którym posługujemy się na co dzień, tyle tylko, że kolejne cyfry oznaczają wielokrotności potęg dwójki, a nie potęg dziesiątki. Pojedynczy bit (równy zero albo jeden) jest najmniejszą możliwą ilością informacji (i to nie tylko w komputerach, ale chodzi tu o informację w ogóle). Operatory bitowe zmieniają każdy bit (cyfrę dwójkową) w liczbie osobno. Jednoargumentowa negacja ~ polega na zamianie wszystkich zer w jedynki i na odwrót. Dwuargumentowa koniunkcja & (iloczyn bitowy) daje na danej pozycji jedynkę wtedy i tylko wtedy, gdy na tej samej pozycji były jedynki w obu argumentach (operandach), a w przeciwnym razie daje zero. Dwuargumentowa alternatywa | (suma bitowa) na danej pozycji daje zero wtedy i tylko wtedy, gdy na tej samej pozycji były zera w obu argumentach (operandach), a w przeciwnym razie daje jedynkę. Dwuargumentowa alternatywa wykluczająca ^ [exclusive or] daje na danej pozycji jedynkę wtedy i tylko wtedy, gdy na tej samej pozycji były różne cyfry w obu argumentach (operandach), a w przeciwnym razie daje zero. Przesunięcie w lewo << o n odpowiada mnożeniu przez n-tą potęgę 2 i każda cyfra binarna na pozycji k idzie na pozycję k-n, a na ostatnich n pozycjach z prawej strony pojawiają się zera. Dla znających assembler tłumaczę, że jest to przesuwanie bez rotacji ani przeniesienia (przeniesienie – jeśli było – zostanie zawsze stracone). Podobnie przesunięcie w prawo >> o n odpowiada dzieleniu przez n-tą potęgę 2 i każda cyfra binarna na pozycji k idzie na pozycję k+n, a na początkowych n pozycjach z lewej strony pojawiają się zera.
Oczywiście należy uważać na liczby ujemne. Liczbą przeciwną do dodatniego n jest liczba ~(n – 1), a przeciwną do ujemnego n jest (~n + 1). Liczb ujemnych jest jakby o jedną więcej, bo zero jakby zalicza się do liczb dodatnich (tzn. jest w ten sam sposób reprezentowane). Taki sposób reprezentacji liczb ujemnych nazywamy zapisem uzupełnieniowym do 2.
Operatory przypisania
Operator przypisania wartości do zmiennej już w zasadzie znamy. Jest nim znak równości =. Posiada on jedną ciekawą cechę pozwalającą skrócić zapis niektórych wyrażeń i przy odrobinie wprawy można się do niego przyzwyczaić. Możemy połączyć inny operator dwuargumentowy z operatorem przypisania w taki sposób: a+=b, a-=b, a*=b, a/=b, a%=b, a^=b. a&=b, a|=b, a<<=b, a>>=b. Oznacza to dokładnie to samo, co taki zapis: a=a+b, a=a-b, a=a*b, a=a/b, a=a%b, a=a^b. a=a&b, a=a|b, a=a<<b, a=a>>b. Choć może to wyglądać dziwnie, nie ma tutaj żadnej magii. Nie można tych rozszerzonych operatorów przeciążać i zawsze działają tak samo, jak dwuargumentowe. Są one po prostu (razem z inkrementacją i dekrementacją) charakterystyczną cechą języków programowania z rodziny wywodzącej się od języka C. Kolejną cechą charakterystyczną jest to, że operacja przypisania nie jest instrukcją, ale wyrażeniem i sama ma wartość (jest to wartość, która została ostatecznie przypisana, czyli ta, która wylądowała w zmiennej po lewej stronie operatora przypisania). Powoduje to, że można łączyć operacje przypisania i zapis a = b = c = 7; oznacza tyle, co c = 7; b = c; a = b; Operatory przypisania są więc prawostronnie łączne, czyli a = b = c oznacza a = (b = c).
Operatory relacji
Wynikami operacji arytmetycznych są liczby, ale operatory relacji dają wynik typu bool, czyli prawdę (true) albo fałsz (false). Argumentami operatorów < (mniejsze od), > (większe od), <= (mniejsze lub równe, nie większe od), >= (większe lub równe, nie mniejsze od) mogą być zmienne podstawowych typów wartościowych, literały tych typów oraz wszystkie inne typy dla których programista zdefiniował znaczenie tych operatorów (bo domyślnie znaczenia nie mają więc i używać ich nie wolno z obiektami dowolnej klasy). Operatory == (jest równe) i != (jest różne) mogą być dodatkowo stosowane do typów referencyjnych i sprawdzają wtedy czy referencja dotyczy tego samego obiektu. Operator is służy do sprawdzania typu zmiennej i jego lewym operandem jest nazwa zmiennej lub wyrażenie, a prawym nazwa typu.
Operatory logiczne
Operatory logiczne podobnie jak relacyjne zwracają wartość logiczną typu bool, ale ich argumentami mogą być tylko inne wartości logiczne bool. Mamy tutaj jednoargumentową negację ! zmieniającą prawdę w fałsz, a fałsz w prawdę, iloczyn logiczny && prawdziwy wtedy i tylko wtedy, gdy wszystkie argumenty są prawdziwe (fałszywy gdy co najmniej jeden jest fałszywy) i sumę logiczną || fałszywą wtedy i tylko wtedy, gdy wszystkie argumenty są fałszywe (prawdziwą gdy co najmniej jeden jest prawdziwy). Zamiast podwójnych znaków && oraz || można stosować pojedyncze & oraz |, ale nie zaleca się tego przez wzgląd na kompatybilność z językami C i C++ w których nie było to możliwe. Gdyby ktoś chciał skopiować nasz kod do jednego z tych języków nie wiedziałby które znaki pojedyncze zamienić na podwójne a bez jakiejkolwiek zamiany może to spowodować działanie odmienne od zamierzonego, o te operatory działają inaczej w innych językach.
Operatory rzutowania na typ (i nie mówcie na nie proszę operatory rzutowe, bo fizycy kwantowi przewrócą się w grobie, jak to usłyszą)
Wyglądają tak (typ), czyli nazwa typu umieszczona w nawiasach. Stosuje się je tak, że umieszcza się je przed zmienną lub wyrażeniem, którego typ wartości chcemy zmienić. Mają dość wysoki priorytet, na równi z operatorami jednoargumentowymi. To, czy są one potrzebne, czy są wymagane, a także to co one tak naprawdę robią – to wszystko zależy od tego jaki jest typ w nawiasach i jaki jest typ wyrażenia za nawiasami. Rzutować [cast] to nie znaczy to samo, co konwertować [convert], choć czasami może być zaimplementowane tak samo. Rzutowanie typu wartościowego na typ object to tzw. boxing (zapudełkowanie), a rzutowanie zmiennej typu object która powstała w rezultacie boxingu na ten sam typ wartościowy to tzw. unboxing (odpudełkowanie). Wszystko stanie się jasne jak słońce, ale dopiero wtedy, kiedy dojdziemy do przeciążania operatorów rzutowych w naszych własnych klasach, ale na to jeszcze za wcześnie.
Operator dostępu do składowej
Jest to po prostu kropka. Kropkę stawiamy między nazwami przestrzeni nazw i jej podprzestrzeni, między przestrzenią nazw a nazwą klasy, między nazwą klasy lub obiektu tej klasy, a nazwą metody, pola lub właściwości tej klasy (tego obiektu). Kropka używana jest w liczbach zmiennoprzecinkowych (właśnie przecinkowych a nie kropkowych) do oddzielania części całkowitej od części ułamkowej danej liczby. Kropkę stawiamy też na końcu zdania, ale tylko jeśli zdanie jest zawarte w literałach typu string 😉
Spotykamy też inne operatory: warunkowy ?:, koalescencji ??, aliasu ::, indeksacji [], wywołania (), delegacji =>, kreacji new, a także częściowo odziedziczone po języku C++ operatory operacji na wskaźnikach używane tylko i wyłącznie w trybie unsafe (niezarządzanym). Czasem mówi się o operatorach, które są jednocześnie słowami kluczowymi i można z nich korzystać jak z funkcji: inicjalizacja default, sprawdzanie nadmiaru arytmetycznego checked, wyłączenie tego sprawdzania unchecked, pobranie rozmiaru sizeof, pobranie typu typeof, czy nowy operator (od C# 4.5) oczekiwania na koniec innego wątku await.
W języku C# (w przeciwieństwie do języka C++) przecinek nie jest operatorem tworzenia listy wyrażeń, w ogóle nie ma operatora delete, a operatorów kreacji, wywołania metody i dostępu -> nie można przeciążać.
Spis treści Poprzednia strona: Data i czas Następna strona: Instrukcja warunkowa