Użycie zmiennych
Przegląd zagadnień
Podczas definicji algorytmu było wspomniane pojęcie dane, czyli coś co przetwarza, na czym pracuje algorytm. Dane w przypadku programu komputerowego nazywamy zmiennymi. Zmienną w pewnym uproszczeniu można zdefiniować jako pewien obszar pamięci (ciąg bitów), do którego można się odwołać przy pomocy pewnej nazwy, nadanej temu obszarowi przez programistę. Przy pomocy nazwy zmiennej, lub krócej zmiennej, możemy "manipulować" (zmieniać, pobierać wartość) bitami obszaru pamięci reprezentowanym przez zmienną. Przy tych "manipulacjach" wykorzystujemy pewne instrukcje zwane operatorami.
Po skończeniu tego modułu studenci powinni:
Potrafić definiować zmienne.
Znać podstawowe operatory w języku C#.
Definicja zmiennej
Definiując zmienną oprócz jej nazwy, czyli identyfikatora zmiennej, musimy podać jej typ. Typ określa:
liczbę bajtów zajmowanych przez zmienną,
sposób interpretacji bitów zmiennej,
czyli można powiedzieć, że typ definiuje zbiór wartości, jakie może przyjmować zmienna. Typ również określa zbiór operacji, jakie możemy przeprowadzać na zmiennej.
W tej części modułu studenci poznają:
Dostępne typy w języku C#.
Sposób definicji zmiennej.
Dopuszczalne identyfikatory.
oraz nauczą się
Nadać zmiennej żądaną wartość.
Wypisać wartość zmiennej na ekranie.
Kopiować wartość zmiennej do zmiennej innego typu.
Wspólny system plików
Wspólny system plików (Common Type System - CTS) - określa w jaki sposób środowisko uruchomieniowe (CLR) definiuje i używa typy oraz jak nimi zarządza. Dostarcza również zorientowany obiektowo model oraz definiuje reguły, które muszą być stosowane we wszystkich językach programowania, których używane są do tworzenia oprogramowania na platformę .NET. Reguły te pozwalają częściom programu napisanych w różnych językach współpracować ze sobą. Mówiąc prościej, nasz program napisany w C#, może używać typów zdefiniowanych np. w bibliotece napisanej w Visual Basic-u.
Wszystkie typy w języku C#, możemy podzielić na dwie rozłączne kategorie:
Typy wartości
Typy referencyjne
Obie kategorie typów zostaną dokładnie omówione w dalszej części rozdziału.
Uwaga:
W języku C# wszystkie typy dziedziczą po klasie Object. Pojęcie dziedziczenia jest dokładnie omówione w kursie "Programowanie obiektowe".
Typy wbudowane
Język C# dostarcza pewien zbiór typów wbudowanych. Typy te są reprezentowane przez zarezerwowane wyrazy - słowa kluczowe. Każdy z typów wbudowanych ma swój odpowiednik (alians) w przestrzeni nazw System. Typy wbudowane możemy podzielić na następujące kategorie:
Typy wartości
typ logiczny: bool
typy numeryczne
typy całkowite: sbyte, byte, short, ushort, int, uint, long, ulong, char
typy rzeczywiste: float, double
typ rzeczywisty o dużej precyzji: decimal
Typy referencyjne:
object,
napisy: string
W poniższej tabeli zostały opisane wszystkie typy wbudowane. Zostało podane słowo kluczowe identyfikujące typ, nazwa typu w przestrzeni nazw System, liczba bajtów zajmowanych przez zmienne danego typu oraz przedział dopuszczalnych wartości.
Słowo kluczowe |
Alians |
Liczba bajtów |
Przyjmowane wartości |
bool |
Boolean |
1 |
true lub false |
sbyte |
SByte |
1 |
od -128 do 127 |
byte |
Byte |
1 |
od 0 do 255 |
short |
Int16 |
2 |
od -32768 do 32767 |
ushort |
UInt16 |
2 |
od 0 do 65535 |
int |
Int32 |
4 |
od -2147483648 do 2147483647 |
uint |
UInt32 |
4 |
od 0 do 4294967295 |
long |
Int64 |
8 |
od -9223372036854775808 |
ulong |
UInt64 |
8 |
od 0 do 18446744073709551615 |
char |
Char |
2 |
od 0 do 65535 |
float |
Single |
4 |
±1.5 × 10e−45 do ±3.4 × 10e38 |
double |
Double |
8 |
±5.0 × 10−e324 to ±1.7 × 10e308 |
decimal |
Decimal |
12 |
±1.0 × 10e−28 to ±7.9 × 10e28 |
object |
Object |
- |
typ podstawowy dla wszystkich innych typów |
string |
String |
- |
Napis - dowolny ciąg znaków |
Wybór typu dla zmiennej jest determinowany przez jej zastosowane, czyli żądane wartości. W przypadku typów reprezentujących liczby rzeczywiste oprócz wielkości przedziału (jak duże liczby), bardzo ważna jest również precyzja - ilość cyfr znaczących. Tylko skończoną ilość liczb możemy zakodować na skończonej ilości bajtów.
Deklaracja zmiennych
Przed użyciem zmiennej musimy ją zadeklarować, czyli podać jej typ i nazwę np.:
int x;
Powyższa linijka rezerwuje cztery bajty pamięci (tyle zajmuje typ int) i nadaje temu obszarowi pamięci w naszym programie nazwę x.
Uwaga:
Kolejność deklaracji jest ważna tylko przy deklaracji zmiennych lokalnych, czyli deklaracji w obrębie bloku metody. W przypadku deklaracji zmiennych jako pól klasy, dostępne one są w całym bloku klasy, zarówno przed jak i po deklaracji. Podobnie jest z przestrzenią nazw. Nazwa typu zdefiniowanego bezpośrednio w bloku przestrzeni nazw lub bloku klasy jest dostępna w całym bloku przestrzeni nazw lub bloku klasy. Można powiedzieć, że przestrzeń nazw nie jest liniowa.
Zmienne możemy deklarować tylko w bloku metody, tzw. zmienne lokalne oraz w bloku klasy (struktury), tzw. pola klasy (struktury). W języku C# nie ma zmiennych globalnych, czyli deklarowanych bezpośredni w bloku przestrzeni nazw lub poza jakimkolwiek blokiem.
Można deklarować wiele zmiennych w pojedynczej deklaracji. Poszczególne nazwy zmiennych oddzielamy przecinkami. Przykład deklaracji trzech zmiennych typu double:
double a,b,c;
W miejscu deklaracji zmiennej lub zmiennym również możemy nadać wartość. W tym celu po nazwie zmiennej umieszczamy znak równości i następnie podajemy wartość zmiennej (literał lub wyrażenie, którego typ jest zgodny z typem zmiennej):
int p =10;
int q = 2*p, r = 4;
Uwaga:
Literały i wyrażenia zostaną omówione w dalszej części tego modułu.
Nazwy, które możemy nadawać zmiennym, czyli ich identyfikatory, muszą spełniać następujące reguły:
Nazwa może składać się z ciągu liter, cyfr i znaku podkreślenia. Nazwa nie może zawierać, więc takich znaków jak np.: spacja, *, +. !.
Nazwa nie może zaczynać się od cyfry, czyli niedopuszczalna jest deklaracja:
int 1x;
natomiast następująca deklaracja jest poprawna:
int x1;
Nazwa nie może być słowem kluczowym. Słowa kluczowe są to wyrazy zastrzeżone dla danego języka programowania. W języku C# występują następujące słowa kluczowe:
abstract event new struct
as explicit null switch
base extern object this
bool false operator throw
break finally out true
byte fixed override try
case float params typeof
catch for private uint
char foreach protected ulong
checked goto public unchecked
class if readonly unsafe
const implicit ref ushort
continue in return using
decimal int sbyte virtual
default interface sealed volatile
delegate internal short void
do is sizeof while
double lock stackalloc
else long static
enum namespace string
Istnieją wyrazy w języku C#, które stają się słowami kluczowymi dopiero w pewnym kontekście. Należą do nich:
get partial set
value where yield
Visual Studio przy domyślnych swoich ustawieniach wszystkie słowa kluczowe oznacza kolorem niebieskim.
Uwaga:
Można używać słów kluczowych jako identyfikatorów stosując prefiks @: int @if;. Znak @ nie jest częścią identyfikatora. Sposób ten jest mocno niezalecany i stosuje się go, gdy łączymy program, z bibliotekami napisanymi w innych językach, w których występują identyfikatory będące słowami kluczowymi języka C#.
Nazwa musi być unikalna w danym bloku kodu. W odróżnieniu od języka C++, w bloku zagnieżdżonym nie można przesłaniać identyfikatorów z bloku nadrzędnego:
{
...
int x;
...
{
double x; //błąd, ta sama nazwa
}
...
}
ale
{
...
int x;
...
}
{
...
double x; //OK, inny blok
...
}
Uwaga:
Od te reguły występują pewne wyjątki. Np.: w klasie można zadeklarować zmienną o danym identyfikatorze (blok klasy), a następnie w metodzie klasy użyć ponownie tego identyfikatora dla nowej zmiennej (blok metody jest zagnieżdżony w bloku klasy).
Istnieje też szereg wskazówek, co do tworzenia identyfikatorów. Nieprzestrzeganie ich nie powoduje błędów. Stosowanie się jednak do nich świadczy o dobrym stylu i może ułatwić późniejszą analizę.
Stosuj identyfikatory mówiące, do czego służy zmienna. Nazwa powinna mieć konkretne znaczenie.
Zawsze używaj formatu PascalCase lub formatu camelCase przy tworzeniu nazw. W formacie PascalCase każdy wyraz, z których składa się identyfikator, rozpoczynamy wielką literą np.:WriteLine, ReadLine, Console, SByte. Formatu PascalCase używamy dla nazw typów, stałych nazwanych, właściwości, przestrzeni nazw.
W przypadku formatowania camelCase pierwszy wyraz będący częścią identyfikatora piszemy małą literą, pozostałe rozpoczynamy wielką literą. np.: loopCountMax. Formatu camelCase używamy dla nazw zmiennych lokalnych, parametrów funkcji.
Unikaj znaku podkreślenia.
Nie używaj notacji węgierskiej. W notacji węgierskiej w prefiksie była zawarta informacja o typie zmiennej.
Unikaj skrótów i akronimów. Jeżeli już używamy skrótów, powinny być one powszechnie znane i rozumiane.
Stosujemy w miarę możliwości nazwy anglojęzyczne. Szczególnie jest to ważne przy tworzeniu bibliotek.
Więcej na temat konwencji stosowanych przy tworzeniu nazw można znaleźć w MSDN pod tematem: “Naming Guidelines” oraz na stronach www: http://home.comcast.net/~lancehunt/CSharp_Coding_Standards.pdf, http://www.idesign.net/idesign/download/Idesign Csharp Coding Standard.zip
Literały
Literał jest to część kodu źródłowego, która reprezentuje konkretną wartość danego typu. Może służy na przykład do nadawania zmiennym żądanych wartości.
Do oznaczenia wartości typu logicznego używamy wyrazów true (prawda) oraz false (fałsz) np.:
bool isHeight = true;
Do określania wartości liczb całkowitych możemy użyć systemu dziesiętnego lub szesnastkowego. W przypadku systemu dziesiątkowego wartość zapisujemy tak jak w matematyce:
int heiht = 234;
int debt = -23; //wartość ujemna
W przypadku systemu szesnastkowego przed ciągiem cyfr szesnastkowych umieszczamy 0x. Do oznaczenia brakujących cyfr szesnastkowych używamy liter: a, b, c, d, e, f.
long weight = 0x2a; //42 w dziesiętnym
Samemu literałowi jest przyporządkowany pierwszy z typów int, uint, long albo ulong w którym dana wartość jest reprezentowana. Możemy również w sposób jawny określić typ literału przez dodanie sufiksu:
u - uint albo ulong, gdy w typie uint się nie mieści,
l - long albo ulong, gdy w typie long się nie mieści
ul, lu - ulong, np.:
long depth = 23l;
Wielkość liter w przypadku sufiksów nie ma znaczenia
Uwaga:
Gdy wartość literału jest poza zakresem typu ulong generowany jest błąd kompilatora.
Stałe rzeczywiste możemy wyrażać:
przy pomocy znaku kropki:
double x = 3.54;
double y = .34;
double z = -3.0;
stosując tzw. postać naukową:
double x = 354e-2;
double y = 3.4e-1;
double z = -0.03e2;
gdzie np.: 354e-2 = 354 * 10 -2
używając sufiksów
double a = 2d;
float b = 4.4f;
decimal c = 6m;
Wielkość liter w przypadku sufiksów nie ma znaczenia.
Literałom rzeczywistym bez dodanego sufiksu przyporządkowany jest typ double.
W przypadku literałów rzeczywistych, gdy wartość literału nie ma dokładnej reprezentacji w danym typie, ale mieści się w zakresie dopuszczalnych wartości danego typu, jest zaokrąglana. W przypadku, gdy wartość literału jest poza zakresem dopuszczalnych wartości dla danego typu rzeczywistego, generowany jest błąd kompilatora.
Stałe znakowe reprezentują pojedynczy znak i zwykle zawierają dany znak umieszczony w apostrofach.
char c = 'a', d = '1';
Wewnątrz apostrofów możemy również umieścić pewną sekwencję znaków, która reprezentuje pojedynczy znak. Sekwencja ta rozpoczyna się od znaku lewego ukośnika (backslash). Poniżej umieszczono dopuszczalne sekwencje znaków:
Sekwencja Znak Nazwa znaku
\' ' apostrof
\" " cudzysłów
\\ \ lewy ukośnik (backslash)
\0 null
\a sygnał dźwiękowy
\b cofnięcie o jeden znak
\f nowa strona
\n nowy wiersz
\r powrót karetki
\t tabulacja pozioma
\v tabulacja pionowa
Ponadto można skorzystać z szesnastkowej sekwencji znaków (hexadecimal escape sequence), która jest poprzedzona znakami \x oraz sekwencji w kodzie Unicode (Unicode character escape sequence), która jest poprzedzona znakami \u. W obu przypadkach podajemy szesnastkowy kod Unicode żądanego znaku.
char c='\x4d'; //można też c = '\x004d';
char d = '\u004d';
Uwaga:
W języku C# sekwencja w kodzie Unicode (Unicode character escape sequence) może rozpoczynać się od znaków \U po którym należy umieścić ciąg składający się z ośmiu cyfr szesnastkowych. Znaki o kodach z przedziału U+10000 do U+10FFFF nie są dozwolone w literałach znakowych i napisowych. Znaki o kodzie powyżej 0x10FFFF nie są w ogóle wspierane w języku C#.
Literał napisowy jest ciągiem znaków ujętym w cudzysłów:
string s = "Dowolny napis";
Jest to tzw. regularny literał napisowy. Może on zawierać zero lub więcej znaków, ale nie może być dzielony na dwie i więcej linii oraz nie może zawierać znaku " i pojedynczego znaku \. Może natomiast zawierać sekwencje znaków rozpoczynających się od lewego ukośnika, które zostały opisane przy literałach znakowych. Polecenie:
Console.WriteLine("\"Pan Tadeusz\"");
spowoduje pojawienie się na ekranie:
"Pan Tadeusz"
Natomiast po wykonaniu polecenia:
Console.WriteLine("Cześć!\nTu Twój komputer.");
Na ekranie pojawi się napis w dwóch liniach:
Cześć!
Tu Twój komputer.
W C# istnieją również literały napisowe dosłowne (verbatim-string-literal). Różnią się od regularnych literałów napisowych tym, że przed cudzysłów otwierający wstawiamy znak małpy @.
string s = @"Dowolny napis";
W literałach napisowych dosłownych znak lewy ukośnik nie ma żadnego specjalnego znaczenia. Polecenie:
Console.WriteLine(@"Cześć!\nTu Twój komputer.");
spowoduje pojawienie się na ekranie:
Cześć!\nTu Twój komputer.
Jeżeli chcemy uzyskać cudzysłów " w literale napisowym dosłownym, należy użyć dwóch znaków cudzysłów "". Polecenie
Console.WriteLine(@"""Pan Tadeusz""");
spowoduje pojawienie się na ekranie:
"Pan Tadeusz"
Literały napisowe dosłowne mogą rozciągać się na wiele linii.
Do deklaracji zmiennej można dodać modyfikator const. Otrzymujemy w ten sposób stałą nazwaną, często nazywaną po prostu stałą.
const int SpeedLight = 300000; //km/s
Wartość stałych nazwanych musi być podana w miejscu deklaracji i później jej nie można zmieniać. Wartość wyrażenia inicjalizującego stałą nazwaną jest obliczana w czasie kompilacji. Wyrażenie zatem nie może zawierać odwołania do zmiennych. Może natomiast zawierać odwołania do innych stałych.
Przykładami stałych zdefiniowanych w bibliotece .NET są:
System.Math.PI; //liczba pi
System.Math.E; //podstawa logarytmu naturalnego
Zastosowanie stałych nazwanych, zamiast bezpośrednio literałów, ułatwia zrozumienie i analizę programu. Program staje się bardziej czytelny.
Uwaga:
Stała nazwana zdefiniowana jako pole klasy jest automatycznie polem statycznym tej klasy.
Pobieranie i wypisywanie wartości zmiennych
W programie niektóre wartości zmiennych muszą być wprowadzone przez użytkownika programu. W aplikacjach konsolowych wykorzystuje się do tego najczęściej metodę ReadLine klasy Console. Metoda ta udostępnia dane wprowadzone przez użytkownika w formie napisu - zmiennej typu string. Napis trzeba zamienić, przekonwertować, na wartość żądanego typu. Można wykorzystać do tego metody klasy Convert: ToInt32, ToBoolean, ToDecimal...
Czyli aby pobrać od użytkownika wartość liczby rzeczywistej należy napisać następujący kod:
string s = Console.ReadLine();
double x = Convert.ToDouble(s);
lub krócej:
double x = Convert.ToDouble(Console.ToString());
W celu wypisania wartości zmiennej na ekranie możemy użyć metody Write lub WriteLine w następujący sposób:
Console.WriteLine(nazwaZminnej);
Console.Write(nazwaZminnej);
Często gdy wypisujemy wartość zmiennej, łączymy to z pewnym opisem. Metoda Write i WriteLine umożliwia również wypisanie wartości kilku zmiennych w pojedynczym wywołaniu metody. Załóżmy, że mamy zmienne:
x = 10, y = 20, z = 30. Plecenie:
Console.WriteLine("x={0}, y={1}, z={2}",x,y,z);
spowoduje wypisanie na ekranie:
x=10, y=20, z=30
Z powyższego przykładu widać, że znaki {index} zostały zastąpione wartością odpowiedniej zmiennej. Index wskazuje numer pozycji argumentu, którego wartość należy wstawić w danym miejscu. Argumenty indeksujemy od zera.
Uwaga:
Gdy wartość indeksu jest większa lub równa liczbie argumentów zgłaszany jest wyjątek.
Gdy chcemy wypisać znak nawias klamrowy otwierający {, musimy podać dwa nawisy klamrowe otwierające {{, aby wyświetli znak nawias klamrowy zamykający }, należy podać dwa nawisy klamrowe zamykające }}.
W celu zademonstrowania wprowadzania i wypisywania wartości zmiennych napisz program obliczający sumę dwóch liczb rzeczywistych. Gotowy program jest dostępny w katalogu Kurs\Demo\Modul2\Modul2.sln projekt Dodawanie, gdzie Kurs jest katalogiem gdzie skopiowano pliki kursu
Uruchom Visual Studio
Utwórz aplikację konsolową o nazwie: Dodawanie
W metodzie Main napisz następujący kod:
static void Main(string[] args)
{
double x, y, suma;
Console.Write("Podaj x: ");
x = Convert.ToDouble(Console.ReadLine());
Console.Write("Podaj y: ");
y = Convert.ToDouble(Console.ReadLine());
suma = x + y;
Console.WriteLine("{0}+{1}={2}", x, y, suma);
Console.ReadKey();
}
Zbuduj i uruchom program.
Jako wartość zmiennej x wprowadź:
2.5 Enter
Prawdopodobnie program został przerwany. Został zgłoszony wyjątek. Spowodowane jest to tym, że metody klasy Convert przy zamianie wykorzystują domyślnie informacje o formatowaniu liczb, dat, godziny, znaku symbolu dziesiętnego itp. z ustawień systemu (Panel Sterowania, Opcje regionalne i językowe). W przypadku, gdy wszystkie formaty ustawione są dla Polski, symbolem dziesiętnym jest przecinek.
Uwaga:
Tak naprawdę ustawienia regionalne (formatowanie liczb, daty) zawarte są we właściwościach bieżącego wątku (Thread.CurrentThread.CurrentCulture) i można je modyfikować w programie, czyli mogą być niezależne od ustawień systemowych.
Z menu Debug wybierz pozycję Stop Debugging.
Uruchom program ponownie i wprowadź wartości zmiennych w odpowiednim formacie.
Wypisywaną wartość można poddać formatowaniu. Uzyskuje się to przez dodanie kilku znaków między nawiasy klamrowe. Oto postać ogólna:
{index[,n][:symbol]}
index - jak już wspominano, numer argumentu który należy wypisać.
n - liczba pozycji (miejsc), które zajmuje dana wartość. Brakujące miejsca uzupełniane są spacjami. Wartość dodatnia oznacza wyrównywanie do prawej, ujemna do lewej.
symbol - pewna litera oznaczająca sposób formatowania liczb. Po literze może wystąpić liczba z przedziału od 0 do 99 oznaczająca precyzję. Dostępne litery:
C lub c - dodawany jest symbol waluty
D lub d - format tylko dla liczb całkowitych. Precyzja określa z ilu cyfr składa się liczba. Gdy liczba składa się z mniejszej ilości cyfr brakujące miejsca uzupełniane zerami z lewej strony.
E lub e - liczba jest wyświetlana w notacji naukowej - postać wykładnicza. Jedna cyfra zawsze poprzedza znak dziesiętny.
F lub f - liczba jest konwertowana do postaci stałopozycyjnej (fixed-point).
G lub g - liczba jest konwertowana do najbardziej zwięzłej postaci (notacja naukowa lub stałopozycyjna)
N lub n - wstawiany jest separator tysięcy.
X lub x - format tylko dla liczb całkowitych. Liczba jest wyświetlana w systemie szesnastkowym.
P lub p - wartość liczby jest mnożona przez sto i dodawany jest znak procentu.
R lub r - gwarantuje, że po skonwertowaniu liczby do napisu (stringu) i późnie wykonaniu konwersji odwrotnej (napisu do liczby) otrzymamy tą samą wartość numeryczną.
Więcej informacji można znaleźć w MSDN Library pod tematem "Standard Numeric Format Strings".
Przykłady zastosowania formatowania można znaleźć w programie Kurs\Demo\Modul2\Modul2.sln projekt Formatowanie, gdzie Kurs jest katalogiem gdzie skopiowano pliki kursu.
W podobny sposób, możemy tworzyć napisy przy pomocy metody Format klasy String:
string s;
s = String.Format("{0} + {1} = {2}", 2, 2, 2+2);
Zmienna s będzie zawierała następujący napis: 2 + 2 = 4.
Typ wartości
Jak wspominano wcześniej w tym module, każdy typ należy do jednej z dwóch kategorii typów: typy wartości lub typy referencyjne.
Do typów wartości należą:
Typy wbudowane numeryczne(byte, sbyte, short, ushort, int, uint, long, ulong, char, float, double, decimal).
Typ wbudowany bool (logiczny).
Typy wyliczeniowe (typ ten zostanie omówiony w dalszej części tego modułu).
Struktury - jest to typ definiowany przez użytkownika. Więcej o tym typie będzie w rozdziale 11 - "Struktury"
Zmienne typu wartości bezpośrednio zawierają dane, dlatego też niektórzy nazywają typ ten typem bezpośrednim. Każda zmienna typu wartości reprezentuje oddzielny obszar pamięci, dlatego operacja na jednej zmiennej nie wpływa na drugą. Zmienne lokalne przechowywane w obszarze pamięci zwanym stosem. Z miejscem przechowywania i sposobem zarządzania tym obszarem związany jest czas życia zmiennej. Czas życia zmiennej możemy zdefiniować jako czas, przez który jest zarezerwowana pamięć dla zmiennej. Pamięć jest zarezerwowana dla zmiennej lokalnej od miejsca jej zadeklarowania do końca bloku, w którym została zadeklarowana.
{
double x; //początek "życia" zmiennej x
...
{
...
int y; ////początek "życia" zmiennej y
...
} //koniec "życia" zmiennej y
...
} //koniec "życia" zmiennej x
Uwaga:
Zasada działania stosu zostanie omówiona dokładnie w rozdziale 11, natomiast pojęcie zmiennej lokalnej w rozdziale 8 tego kursu.
Przed użyciem zmienna musi być zainicjalizowana - musi mieć nadaną wartość:
int x;
Console.Write(x); //Błąd kompilacji
Zmienne typu wartości posiadają zawsze jakąś wartość, nie mogą być null.
Uwaga:
W wersji C# 2.0 dla wszystkich typów wartości wprowadzono pewną formę typu (nullable types), której zmienne mogą przyjmować wartości null (wartość nieokreślona). Typ ten uzyskujemy przez dodanie do końca nazwy typu wartości znaku zapytania ?.
int? x;
x = null; //OK
int y;
y = null; //Błąd kompilacji
Typy nullable wprowadzono, aby ułatwić współpracę z bazami danych.
Typ wyliczeniowy
Typ wyliczeniowy jest to typ definiowany przez użytkownika (programistę). Stosujemy go, gdy chcemy, aby zmienne tego typu mogły przyjmować wartości z określonego zestawu stałych. Definiuje się go przy pomocy słowa kluczowego enum, po którym występuje nazwa nowego typu, a następnie w nawiasach klamrowych podajemy nazwy symboliczne dopuszczalnych wartości, oddzielone przecinkami. Nazwy, jakie możemy nadawać typom wyliczeniowym oraz poszczególnym elementom, podlegają tym samym zasadom co identyfikatory zmiennych. Oto przykład definicji tego typu:
enum PoryRoku {Wiosna, Lato, Jesien, Zima};
Domyślnie pierwszy element ma przypisaną wartość 0, wartość każdego kolejnego elementu jest większa o jeden od wartości poprzednika. Można również przypisać w sposób jawny wartości poszczególnym elementom np.:
enum Kolory{czerwony=4, zielony, niebieski=7};
Element zielony ma w sposób niejawny przypisaną wartość 5.
Wartości elementom typu wyliczeniowego mogą być nadawane w dowolnej kolejności, a nawet można im przypisać wartości jednakowe, gdy kilka symboli oznacza tę samą wartość.
Uwaga:
Typy wyliczeniowe, jak inne typy definiowane przez użytkownika (klasy, struktury), nie można definiować wewnątrz bloku metody.
Oto prosty przykład użycia typu PoryRoku:
PoryRoku sezon = PoryRoku.Wiosna;
Console.WriteLine("Ulubiona pora roku: {0}", sezon);
Na ekranie zostanie wypisane:
Ulubiona pora roku: Wiosna.
Widać więc, że zamiast wartości liczbowej zmiennej zostanie wypisana wartość symboliczna.
Przykłady zastosowania typu wyliczeniowego można znaleźć w programie Kurs\Demo\Modul2\Modul2.sln projekt PoryRoku oraz Konsola, gdzie Kurs jest katalogiem gdzie skopiowano pliki kursu. Konwersja wykorzystywana w projekcie PoryRoku jest opisana w dalszej części tego modułu.
Uwag:
Przy definicji typu wyliczeniowego można podać, jaki typ całkowity jest typem bazowym dla definiowanego typu wyliczeniowego. Typem bazowym może być dowolny tym całkowity poza typem char.
enum PoryRoku : byte {Wiosna, Lato, Jesien, Zima};
Przy tworzeniu nazwy typu wyliczeniowego, podobnie jak w przypadku nazw innych typów, stosuje się format PascalCase.
Stosowanie typu wyliczeniowego, podobnie jak stałych nazwanych czyni nasz kod bardziej czytelnym. Oprócz tego:
Zapewnia, że zmiennym są przypisane odpowiednie wartości.
W środowisku Visual Studio ułatwia pisanie kodu dzięki automatycznemu uzupełnianiu nazw (IntelliSense).
Typ referencyjny
Drugą kategorią typów są tak zwane typy referencyjne. Do typów referencyjnych zaliczamy:
Typy wbudowane string i object
Klasy
Interfejsy
Delegacje
Tablice
Klasy, interfejsy oraz delegacje zostaną dokładnie omówione w kursie "Programowanie obiektowe". Tablice zostaną omówione w rozdziale szóstym i siódmym tego kursu.
Zmienne typów referencyjnych zawierają odwołanie, nie zawiera bezpośrednio danych. Dane (obszar pamięci) do których odwołuje się zmienna nazywamy obiektem. Zmienne, które nie odwołują się do żadnego obiektu, posiadają wartość null, nie mylić ze zmiennymi niezainicjalizowanymi. Często stosuje się pewien skrót myślowy i zmienną nazywa się obiektem danego typu. Obiekty przechowywane są na w obszarze pamięci, który nazywa się zarządzą stertą.
Dwie zmienne danego typu referencyjnego mogą odwoływać się do tego samego obiektu, więc operacja na jednej zmiennej może zmieć dane innej zmiennej.
Obiekty tworzy się przy pomocy słowa kluczowego new, chociaż istnieją pewne uproszczenia składni np. typ string. Przykład utworzenia obiektu klasy Random, która reprezentuje generator liczb pseudolosowych.
Random r; //deklaracja zmiennej
//r.Nex(0,101); //błąd kompilatora, zmienna
//niezainicjalizowana
r = null; //inicjalizacja wartością null
r = new Random(); //utworzenie obiektu klasy
//Random
int i = r.Next(0,101); //losowanie liczby całkowitej
//z przedziału <0;100>
Uwaga:
Użycie zmiennej referencyjnej o wartości null powoduje generację wyjątku w czasie działania programu.
Oczywiście zmienną możemy zainicjalizować w miejscu deklaracji:
Random r = new Random();
Na zmienną r stosując skrót myślowy można powiedzieć, że jest obiektem klasy Random.
Poniższy kod:
string s1 = "Ala ma kota";
string s2 = s1;
można przedstawić graficznie w następujący sposób:
Czas życia obiektu jest zarządzany przez proces automatycznego odzyskiwania pamięci (garbage collection). Nie jesteśmy wstanie określić dokładnie miejsca w programie, kiedy pamięć zajmowana przez obiekt zostanie zwolniona, czyli kiedy obiekt zostanie usunięty z pamięci. Możemy być jednak pewni, że proces automatycznego odzyskiwania pamięci nie usunie obiektu, jeżeli istnieje przynajmniej jedna zmienna, która odwołuje się do danego obiektu.
Konwersja typów
Skopiowanie wartości zmiennej do zmiennej innego typu nazywamy konwersją. Konwersja oczywiście nie jest możliwa między dowolnymi dwoma typami. W języku C# istnieje szereg konwersji standardowych.
Uwaga:
W języku C# dla typów użytkownika może definiować własne metody konwersji. Szerzej o tym jest napisane w kursie "Programowanie obiektowe".
Konwersje standardowe możemy podzielić na:
Konwersje niejawne.
Konwersje jawne.
Konwersja niejawna zachodzi automatycznie. Programista nie musi jej oznaczać:
int x = 10;
long y = x; //konwersja z typu int do typu long
W przypadku konwersji niejawnej nigdy nie ma utraty wartości, może być tylko utracona precyzja - dokładność. Można ją porównać do przelewania zawartości pojemnika o małej objętości do pojemnika o dużej objętości. Konwertować niejawnie można z typu "małego" do typu "dużego". "Duży" typ zawiera wartości typu "małego". Poniższa tabela przedstawia standardowe konwersje niejawne w języku C#.
Z |
Do |
sbyte |
short, int, long, float, double, decimal |
byte |
short, ushort, int, uint, long, ulong, float, double, decimal |
short |
int, long, float, double, decimal |
ushort |
int, uint, long, ulong, float, double, decimal |
int |
long, float, double, decimal |
uint |
long, ulong, float, double, decimal |
long, ulong |
float, double, decimal |
float |
double |
char |
ushort, int, uint, long, ulong, float, double, decimal |
Konwersje jawną można porównać do przelewania z dużego naczynia do małego. Przelewając ryzykujemy, że część płynu może się nie zmieścić w małym naczyniu i się rozleje. W przypadku konwersji jawnej zachodzi, więc obawa o utratę wartości. Programista musi poinformować, że dokonuje tej konwersji świadomie. Konwersję jawną zapisujemy przy pomocy operatora konwersji:
short s =300;
//byte b = s; //błąd, musi być konwersja jawna
byte b = (byte)i; //OK
Console.WriteLine(b);
Na ekranie zostanie wypisane:
44
Została utracona wartość liczby. Chcąc kontrolować takie sytuacje można użyć polecenia checked.
checked {
int n = int.MaxValue;
Console.WriteLine(++n);
// OverflowException
}
lub operatora checked
int m = int.MaxValue;
int n = checked(m+1); // OverflowException
short s= 300;
byte b = checked((byte)s); // OverflowException
Wyraz checked powoduje, że w czasie wykonania programu jest kontrolowane, czy nie nastąpiło przepełnienie. Wystąpienie przepełnienia powoduje rzucenie wyjątku. Wyjątki zostaną przybliżone w następnym rozdziale.
Przełącznik kompilatora /checked+ domyślnie sprawdza przepełnienie. Domyślną opcją kompilatora jest /checked-, wyłączenie sprawdzania przepełnienia. Przy włączonej opcji kompilatora sprawdzenia przepełnienia, można ją wyłączyć przy pomocy słowa unchecked. Stosuje się je w identyczny sposób jak słowo checked.
Uwaga:
checked i unchecked dotyczą tylko liczb całkowitych.
Literały rzeczywiste traktowane są jako typ double, więc następujący kod powoduje błędy kompilatora:
float f = 4.2;
decimal d = 2.4;
Należy to zapisać to:
float f = (float)4.2;
decimal d = (decimal)2.4;
lub krócej
float f = 4.2f;
decimal d = 2.4d;
Kopiując wartość zmiennej typu wyliczeniowego do zmiennej typu całkowitego i dokonując konwersji odwrotnej, również musimy stosować konwersję jawną,.
enum PoryRoku {Wiosna, Lato, Jesien, Zima};
...
int i = 2;
PoryRoku poryRoku = (PoryRoku)i;
int j = (int)poryRoku;
Uwaga:
Podczas konwersji nie jest sprawdzane, czy dana wartość całkowita ma swoją reprezentację w danym typie wyliczeniowym - nie jest zgłaszany żaden błąd, ani nie ma generacji żadnego wyjątku.
W języku C# istnieje jeszcze jedna standardowa konwersja - opakowanie (boxing). Opakowanie wartości polega na niejawnym przekształceniu typu wartości w typ object.
int ilosc = 10;
object obj = i; //opakowanie
W wyniku operacji opakowania na zarządzanej stercie tworzona jest instancja obiektu, do której jest dowiązana zmienna obj. Wartość zmiennej ilosc została skopiowana do obiektu obj. Zmienna iosc oraz obiekt obj są niezależne. Zmiana wartości obiektu obj nie wpływa na wartość zmiennej ilosc.
Operacja odwrotna do opakowania - rozpakowanie musi być wywoływana jawnie. Kompilator musi być poinformowany, jaki typ wartości ma uzyskać z obiektu na zarządzanej stercie. Podczas wykonywania rozpakowania następuje sprawdzenie, czy instancja obiektu zawiera wartość żądanego typu. Jeżeli jest tak w istocie, to wartość zostanie rozpakowana. W przeciwnym wypadku zostanie zgłoszony wyjątek - InvalidCastException. Więcej informacji na temat wyjątków można znaleźć w następnym rozdziale.
int ilosc = 10;
object obj = i; //opakowanie
int ilosc2 = (int)obj; //rozpakowanie
double d = (double)obj; //wyjątek
Operatory
Dla wbudowanych typów danych w językach programowania wprowadza się pewną liczbę prostych, standardowych operacji. Operacje te wyrażone są przy pomocy ciągu znaków, które nazywamy operatorami. Ciąg znaków składa się najczęściej z pojedynczego znaku lub dwóch znaków. Przykładem operatora jest znak równości = - operator przypisania. Oznacza on skopiowanie wartości zmiennej lub wyrażenia znajdującego się po prawej stronie do zmiennej znajdującej się po lewej stronie znaku równości. Pojęcie wyrażenie, występujące w poprzednim zdaniu, oznacza ciąg operatorów i argumentów. Wyrażenie może dawać w wyniku pewną wartość i powodować efekty uboczne. Argumentem może być identyfikator zmiennej, literał, wywołanie metody, nazwa typu lub inne wyrażenie. Wyrażenie może składać się tylko z pojedynczego argumentu - identyfikatora zmiennej, literału, wywołania metody.
Po skończeniu tej części modułu studenci będą:
Znali operatory występujące w języku C#.
Potrafili budować wyrażenia i obliczać ich wartość.
Uwaga:
Dla typów definiowanych przez użytkownika można przeciążyć, zdefiniować własną wersję większości operatorów. Temat przeciążenie operatorów jest omawiany w kursie "Programowanie obiektowe".
Predefiniowane operatory w C#
Omawiając operatory podzielimy je na grupy.
Operatory arytmetyczne - oznaczają proste działania matematyczne. Do operatorów arytmetycznych zaliczamy: + (dodawanie), - (odejmowanie), * (mnożenie), / (dzielenie), % (reszta z dzielenia). Jako argumenty mogą przyjmować wartość (zmienną) dowolnego typu numerycznego..
double x = 20, y = 10;
double z = x + y; //z = 30
z = x - y; //z = 10
z = x * y; //z = 200
z = x / y; //z = 2
z = 7 % 3; //z = 1
z = -7 % 3; //z = -1
z = 7 % -3; //z = 1
z = -7 % -3; //z = -1
Uwaga:
W przypadku dzielenie liczb całkowitych wynik jest również całkowity.
z = 6 / 5; // z = 1 nie 1.2
W przypadku liczb całkowitych oraz liczb typu decimal przy dzieleniu przez zero oraz reszty z dzielenia przez zero zgłaszany jest wyjątek.
W przypadku dzielenia liczb typu float lub double przez zero, wynikiem jest nieskończoność (+ lub minus) lub wynik "nie jest liczbą".
W przypadku dodawania, odejmowania, mnożenia licz całkowitych, jeżeli wynik przekracza zakres dopuszczalnych wartości danego typu, zachowanie jest uzależnione od opcji kompilatora /checked (patrz temat "Konwersja typów" w tym rozdziale). W przypadku typów rzeczywistych, jeżeli wynik nie mieści się w zakresie dopuszczalnych wartości danego typu, wynikiem jest plus lub minus nieskończoność. Dla typu decimal przy przekroczeniu zakresu dopuszczalnych wartości rzucany jest wyjątek. OverflowException.
Dokładny opis sposobu działania w sytuacjach kłopotliwych (zero i nieskończoność, dwie nieskończoności itp.) można znaleźć w specyfikacji języka C# w rozdziale 7.7 - C# Language Specification 1.2.doc.
Uwaga:
Operatory dodawania i odejmowania są również automatycznie dostępne dla typów wyliczeniowych. Wynikiem dodawania lub odejmowania dwóch wartości danego typu wyliczeniowego, jest wartość typu całkowitego bazowego dla danego typu wyliczeniowego.
Operator konkatenacji - łączenia napisów +.
W przypadku znaku dodawania, gdy po jego obu stronach występują wartości typu string oznacza on łączenie napisów, tworzony jest nowy obiekt reprezentujący połączone napisy.
string s = "Ala ma ";
string s2 = s + "kota"; //s2 = "Ala ma kota"
Operatory relacyjne.
Wynikiem ich działania jest prawda lub fałsz - typ logiczny.
Dla typów numerycznych oraz typów wyliczeniowych mamy następujące operatory relacyjne: == (czy równe), != (czy różne), < (czy mniejsze), > (czy większe), <= (czy mniejsze lub równe), >= (czy większe lub równe) np. :
bool b;
int i =10, j =20;
b = i > j; //b = false
Uwaga:
W przypadku typów rzeczywistych należy pamiętać że:
-∞ == -∞< -max < ... < -min < -0.0 == +0.0 < +min < ... < +max < +∞ == +∞
gdzie max oraz min są odpowiednio największą i najmniejszą wartością dodatnią danego typu rzeczywistego.
W przypadku typów referencyjnych predefiniowane są operatory: == (czy równe), != (czy różne). Wersje predefiniowane tych operatorów sprawdzają czy zmienne odwołują się do tego samego obiektu (równość referencji), a nie czy obiekty mają tę samą wartość (równość wartości). Programista może zmienić znaczenie tych operatorów. Jest tak np. w przypadku typu string. Dla typu string operatory te porównują wartość nie referencje. W przypadku, gdy obie zmienne danego typu referencyjnego mają wartość null przyjmuje się, że są równe.
Uwaga:
Gdy chcemy porównywać napisy, możemy użyć metody string.Compare. Opertory <,>,<=, >= dla typu string nie są zdefiniowane.
Operatory == (czy równe), != (czy różne) zdefiniowane są również dla typu logicznego.
Operatory przesunięcia << lub >>
Są to dwuargumentowe operatory pracujące na argumentach typu całkowitego lub typów wyliczeniowych.
x<<n; //przesunięcie w lewo
x>>n //Przesunięcie w prawo
n - określa ilość bitów przesunięcia.
Uwaga:
Do obliczenia rzeczywistej ilości bitów do przesunięcia, jeżeli x jest typu int lub uint, branych jest tylko pięć najmniej znaczących bitów n (n & 0x1F), natomiast gdy x jest typu long lub ulong, branych jest sześć najmniej znaczących bitów n (n & 0x3F)
W przypadku przesunięcia w lewo sposób obliczania wartości liczby po przesunięciu jest prosty. Zapisujemy liczbę w notacji dwójkowej. Następnie skreślamy n najbardziej znaczących cyfr dwójkowych (z lewej strony), gdzie n oznacza ilość bitów przesunięcia i dopisujemy n 0 z prawej strony.
int x = 0x2f; //101111
int y = x << 2; //10111100
Uwaga:
W przypadku liczb ze znakiem liczby kodowane są następująco. Najbardziej znaczący bit jest nazywany bitem znaku i jeżeli jest ustawiony do równania obliczającego wartość liczby zamiast liczby 1 wstawiamy -1. Zostanie to pokazane na przykładzie typu sbyte (osiem bitów):
d7(16) = 1101 0111(2) = -1 * 27 + 1 * 26 + 0 * 25 + 1 * 24 + 0 * 23 + 1 * 22 +
+ 1 * 21 + 1 * 20 = -128 +64 + 16 + 4 + 2 + 1 = -41
int x = -1; // 0xffffffff
int y = x << 1; // 0xfffffffe, y=-2
W przypadku przesunięcia w prawo sytuacja jest bardziej skomplikowana. Dla liczb dodatnich skreślamy n najmniej znaczących bitów i z przodu dodajemy n zer. Dla liczb ujemnych również skreślamy n najmniej znaczących bitów, ale z przodu dopisujemy jedynki.
Przesunięcie w lewo jest równoważne mnożeniu przez n-tą potęgę liczby dwa, natomiast przesunięcie w prawo jest równoważne dzieleniu przez n-tą potęgę dwóch.
Operatory bitowe (logical operators):
Do operatorów bitowych należ: & (koniunkcja), ^ (różnica symetryczna), | (alternatywa). ~ (negacja bitowa). Zasadę ich działania dla typów całkowitych oraz wyliczeniowych przedstawia poniższa tabela:
x = 1 1 0 0 ...(pozostałe bity)
y = 1 0 1 0 ...(pozostałe bity)
x & y = 1 0 0 0 ...(pozostałe bity)
x | y = 1 1 1 0 ...(pozostałe bity)
x ^ y = 0 1 1 0 ...(pozostałe bity)
~x = 0 0 1 1 ...(pozostałe bity)
Argumentami operatorów bitowych & (koniunkcja), ^ (różnica symetryczna), | (alternatywa) mogą być zmienne typu logicznego. Działanie ich jest następujące:
x = true false true false
y = true true false false
x & y = true false false false
x | y = true true true false
x ^ y = false true true false
Operatory logiczne (conditional logical operators)
Do operatorów logicznych zaliczamy: && (iloczyn logiczny), || (suma logiczna) oraz ! (negacja logiczna). Argumentami tych operatorów mogą być wartości logiczne.
Wyrażenie x && y zwraca wartość true, jeżeli x i y ma wartość true. Od wyrażenia x & y różni się tym, że wartość y jest obliczana tylko wtedy, gdy x ma wartość true.
Wyrażenie x || y zwraca wartość true, jeżeli x lub y ma wartość true. Od wyrażenia x | y różni się tym, że wartość y jest obliczana tylko wtedy, gdy x ma wartość false.
Operator ! (negacja logiczna) zmienia wartość true na false i na odwrót.
bool x = true;
bool y = ! x; //y=false
Do operatorów logicznych zaliczamy również operatory true oraz false. Wykorzystywane są przy przeciążeniu operatorów && oraz || i są poza zakresem tego kursu.
Operatory inkrementacji (zwiększania o 1) i dekrementacji (zmniejszania o 1).
Oba operatory mają dwie wersje: przyrostkową oraz przedrostkową. Zasada działania zostanie pokazana na przykładzie
int x = 10, y = 10;
int z = x++;
int v = ++y;
Wartość zmiennych x, y, v po wykonaniu powyższego kodu będzie 11, natomiast wartość v będzie wynosić 10. Dzieje się tak, ponieważ wersja przyrostkowa x++ najpierw wstawia wartość zmiennej do wyrażenia, a następnie zwiększa jej wartość o jeden, natomiast wersja przedrostkowa (++y) najpierw zwiększa wartość zmiennej o jeden, a następnie zwraca jej wartość, już zwiększoną do wyrażenia.
Dla operator dekrementacji będzie to wyglądało następująco:
int a = 10, b = 10;
int c = a--; //c=10, a = 9
int d = --b; //d=9, b=9
Operatory te można stosować do wszystkich zmiennych typów numerycznych (int, char, double, decimal ...).
Operatory przypisania.
Wcześniej w tym rozdziale był używany operator przypisania (podstawienia) oznaczony pojedynczym znakiem równości =. Dla zmiennych typów wartości oznacza on skopiowanie wartości zmiennej lub wyrażenia znajdującego się po prawej stronie znaku równości, do zmiennej znajdującej się po lewej stronie znaku równości. Dla zmiennych typów referencyjnych oznacza skopiowanie tylko odwołania. Do operatorów przypisania zaliczmy również: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=.
Znaczenie poszczególnych operatorów jest zdefiniowane poniżej.
x += 2; x = x + 2;
x -= 2; x = x - 2;
x *= 2; x = x * 2;
x /= 2; x = x / 2;
x %= 2; x = x % 2;
x &= 2; x = x & 2;
x |= 2; x = x | 2;
x ^= 2; x = x ^ 2;
x <<= 2; x = x << 2;
x >>= 2; x = x >> 2;
Plus + i minus - jednoargumentowy.
Operatory te są predefiniowane dla typów numerycznych i mają identyczne znaczenie jak w zapisie matematycznym. W przypadku operator + do wyrażenia wstawiana jest wartość jego argumentu, nie zmienia on wartości swojego argumentu. Operator - oznacza liczbę przeciwną. Operator - nie może być stosowany do zmiennych typu ulong, generowany jest błąd kompilacji. W przypadku argumentu typu uint, rezultat jest konwertowany do long.
Operator dostępu do składowej kropka . .
Znak kropki oddziela nazwę kontenera (nazwa przestrzeni nazw, nazwa klasy, nazwa struktury) od nazwy rzeczy zagnieżdżonej (nazwa podprzestrzeni, nazwa klasy, nazwa metody ...). Pełni podobną rolę jak znak ukośnika przy podawaniu ścieżki dostępu, np.:
System.Collections.Generic;
System.Console.WriteLine();
Wyrażenie warunkowe.
Ogólna postać wyrażenia warunkowego jest następująca:
warunek ? wyrażenie1 : wyrażenie2;
Wartość wyrażenia warunkowego jest obliczany w następujący sposób. Jeżeli warunek jest prawdziwy to wartością całego wyrażenia warunkowego jest wyrażenie1, w przeciwnym wypadku wartością wyrażenia warunkowego jest wyrażenie2. W zależności od wartości warunku obliczane jest tylko wyrażenie1 albo wyrażenie2. Nigdy nie są obliczane oba wyrażenia. Poniżej zostanie przedstawiony kod obliczający wartość bezwzględną (moduł liczby) przy pomocy wyrażenia warunkowego:
double x, modul;
...
modul = x < 0 ? -x : x;
Argumentu warunek musi być typu logicznego. Natomiast wyrażenie1 i wyrażenie2 muszą być tego samego typu, albo między typami tych argumentów musi istnieć konwersja niejawna.
Tworzenie nowego obiektu - operator new.
W przypadku wywołania operatora new dla typów referencyjnych, następuje alokacji pamięci dla obiektu i inicjalizacja pamięci (wywołanie metody zwanej konstruktorem).
Ranodom r =new Random(); //utworzenie obiektu klasy Random
Operator new możemy również używać do "tworzenia" zmiennych typów wartości. W przypadku typów wartości nie ma jednak dynamicznej alokacji pamięci. Wywoływana jest, podobnie jak dla typów referencyjnych, metoda inicjalizująca zmienną zwana konstruktorem.
int i = new int(); //int i = 0;
Uwaga:
Więcej o metodzie zwanej konstruktorem można znaleźć w kursie "Programowanie obiektowe".
Operator rzutowania () i operatory kontrolujące przepełnienie typów całkowitych checked i unchecked zostały opisane wcześniej w tym rozdziale przy omawianiu konwersji jawnych.
Operator indeksowania [] zostanie dokładnie omówiony w rozdziale szóstym tego kursu.
Operatory informujące o typie (as, is, sizeof, typeof) zostaną przybliżone w kursie "Programowanie obiektowe".
Operatory operujące na wskaźnikach (adresach) mogą być używane tylko w kodzie niezarządzanym (unsafe context). Jest to tzw. kod niebezpieczny i jest poza zakresem tego kursu.
Właściwości operatorów
Operatory (funkcje operatorowe) charakteryzują się pewnymi właściwościami. Należą do nich:
liczba argumentów
priorytet
łączność
typ argumentów
Ze względu na liczbę argumentów operatory możemy podzielić na:
operatory jednoargumentowe (unary operators), np.: inkrementacji ++, dekrementacji --, minus jednoargumentowy -, ...
operatory dwuargumentowe (binary operators), np.: operatory arytmetyczne, przypisania, relacyjne...
operator trójargumentowy (ternary operator), istnieje tylko jeden - wyrażenie warunkowe (? :)
Priorytet operatorów to inaczej kolejność działań. W wyrażeniach zawierających wiele operatorów priorytet (precedence) kontroluje, w jakiej kolejności poszczególne operatory są "obliczane". Rozważmy prosty przykład:
double x = 10, y = 5, z = 2;
double a = x + y * z;
Wartością zmiennej a będzie 20, a nie 30, ponieważ najpierw zostanie wykonane mnożenie, ma wyższy priorytet, a dopiero następnie dodawanie, czyli identycznie jak w matematyce. Kolejność działań możemy zmieniać stosując nawiasy. W języku C# do ustalenia kolejności działań możemy stosować tylko nawiasy okrągłe, wielokrotnie je zagnieżdżając.
double x = 10, y = 5, z = 2;
double a = (x + y) * z; //a = 30
double b = (2*(x+y) + z) * 3; //b= 96
Kolejność obliczeń wartości zmiennej b jest następująca:
Obliczamy wartość wyrażenia znajdującego się w najbardziej zagnieżdżonych nawiasach, czyli x+y. Po uproszczeniu otrzymujemy wyrażenie (2*15+z)*3.
Obliczamy wartość wyrażenia w nawiasie. Ponieważ mnożenie ma wyższy priorytet, jest wykonywane najpierw. Po wykonaniu działań w nawiasie otrzymujemy wyrażenie 32*3, którego wartość wynosi 96
Nawiasy okrągłe stosowane są nie tylko w celu zmiany kolejności działań, ale również, aby wyrażenie stało się bardziej czytelne.
Poniższa tabela przedstawia operatory w kolejności od najwyższego priorytetu do najniższego. Operatory należące do tej samej kategorii mają ten sam priorytet.
Kategoria |
Operatory |
Operatory o najwyższym priorytecie |
x.y - dostęp do składowej - kropka |
Jednoargumentowe (unary)
|
+, -, - operatory jednoargumentowe plus i minus |
Operatory multiplikatywne |
* - mnożenie |
Operatory addytywne |
+ - dodawanie |
Operatory przesunięcia |
<<, >> |
Relacyjne i sprawdzające typ |
<, >, <=, >= |
Równości |
==, != |
Iloczyn bitowy |
& |
Różnica symetryczna |
^ |
Suma bitow |
| |
Koniunkcja |
&& |
Alternatywa |
|| |
Wyrażenie warunkowe |
? : |
Operatory przypisania |
=, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |= |
Nasuwa się pytanie, co będzie, jeżeli w wyrażeniu występują operatory o tym samym priorytecie. Kolejność działań w takim wypadku jest ustawiana na podstawie właściwości zwanej łącznością operatorów. Rozważmy następujący przykład:
double x = 10, y = 5, z = 2;
double a = x - y - z;
Do zmiennej a zostanie podstawiona wartość 3, ponieważ najpierw zostanie wykonane odejmowanie x-y, a następnie od powyższego wyniku odjęta zostanie wartość z. Działania są wykonywane od lewej do prawej strony. O tak działających operatorach mówi się, że są lewostronnie łączne.
Rozważmy teraz inny przykład:
double x = 10, y = 5, z = 2;
double a = x = y = z;
Wszystkie zmienna x, y, z, a, po wykonaniu tego kodu będą miały wartość 2, ponieważ przypisanie jest wykonywane od prawej do lewej strony. Czyli najpierw zostanie wykonane y = z, gdzie wartością tego wyrażenia jest wartość stojąca po prawej stronie znaku równości. Wartość wyrażenia y = z, czyli w tym przypadku 2, zostanie następnie przypisana do x. O tak działających operatorach mówi się, że są prawostronnie łączne.
Do operatorów prawostronnie łącznych należą wszystkie operatory jednoargumentowe, operatory przypisania oraz wyrażenie warunkowe ( ? :).
Natomiast do operatorów lewostronnie łącznych należą wszystkie operatory dwuargumentowe poza operatorami przypisania.
Typ argumentów stojących obok operatora wpływa na jego znaczenie. I tak operator +, gdy stoją obok niego wartości numeryczne oznacza dodawanie, natomiast w przypadku, gdy stoją zmienne typu string, oznacza łączenie napisów (konkatenację).
W przypadku operatorów działających na typach numerycznych należy zwrócić uwagę na proces zwany promocją typów.
W przypadku operatorów jednoargumentowych: +, -, ~, gdy ich argumentem jest zmienna typu sbyte lub byte lub short lub ushort lub char, automatycznie jej typ jest konwertowany do typu int. Dodatkowo dla operatora - zmienna typu uint jest konwertowana do typu long. Wobec powyższego poniższy kod spowoduje błąd kompilator, niedozwolonej konwersji niejawnej.
short x = 10;
short y = +x;
W przypadku natomiast operatorów dwuargumentowych: +, -, *, /, %, &, |, ^, ==, !=, >, <, >=, <= regułę promocji można podać w następujących punktach, których kolejność ma znaczenie:
Jeżeli jeden argument jest typu decimal, drugi argument jest konwertowany do typu decimal, albo jest generowany bład kompilacji gdy konwercja niejawna jest niedozwolona (drugi argument jest typu double lub float).
W przeciwnym wypadku, jeżeli jeden argument jest typu double drugi argument jest konwertowany do typu double.
W przeciwnym wypadku, jeżeli jedne argument jest typu float drugi argument jest konwertowany do typu float.
W przeciwnym wypadku, jeżeli jeden argument jest typu ulong drugi argument jest konwertowany do typu ulong, albo jest generowany bład kompilacji gdy drugim argumentem jest liczba ze zankiem (sbyte, short, int, long).
W przeciwnym wypadku, jeżeli jeden argument jest typu long drugi argument jest konwertowany do typu long.
W przeciwnym wypadku, jeżeli jeden argument jest typu uint drugi argument jest typu sbyte, short lub int oba argumenty konwertowane są do typu long.
W przeciwnym wypadku, jeżeli jeden argument jest typu uint drugi argument jest konwertowany do typu uint.
W przeciwnym wypadku, oba argumenty konwertowane są do typu int.
Wobec powyższego poniższy kod spowoduje błąd kompilator, niedozwolonej konwersji niejawnej:
short x = 10, y = 10;
short z = x + y;
Pytania sprawdzające
Ile bajtów zajmuje zmienna typu char w języku C#?
Odp.
Dwa bajty.
Czy w języku C# można używać niezainicjalizowanych zmiennych?
Odp.
Nie, Przed użyciem każda zmienna musi być zainicjalizowana.
W jakiej przestrzeni nazw znajduje się klasa Console?
Odp.
W przestrzeni nazw System.
Jak wywoływana jest konwersja jawna?
Odp.
Konwersję jawną wywołujemy przy pomocy operatora rzutowania - (typ).
Podaj wartości zmiennych, po wykonaniu następującego kodu:
int x = 10, y = 5, z =20;
int a = z - x * y;
int b = x++;
double c = 11 / y;
Odp.
x = 11, y = 5, z = 20, a = -30, b = 10, c = 2.0
Laboratorium
Ćwiczenie 1
Utworzenie programu, który zamienia (wymienia) wartości dwóch zmiennych. Algorytm ten po angielsku nosi nazwę Swap (zamiana). Jest to jeden z podstawowych algorytmów używany do budowy bardziej skomplikowanych programów.
Uruchom Visual Studio
Naciśnij przycisk Start systemu Windows, wybierz Wszystkie Programy następnie Microsoft Visual Studio 2005/ Microsoft Visual Studio 2005.
Utwórz nowy projekt
Z menu File wybierz New/Project...
W oknie dialogowym New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
lokalizacja: Moje Dokumenty\Visual Studio 2005\Projects\
nazwa projektu: Zamiana.
nazwa rozwiązania: Lab2
Wewnątrz metody Main napisz następujący kod:
Zadeklaruj trzy zmienne rzeczywiste a, b, tmp. Zmienna a i b są to te zmienne, między którymi będziemy wymieniać wartości, zmienna tmp jest zmienną pomocniczą:
double a, b, tmp;
Pobierz od użytkownika wartości zmiennych a i b:
Console.Write("Podaj wartość zmiennej a: ");
a = Convert.ToDouble(Console.ReadLine());
Console.Write("Podaj wartość zmiennej b: ");
b = Convert.ToDouble(Console.ReadLine());
Wymień wartości zmiennych a i b, przy pomocy zmiennej tmp.
tmp = a;
a = b;
b = tmp;
Wypisz wartości zmiennych po wymianie, a następnie zatrzymaj działanie programu:
Console.WriteLine("a = {0}, b = {1}", a, b);
Console.ReadKey();
Skompiluj i uruchom program.
Ćwiczenie 2
Utworzenie programu, który oblicza wartość wyrażenia:
Dodaj do bieżącego rozwiązania nowy projekt
Z menu File wybierz Add/New Project...
W oknie dialogowym Add New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
nazwa projektu: Wyrazenie.
Uczyń nowo utworzony projekt projektem startowym
Zaznacz projekt Wyrazenie w okienku Solution Explorer i z menu kontekstowego wybierz Set as StartUp Project.
albo, gdy rozpoczynasz laboratorium od tego ćwiczenia
Uruchom Visual Studio
Naciśnij przycisk Start systemu Windows, wybierz Wszystkie Programy następnie Microsoft Visual Studio 2005/ Microsoft Visual Studio 2005.
Utwórz nowy projekt
Z menu File wybierz New/Project...
W oknie dialogowym New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
lokalizacja: Moje Dokumenty\Visual Studio 2005\Projects\
nazwa projektu: Wyrazenie.
nazwa rozwiązania: Lab2
Wewnątrz metody Main napisz następujący kod:
Zadeklaruj cztery zmienne rzeczywiste x, y, z, v.
double x,y,z,v;
Pobierz od użytkownika wartości zmiennych x, y i z:
Console.Write("Podaj wartość zmiennej x: ");
x = Convert.ToDouble(Console.ReadLine());
Console.Write("Podaj wartość zmiennej y: ");
y = Convert.ToDouble(Console.ReadLine());
Console.Write("Podaj wartość zmiennej z: ");
z = Convert.ToDouble(Console.ReadLine());
Napisz zadany wzór z użyciem operatorów języka C#, wartość utworzonego wyrażenia wstaw do zmiennej v:
v = (4*x-y)/(3*(z*z+1));
Wypisz wartości obliczonego wyrażenia, a następnie zatrzymaj działanie programu:
Console.WriteLine("Wartość wyrażenia wynosi:
¬{0}.", v);
Console.ReadKey();
Skompiluj i uruchom program.
Ćwiczenie 3
Program obliczający pole i obwód koła o zadanym promieniu
Dodaj do bieżącego rozwiązania nowy projekt
Z menu File wybierz Add/New Project...
W oknie dialogowym Add New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
nazwa projektu: Kolo.
Uczyń nowo utworzony projekt projektem startowym
Zaznacz projekt Kolo w okienku Solution Explorer i z menu kontekstowego wybierz Set as StartUp Project.
albo, gdy rozpoczynasz laboratorium od tego ćwiczenia
Uruchom Visual Studio
Naciśnij przycisk Start systemu Windows, wybierz Wszystkie Programy następnie Microsoft Visual Studio 2005/ Microsoft Visual Studio 2005.
Utwórz nowy projekt
Z menu File wybierz New/Project...
W oknie dialogowym New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
lokalizacja: Moje Dokumenty\Visual Studio 2005\Projects\
nazwa projektu: Kolo.
nazwa rozwiązania: Lab2
Wewnątrz metody Main napisz następujący kod:
Zadeklaruj zmienną rzeczywistą r.
double r;
Pobierz od użytkownika długość promienia:
Console.Write("Podaj długość promienia: ");
r = Convert.ToDouble(Console.ReadLine());
Zadeklaruj zmienne pole oraz obwod i odpowiednio je zainicjalizuj. We wzorach na obwód i pole skorzystaj ze stałej Math.PI.
double pole = Math.PI * r * r;
double obwod = 2 * Math.PI * r;
Wypisz obliczone wartości pola i obwodu koła z dokładnością do trzech miejsc po przecinku, a następnie zatrzymaj działanie programu:
Console.WriteLine("Pole koła o promieniu {0}
¬wynosi: {1:f3}.", r, pole);
Console.WriteLine("Obwód koła o promieniu {0}
¬wynosi: {1:f3}.", r, obwod);
Console.ReadKey();
Skompiluj i uruchom program.
Ćwiczenie 4
Program obliczający cenę brutto oraz podatek od podanej ceny netto..
Dodaj do bieżącego rozwiązania nowy projekt
Z menu File wybierz Add/New Project...
W oknie dialogowym Add New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
nazwa projektu: Podatek.
Uczyń nowo utworzony projekt projektem startowym
Zaznacz projekt Podatek w okienku Solution Explorer i z menu kontekstowego wybierz Set as StartUp Project.
albo, gdy rozpoczynasz laboratorium od tego ćwiczenia
Uruchom Visual Studio
Naciśnij przycisk Start systemu Windows, wybierz Wszystkie Programy następnie Microsoft Visual Studio 2005/ Microsoft Visual Studio 2005.
Utwórz nowy projekt
Z menu File wybierz New/Project...
W oknie dialogowym New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
lokalizacja: Moje Dokumenty\Visual Studio 2005\Projects\
nazwa projektu: Podatek.
nazwa rozwiązania: Lab2
Wewnątrz metody Main napisz następujący kod:
Zadeklaruj stałą symboliczną rzeczywistą o dużej precyzji o nazwie Vat i nadaj jej wartość 0,22.
const decimal Vat = 0.22M;
Zadeklaruj zmienną rzeczywistą o dużej precyzji netto.
decimal netto;
Pobierz od użytkownika cenę netto:
Console.Write("Podaj cenę netto: ");
netto = Convert.ToDecimal(Console.ReadLine());
Zadeklaruj zmienne podatek oraz brutto i odpowiednio je zainicjalizuj..
decimal podatek = Vat * netto;
decimal brutto = netto + podatek;
Wypisz obliczone wartości podatku i ceny brutto, dodając symbol waluty, a następnie zatrzymaj działanie programu:
Console.WriteLine("Cena wynosi {0:C}, w tym
¬kwota podatku: {1:C}.", brutto, podatek);
Console.ReadKey();
Skompiluj i uruchom program.
Ćwiczenie 5
Program wyznaczający maksimum z dwóch liczb całkowitych przy pomocy wyrażenia warunkowego..
Dodaj do bieżącego rozwiązania nowy projekt
Z menu File wybierz Add/New Project...
W oknie dialogowym Add New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
nazwa projektu: Maksimum.
Uczyń nowo utworzony projekt projektem startowym
Zaznacz projekt Maksimum w okienku Solution Explorer i z menu kontekstowego wybierz Set as StartUp Project.
albo, gdy rozpoczynasz laboratorium od tego ćwiczenia
Uruchom Visual Studio
Naciśnij przycisk Start systemu Windows, wybierz Wszystkie Programy następnie Microsoft Visual Studio 2005/ Microsoft Visual Studio 2005.
Utwórz nowy projekt
Z menu File wybierz New/Project...
W oknie dialogowym New Project określ następujące właściwości:
typu projektu: Visual C#/Windows
szablon: Console Application
lokalizacja: Moje Dokumenty\Visual Studio 2005\Projects\
nazwa projektu: Maksimum.
nazwa rozwiązania: Lab2
Wewnątrz metody Main napisz następujący kod:
Zadeklaruj zmienne całkowite: a, b, max.
int a, b, max;
Pobierz od użytkownika wartości zmiennych a i b:
Console.Write("Podaj pierwszą wartość: ");
a = Convert.ToInt32(Console.ReadLine());
Console.Write("Podaj drugą wartość: ");
b = Convert.ToInt32(Console.ReadLine());
Wyznacz wartość maksymalną z liczb a i b:
max = a > b ? a : b;
Wypisz obliczoną wartość maksymalną, a następnie zatrzymaj działanie programu:
Console.WriteLine("Wartość maksymalna wynosi:
¬{0}.", max);
Console.ReadKey();
Skompiluj i uruchom program.
W wersji C# 3.0 zostają wprowadzone typy domniemane, gdzie nie podajemy nazwy typu w sposób jawny, ale kompilator na podstawie jak inicjalizujemy zmienną jest w stanie określić jej typ.
.
.
Ala ma kota
s2
s1
Zarządzana sterta
Stos
Obiekt
L
L