Każda deklarowana zmienna lub funkcja jest składową jakiegoś typu (klasy, struktury). Bezpośrednio w przestrzeni nazw możemy deklarować tylko typy. Kolejność deklaracji nie ma znaczenia.
Każdy identyfikator jest słowem złożonym ze znaków Unicode, zaczynającym się od litery lub znaku podkreślenia. Brana jest pod uwagę wielkość liter. Zasięg identyfikatora rozciąga się do końca bieżącego bloku programu.
W C# wszystkie typy (predefiniowane oraz stworzone przez programistę) należą do jednej z trzech kategorii :
Bezpośrednie : (value type) są strukturami przechowywanymi (na stosie) jako wartości. Do tej kategorii należą takie typy jak np. int, bool, float, char ... oraz struktury. Jeśli typ bezpośredni jest składową większego obiektu (klasy, tablicy), to jest przechowywany bezpośrednio jako część tego obiektu.
Referencyjne : (reference type) ich wartości są przechowywane na stercie (w pamięci rezerwowanej dynamicznie), na stosie znajdują się jedynie referencje (wskazania) do obiektów. Do tej kategorii należą wszystkie klasy (np. object, string), ale także tablice czy interfejsy. Zmienna typu referencyjnego może mieć wartość null oznaczającą, że nie wskazuje żadnego obiektu. Obiekt typu referencyjnego pozostaje na stercie tak długo, aż system nie stwierdzi, że nie ma już żadnych odwołań do tego obiektu (W C# mamy do czynienia z automatycznym zwalnianiem pamięci zajmowanej przez nieużywane obiekty).
Wskaźnikowe : używane do jawnego manipulowania pamięcią. Zwykle się ich nie używa (ponadto wskaźniki mogą być używane tylko w blokach nienadzorowanych).
Przypisanie (operator = ) typu referencyjnego polega na skopiowaniu wskazania na obiekt (nie powstaje nowy egzemplarz obiektu, a tylko mamy kolejne wskazanie na ten sam obiekt). Przypisanie (i ogólnie odwołanie się) typu bezpośredniego polega na skopiowaniu wartości całego obiektu.
Każdy typ bezpośredni posiada (niejawny) przypisany do niego typ referencyjny. W momencie rzutowania typu bezpośredniego na typ referencyjny automatycznie jest tworzony obiekt tego typu referencyjnego. Dzięki temu typy bezpośrednie możemy traktować jako pochodne typu object.
void : typ pusty. Możemy deklarować metody (ew. delegacje) tego typu - w takim wypadku metoda nie musi zwracać żadnej wartości.
null : słowo kluczowe oznaczające pustą referencję (wskazanie puste). Zmienna typu referencyjnego mająca wartość null nie wskazuje na żaden obiekt.
W C# zdefiniowano pięć rodzajów dostępu do typów i ich składowych:
public : składowa lub typ zadeklarowany jako publiczny są dostępne z dowolnego miejsca. Ten rodzaj dostępu jest domyślny dla interfejsów.
private : składowa zadeklarowana jako prywatna jest dostępna tylko z wnętrza typu, w którym została zadeklarowana. Ten rodzaj dostępu jest domyślny dla składowych klas i struktur.
protected : składowa zadeklarowana jako chroniona jest dostępna z wnętrza klasy, w której została zadeklarowana lub z wnętrza klasy pochodnej.
internal : typ lub składowa typu są dostępne tylko z wnętrza złożenia, w którym nastąpiła ich deklaracja (podczas kompilacji pliki .cs z kodem źródłowym programu są kompilowane w moduły - zgodnie z podziałem na przestrzenie nazw - a następnie grupowane w złożenia, ang. assembly)
protected internal : składowa zadeklarowana z takim rodzajem dostępu jest widoczna z wnętrza klasy, w której została zadeklarowana (lub klasy pochodnej od niej) oraz z wnętrza złożenia, w którym się znajduje.
Wymienione rodzaje dostępu można stosować w stosunku do typów (klas, struktur) oraz ich składowych (metod i danych). Każdy typ/składowa w C# posiada któryś z wymienionych rodzajów dostępu (co najwyżej przypisany domyślnie).
int : ( inaczej System.Int32). Liczba całkowita ze znakiem, zajmująca 4 bajty.
uint : ( System.UInt32). Liczba całkowita bez znaku. Zajmuje 4 bajty.
short : ( System.Int16 ). Liczba całkowita krótka ze znakiem. Zajmuje 2 bajty.
ushort : ( System.UInt16). Liczba całkowita krótka bez znaku. Zajmuje 2 bajty.
long : ( System.Int64 ). Liczba całkowita długa ze znakiem. Zajmuje 8 bajtów.
ulong : ( System.UInt64 ). Liczba całkowita długa bez znaku. Zajmuje 8 bajtów.
byte : ( System.Byte ). Pojedynczy bajt, bez uwzględnienia znaku. Istnieje także typ sbyte (System.Sbyte ) oznaczający pojedynczy bajt (ew. małą liczbę całkowitą) ze znakiem.
char : ( System.Char ) reprezentuje pojedynczy znak Unicode. Zajmuje 2 bajty. Literał tego typu zapisujemy w apostrofach, np. 'A' (znak A) lub '\u0041' (zapis w Unicode). Ponadto możemy używać pewnych sekwencji specjalnych, np. '\n'' (znak nowej lini), '\t' (tabulacja), ' \" ' (cudzysłów), '\'' (apostrof), '\0' (null).
bool : ( System.Boolean ) typ logiczny mogący przyjmować wartośći
true
lub false
. Nie można dokonywać konwersji
z bool na typ całkowity lub odwrotnie.
float : ( System.Single ) liczba zmiennopozycyjna pojedynczej precyzji. Zajmuje 4 bajty. W zakresie wartości typów zmiennopozycyjnych (float i double) mieszczą się wartości specjalne ?0, ?? oraz NaN (not a number). Literały typu float zapisujemy z przyrostkiem 'f' lub 'F', np. 9.4f Domyślnie literał zmiennopozycyjny jest typu double (co można dodatkowo podkreślić używając przyrostków 'd' lub 'D').
double : ( System.Double ) liczba zmiennopozycyjna podwójnej precyzji. Zajmuje 8 bajtów.
decimal : ( System.Decimal ) liczba w systemie dziesiętnym zajmująca 12 bajtów (przechowuje 28 cyfr oraz pozycję punktu dziesiętnego w tych liczbach). Ma mniejszy zakres w porównaniu z liczbami zmiennopozycyjnymi jednak zapewnia bardzo dużą precyzję przechowywania liczb o podstawie 10. Liczba zapisywana jako dziesiętna wymaga przyrostka 'm' lub 'M', np. decimal d = 10.1m ;
object : ( System.Object ) typ bazowy dla wszystkich innych typów (z wyjątkiem wskaźnikowych). Powoduje narzut 8 bajtów pamięci dla egzemplarzy typów wywodzących się z niego przechowywanych na stercie (w wypadku przechowywania na stosie nie ma narzutu).
string : ( System.String ) reprezentuje łańcuch (zmiennej długości)
znaków Unicode. Możemy używać tych samych sekwencji specjalnych co w
przypadku typu char, np. '\n', '\0', ... Pomimo tego, że string jest klasą
(typem referencyjnym) ma pewne szczególne przywileje - można go tworzyć bez
użycia operatora new, np. string s = " tekst \n " ;
Poza
zwykłymi literałami łańcuchowymi istnieją także tzw. literały dosłowne -
zawarte wewnątrz @"...". Zawartość wewnątrz takiego literału jest brana 'dosłownie'.
Tożsame są łańcuchy @"\\serwer\plik.txt"
oraz
"\\\\serwer\\plik.txt"
.
Zapisując literał oznaczający liczbę możemy określić typ literału itp. , np.
0x5 : liczba 5 w systemie szesnastkowym. Literały szesnastkowe mają przedrostek 0x
5UL : oznacza wartość 5 typu ulong ( U - liczba bez znaku, L - liczba długa ).
Zmienna (dostępna poprzez identyfikator) reprezentuje miejsce w pamięci.
Każda zmienna posiada swój typ, który określa zbiór możliwych wartości i
operacji. C# jest językiem o ścisłej typizacji, zbiór możliwych operacji
dostępnych dla danego typu/zmiennej jest określany już podczas kompilacji,
a zmienna jest dostępna tylko poprzez powiązany z nią typ. Zmienne deklarujemy
według wzoru: modyfikatory typ identyfikator = wartPocz;
(Modyfikatory oraz wartość początkowa nie są wymagane)
Np. int a = 10 ;
static : pozwala zadeklarować składową statyczną (istniejącą na
rzecz całego typu, a nie pojedynczego obiektu tego typu). Składowe statyczne
nie wymagają istnienia obiektów. Dana składowa statyczna istnieje jeszcze
przed pojawieniem się pierwszego obiektu danej klasy - ponadto istnieje tylko
jedna jej wartość, wspólna dla wszystkich obiektów klasy,
np. static string napisWspólnyDlaCałejKlasy = "..."
const : pozwala zadeklarować stałą (daną składową której wartość
jest obliczana w czasie kompilacji i nie może być zmieniana w czasie działania
programu). Typ stałej musi być jednym z typów predefiniowanych (np. int, float,
long, string), np. public const double PI = 3.14 ;
readonly : pozwala zadeklarować składową, której wartość nie
będzie mogła być modyfikowana po początkowym nadaniu jej wartości. W odróżnieniu
od danych stałych (const) wartości danych tego typu są obliczane podczas
działania programu, a nie podczas kompilacji. Np.
readonly MyClass a = new MyClass( ); readonly int b = 10;
volatile : deklaruje pole typu nietrwałego. Optymalizacje dokonywane
na takim polu przez kompilator i CLR (reorganizacja kolejności instrukcji
itp.) są ograniczone zawsze odczyt lub przypisanie wartości takiej zmiennej
powoduje fizyczne wywołanie odczytu lub zapisu do pmięci.
Np. volatile int a = 1;
W przypadku typów referencyjnych nie wystarczy prosta deklaracja zmiennej. Deklaracja np. MyClass zm ; jest poprawna, jednak nie tworzy nowego obiektu typu, a jedynie samo wskazanie (zm nie wskazuje na żaden obiekt). Aby utworzyć egzemplarz typu referencyjnego konieczne jest użycie operatora new.
new : alokuje pamięć na stercie oraz wywołuje konstruktor podanego
typu, w celu utworzenia obiektu. Pozwala utworzyć dynamicznie obiekt (obiekty
typu referencyjnego, takie jak klasy, tworzymy tylko dynamicznie). Aby utworzyć
egzemplarz danej klasy używamy konstrukcji,
np. MyClass zm = new MyClass( );
Przed użyciem zmiennej konieczne jest przypisanie jej wartości (za wyjątkiem
bloków unsafed) może się to odbyć jawnie lub niejawnie (z użyciem wartości
domyślnej. Jednak w tym wypadku kompilator wygeneruje ostrzeżenie). Wartości
domyślne to:
null
- dla referencji (gdy tworzymy zmienną typu referencyjnego bez przypisania do niej obiektu).
0
- dla wszystkich typów liczbowych (np. int, float) i wyliczeniowych.
false
- dla typu logicznego bool.
W C# istnieją konwersje jawne (przy użyciu rzutowania) i niejawne
(przeprowadzane automatycznie w razie potrzeby). Rzutujemy stosując konstrukcję:
( typ ) wartość . Przykładowo:
int liczba = 100;
long długaLiczba = liczba ; // Konwersja niejawna
short krótkaLiczba = (short) liczba ; // Jawna konwersja
Każdy typ posiada własny zestaw reguł definiujących zasady konwersji. Dla typów wbudowanych niejawne konwersje są dostępne wtedy, gdy nie powodują utraty informacji (np. z int do long, z float do double, z long do float, ...) w innym przypadku musimy stosować rzutowanie.
W klasie możemy zdefiniować możliwość jej konwersji do innego typu. W
tym celu należy zdefiniować metodę typu
public static implicit operator Typ ( ... )
aby móc niejawnie przekształcać klasę do typu Typ lub funkcję
public static explicit operator Typ ( ... )
aby móc to zrobić jawnie. Ponadto taka funkcja musi pobierać jeden parametr
takiego typu jak definiowana klasa, np.
public class A {
double wartość ;
public static implicit operator double ( A a ) { return a.wartość ; }
public static explicit operator long( A a ) { return (long)wartość; }
}
Tablice są obiektami (wywodzącymi się z System.Array) umożliwiającymi przechowywanie wielu elementów pewnego typu w ciągłym obszarze pamięci. Operator [ ] służy do tworzenia i indeksowania tablic (indeksy rozpoczynają się od zera). Po utworzeniu tablicy jej długość nie może być już zmieniona. W przypadku przekroczenia zakresu tablicy zgłaszany jest wyjątek IndexOutOfRangeException.
Tworzenie tablic typów bezpośrednich jest bardzo efektywne, np.
int[] tablInt = new int[ 100 ] ;
int[ ] tabl = { 1, 2 , 3 } ;
MojaStruktura[ ] tablStr = new MojaStruktura[100];
Tworzenie tablic typów referencyjnych wymaga późniejszej inicjalizacji
wartości tablicy, np.
mKlasa[] tablKlas = new mKlasa[100];
/* Na razie tablica jest wypełniona referencjami o wartości null. */
for ( int i=0; i < tablKlas.Length; i++ ) tablKlas[i] = new mKlasa();
/* inicjalizacja tablicy. */
Możliwe jest tworzenie tablic wielowymiarowych:
int [,,] tabl3D = new int [5][10][10]; // Tworzy regularną tablicę.
int [ ] [ ] tabl2D = new int [10] [ ] ; // Tworzy tablicę nieregularną.
for( int a=0; a < 10; a++ ) // tablice nieregularne są
tabl2D[a] = new int[ a ] ; // tablicami tablic
Tablice posiadają składowe informujące o ich długości:
Length : informuje o długości tablic jednowymiarowych,
np. for( int i=0; i < tabl.Length ; i++) ... ;
GetLength : metoda pozwalająca uzyskać informację o długości
tablicy w danym wymiarze (dla tablic wielowymiarowych), liczonym od 0.
Np. for( int i=0; i < tabl3d.GetLength(2); i++) ... ;
enum : słowo kluczowe pozwalające zdefiniować typ wyliczeniowy (nowy typ danych, składający się z nazwanych stałych numerycznych).
Przykładowo:
public enum DniTyg { Pn, Wt, Sr, Czw, Pt, Sb, Nd } ; // Definicja typu . . . DniTyg dzien = DniTyg.Pn ; // Deklaracja zmiennej. Nazwy składowych // wyliczenia muszą być poprzedzone nazwą // typu wyliczeniowego.
Domyślnie kolejne elementy typu wyliczeniowego (stałe numeryczne)
mają przyznawane wartości liczbowe w kolejności 0, 1, 2, 3 itd. Można
jednak zdefiniować własny porządek, np.
public enum Kierunki { Północ = 1, Południe=2, Wschód=4, Zachód=8 }
Kierunki k = Kierunki.Północ | Kierunki.Wschód ;
Może zajść niejawna konwersja pomiędzy typem wyliczeniowym a numerycznym (lub odwrotnie), ponadto typ wyliczeniowy może być jawnie konwertowany do innego typu wyliczeniowego.
Wszystkie funkcje w C# są składowymi pewnych typów - pełnią rolę metody
pewnej klasy lub struktury. Ogólna deklaracja metody:
[modyfikatory] typ nazwaMetody ( [listaParametrów] ) { ... }
Dostępne modyfikatory metod:
public : (oraz private, protected, internal, protected internal). Modyfikuje dostęp do metody.
virtual : pozwala tworzyć metodę wirtualną (przydatne przy dziedziczeniu).
abstract : pozwala utworzyć metodę abstrakcyjną (przydatne przy dziedziczeniu)
extern : wskazuje, że metoda jest zaimplementowana z użyciem kodu nienadzorowanego.
static : metoda statyczna działa na rzecz całego typu, a nie pojedynczego
obiektu wywołujemy ją według wzoru : nazwaTypu.nazwaFunkcjiStatycznej() .
Aby wywołać metodę statyczną nie potrzebujemy żadnego obiektu (klasy lub struktury).
Na liście parametrów poza typem możemy podać modyfikatory:
ref : pozwala przekazać dany parametr przez referencję (domyślnie
parametry są przekazywane przez wartość. Modyfikator ref musi się pojawić na
liście parametrów metody oraz podczas jej wywoływania).
out : parametr tego typu służy przekazaniu wartości
z metody na zewnątrz (domyślnie parametr przekazuje wartość z zewnątrz
do metody). Przekazując do metody zmienną w trybie out przekazujemy ją
przez referencję, oczekując jednocześnie że wewnątrz metody zostanie jej nadana
odpowiednia wartość. Modyfikator out musi się pojawić na liście parametrów metody
oraz podczas jej wywołania.
params : dzięki temu modyfikatorowi metoda może przyjmować
dowolną liczbę parametrów. Modyfikator params może być zastosowany tylko do
ostatniego parametru metody.
public class A {
public void f_1 ( ref int parametr ) { ... }
public void f_2 ( params int[ ] tabl ) {
}
}
...
A a = new A( ) ; // Utworzenie obiektu klasy A
a.f_1( ref x ); // Przekazanie przez referencję zmiennej int x ;
a.f_2( 1, 2, 3 ); // Tablica tabl (wewnątrz metody f_2) będzie
// zawierała 3 elementy
Możliwe jest przeciążanie metod (może istnieć wiele metod o tej samej nazwie, ale pobierających różne parametry. Kompilator dokona właściwego wyboru metody).
Atrybuty pozwalają uzupełnić elementy kodu (deklaracje typów i składowych) o pewne dodatkowe informacje. Te dodatkowe informacje będą przechowywane w metadanych opisujących dany element kodu.
Atrybuty zapisujemy: [ NazwaAtrybutu ]
Przykładowo pisząc usługę WebService i chcąc udostępnić publicznie pewną
metodę wystarczy przed jej definicją zapisać atrybut [WebMethod],
np.
[WebMethod]
public int dodaj(int a, int b)
{
return a + b;
}
Konkretne atrybuty mogą być umieszczane tylko przed konkretnymi elementami języka. Przykładowo atrybut [Serializable] może być umieszczony przed definicją klasy lub struktury, ale nie przed deklaracją funkcji.
Atrybut może pobierać parametry.Np.
[Obsolete("Typ przestarzały", true)]
public class A { ... }
Atrybuty są klasami wywodzącymi się z klasy System.Attribute (jednak ich użycie następuje już według specjalnej konwencji, z użyciem nawiasów kwadratowych). Istnieją atrybuty predefiniowane (np. Serializable, Obsolete, NonSerialized, ... ). Ponadto użytkownik może definiować własne atrybuty, wyprowadzając je z klasy System.Attribute.
Właściwości są bardzo podobne do pól ... oraz metod. Ogólna deklaracja właściwości:
[modyfikatory] typ nazwaWłaściwości { [modyfikatory] get { ... } [modyfikatory] set { ... } ... }
Modyfikatorami dla właściwości mogą być modyfikatory określające rodzaj dostępu (public, private, ...) oraz virtual, static i abstract.Np.
public class A { float wartość ; // To jest pole prywatne public int wart( ) { get { return (int)wartość ; } set { wartość = (float)value ; } } } ... A a = new A( ); a.wart = 10 ; // użycie części set deklaracji właściwości Console.Write( a.wart ); // użycie części get
Część get właściwości musi zwracać wartość o takim samym typie jak typ właściwości, natomiast część set właściwości posiada parametr value, zgodny z typem właściwości.
W rzeczywistości, w podanym wyżej przykładzie właściwość wart() zostanie przez kompilator przekształcona do dwóch metod - get_wart( ) oraz set_wart( ). Ponadto metody te będą typu inline (będą rozwijane w miejscu wywołania - co zapewnia większą szybkość ich 'wywołania').
Definiując właściwość możemy określić samą część get - w takim wypadku będzie to właściwość tylko do odczytu
Istnieje także specjalny rodzaj właściwości, który pozwala wprowadzić indeksowanie do tworzonej przez nas klasy. Od zwykłych właściwości różni się tym, że zamiast nazwaWłaściwości(...) wstawiamy this[...]. Np.
public class Tablica2D { ... int[ ] tablica = new int[ wlkX * wlkY ] ; public int this [ int x, int y ] { get { return tablica[ x + y*wlkX ] ; } set { tablica[x+y*wlkX] = (int) value ; } } } ... Tablica2D tabl = new Tablica2D( 10,10 ); tabl [0,0] = 0 ; // klasę można indeksować