A C# három alapvető típuskategóriát különböztet meg:
Míg az első két kategória bárhol használható, addig a mutatók csak az úgynevezett nem biztonságos környezetben vehetők igénybe. C#-ból koncepcionális szinten kiűzték a pointerkezelést, mindenhol a típusos referencia használata a kötelező, de a kompatibilitás érdekében ezzel a direktívával a fordító engedélyezi a típus nélküli pointerek használatát. Erről az Új nyelvi elemek fejezetben lesz szó.
Az érték típusok változói közvetlenül, tehát indirekció nélkül tartalmazzák értéküket. Azonban lehetőség van un. boxing eljárással objektumot gyártani az egyszerű típusból is.
Minden egyszerű típus az object ősosztályból származik. Fontos, hogy az elemi típusoknak van egy alapértelmezett konstruktora, mely a példányváltozó kezdőértékét false-ra, 0-ra illetve 0.0-ra állítja, a típusnak megfelelően.
Az érték típusokon belül megkülönböztetjük a struktúra (struct) és az enumerációs (enum) típusokat. A struktúra típusokon belül egy kitüntetett csoportot alkotnak az egyszerű (simple) típusok.
típus |
előjel |
méret (byte) |
értéktartomány |
sbyte |
+ |
1 |
-128 - 127 |
short |
+ |
2 |
-32768 - 32767 |
int |
+ |
4 |
-2147483648 - 2147483647 |
long |
+ |
8 |
-9223372036854775808 - 9223372036854775807 |
byte |
|
1 |
0 - 255 |
ushort |
|
2 |
0 - 65535 |
uint |
|
4 |
0 - 4294967295 |
ulong |
|
8 |
0 - 18446744073709551615 |
float |
+ |
4 |
kb. ±1.5x10-45 - ±3.4x1038 |
double |
+ |
8 |
kb. ±5.0x10-324 - ±1.7x10308 |
decimal |
+ |
12 |
kb. ±1.0x10-28 - ±7.9x1028 |
char |
|
2 |
tetszőleges Unicode karakter |
bool |
|
½ |
true vagy false |
Az egyszerű típusok a jól ismert előredefiniált alaptípusokat tartalmazzák:
bool, sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal. Ezek rendre a logikai, a 8, 16, 32, 64 bites előjeles és előjel nélküli illetve a 16 bites előjel nélküli (Unicode), valamint a 32 illetve 64 bites lebegőpontos számoknak (IEEE 754) felelnek meg.
Újdonság a decimal típus, mely 128 bit széles tört a 1.0*10-28 - 7.9*1028 intervallumban, 28-29 értékes jeggyel.
Ezek a típusnevek tulajdonképpen a System névtérben előre definiált struct típusok álnevei, ebből kifolyólag minden egyszerű típusnak vannak tagjai, pl. az int típusnak a MaxValue.
int i = int.MaxValue; // System.Int32.MaxValue constant
A logikai igaz érték jelölésére a true, a hamis érték jelölésére a false literál használható.
A kezdő karakter határozza meg a szabályrendszert:
Egy egész szám után írt "l" vagy "L" betű long típusúként deklarálja azt. A szám után írt "u", ill. "U" a megfelelő előjeles változatot jelenti. Egyszerre is használhatjuk mind a kettőt, pl.: 123uL - ulong-ként lesz deklarálva.
Lebegőpontos számokat decimális számrendszerben lehet megadni. A számban szerepelhet tizedespont, előjel, exponens jelölés valamint a típusra vonatkozó utótag is, például: -12344.3e4F
Karaktert aposztrófok között lehet megadni, például: 'a'. Használhatunk speciális escape szekvenciákat is:
\' |
aposztróf |
0x0027 |
\" |
idézőjel |
0x0022 |
\\ |
fordított perjel |
0x005C |
\0 |
null |
0x0000 |
\b |
törlés |
0x0008 |
\f |
lapdobás |
0x000C |
\n |
újsor |
0x000A |
\r |
sorvissza |
0x000D |
\t |
vízszintes tab |
0x0009 |
\v |
függőleges tab |
0x000B |
A szövegeket idézőjelek közé tesszük. Egy speciális karakter, a @ használatával az őt követő szöveget úgy, ahogy le van írva betű szerint értelmezi a fordító, például:
A string típus Unicode karaktersorozatokat ábrázol. Összehasonlításnál vagy karakterről karakterre meg kell egyezniük (azonos hossz mellett), vagy mindkettő null értékkel kell rendelkeznie az egyenlőséghez.
[hozzáférési kategória] struct struktúranév [:interfacek]
{
deklarációk;
};
C#-ban a struct érték típus, de más nyelvekkel ellentétben tartalmazhat konstansokat, metódusokat, konstruktorokat, operátorokat, indexereket. Az egyszerű, nagy mennyiségben előforduló objektumokat lehet vele költségtakarékosan ábrázolni. (Példa.: Egy pontokat tartalmazó 1000 elemű tömb) Lehetőség van interface hivatkozások deklarálására is, és ezek implementálására. A struct deklarálásakor a benne szereplő tagok automatikusan felveszik kezdeti alapértéküket, ezért nem is lehet paraméternélküli konstruktort deklarálni a struct-ban.
A C# nem támogatja az uniók (union) használatát.
A referencia típusok legfőbb tulajdonsága, hogy tartalmuk közvetetten egy referencián keresztül érhetők el. A nyelven a referenciát külön nyelvi elem nem jelöli. A típus példányai fizikailag memória halomterületén foglalnak helyet. Referencia típusok: class, interface, object, string, array, delegate
Viselkedését és lehetőségeit tekintve megfelel a Java megközelítésének. Részletesebben erről az Objektum-elvű programozás fejezetben lesz szó.
[hozzáférés kategória] class oszálynév[:ősök]
{
deklarációk;
}
Az osztály referencia típus, melynek adattagok, konstruktorok, destruktorok, metódusok, konstansok, propertik, operátorok, indexerek, osztály adattagok lehetnek a deklarációs részében.
Az object (osztály)típus minden típus ősosztálya.
A struct, illetve a class szintaxisa tehát hasonló a C++-ban megszokottakkal, azonban szemantikailag más-más jelentésük van. A struktúrák érték típusok, míg az osztályok referencia típusok. A struktúrák tehát értéküket közvetlenül tartalmazzák. A struktúrák lehetőségei azonban nem korlátozódnak a más nyelvekből ismert rekord szerkezetekre: egy struktúrának például lehet konstruktora.
[hozzáférés] interface interfacenév[: őslista]
{
deklarációk;
};
A deklarációs rész metódusokat, propertiket és indexereket tartalmazhat. Őslista csak interface listát tartalmazhat, ebből származik a deklarált interface. Az interface-t egy osztály tudja implementálni. A property deklaráció üres get/set metódusokat tartalmaz.
A delegate típusról az Új nyelvi elemek fejezetben lesz szó.
Az array típus segítségével tömböket hozhatunk létre. Minden tömb a System.Array osztályból származik.
int[,] b = {{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8,9}};
int[] a = new int[5];
for (int i = 0; i < a.Length; i++) a[i] = i * i;
if(b[4, 1] == 9 || b[0, 0] == 0){…}
A tömb azonos típusú elemek indexelt sorozata. A tömb elemeinek számozása 0-val kezdődik, ami azt jelenti, hogy a tömb első eleme a 0.-ik pozíción lesz tárolva. Őket alapvetően kétféleképpen lehet deklarálni: adott hosszúságú vagy dinamikus. Az adott hosszúságú tömbök a deklarációnál definiált számú elemet tartalmazhatnak, míg a dinamikus tömbök lehetővé teszik, hogy a tömb mérete változtatható legyen. A nyelvben a tömbök objektumok, ami nem azt jelenti, hogy ha deklarálunk egy tömböt, akkor létre is jön egy tömb. A deklaráció után szükség van a tömb példányosítására, ezt a new operátorral tehetjük meg. A tömbök inicializására a {} jeleket használjuk.
Adott méretű tömb deklarálása: int[] Tomb; Tomb = new int[3];
Ugyanez a tömb inicializálva: Tomb = new int[3] { 1,2,3 }
Dinamikus tömb létrehozása inicializálással: Tomb = new int[] { 1,2,3 }
A deklarációval egybekötött inicializáció: int[] Tomb = new int[3] { 1,2,3}
Ha egy tömböt nem inicializálunk , akkor a tömb elemei automatikusan inicializálódnak a elem típusának alapértelmezett inicializáló értékére.
A nyelvben a tömböket négy típusba lehet sorolni: egy-dimenziós tömbök, múlti-dimenziósak vagy más néven négyszögszerűekre, kesztyűszerűek és a kevert típusúakra.
Példa egy kesztyűszerű dinamikus tömbre:
int[][] numArray = new int[][] {new int[] {1,3,5}, new int[] {2,4,6,8,10}};
Az egyszerű tömbök a legegyszerűbb formái a tömböknek. Az ilyen típusú tömbök egy előre definiált elem tárolására szolgálnak. A múlti-dimenziós tömbök olyan példányok, amelyek dimenziója több mint 1. A kesztyűszerű tömböket gyakran hívják tömbök tömbjének is. A kesztyűszerű tömb elemei maguk is tömbök. A kevert típusú tömbök a múlti-dimenziós és a kesztyűszerű tömbök kombinációja.
C#-ban a minden tömb típus a System.Array bázistípusból származik. Az Array osztály egy absztrakt bázisosztály, de a CreateInstance metódusa létre tud hozni tömböket. Az Array bázisosztály biztosítja a műveleteket a tömbök létrehozásához, módosításához, bennük való kereséshez illetve rendezésükhöz.
Az Array osztály tulajdonságait megadó függvények:
IsFixedSize |
Visszatérési értéke megmondja, hogy a tömb rögzített hosszúságú-e |
IsReadOnly |
Visszatérési értéke megmondja, hogy a tömb írásvédett-e |
IsSynchronized |
Visszatérési értéke megmondja, hogy a tömb elérése kizárólagos-e (thread-safe) |
Length |
Megadja a tömb elemeinek számát |
Rank |
Visszaadja a tömb dimenziójainak számát |
SyncRoot |
Visszatér egy objektummal, amit a tömb szinkronizált hozzáféréséhez használhatunk |
Az Array osztály biztosít még számos szolgáltatást a tömbökhöz, többek közt: bináris keresés az elemek között, az elemek rendezése. Szintén a bázisosztály jóvoltából a tömbök tudják saját méretüket, korlátait, sőt meg is tudja változtatni saját bejárási irányát:
BinarySearch |
Ez a metódus valósítja meg a bináris keresést a tömbön. |
Clear |
A metódus minden elemet töröl a tömbből és az elemszámot 0-ra állítja. |
Clone |
A metódus másolatot készít a tömbről. |
Copy |
Az eljárás egy tömb részét átmásolja egy másik tömbbe, végrehajtja az esedékes típuskényszerítést és csomagolást (boxing). |
CopyTo |
A metódus átmásolja az elemeket egy egy-dimenziós tömbből egy másik egy-dimenziós tömbbe egy megadott indextől kezdve. |
CreateInstance |
Létrehoz egy tömb példányt. |
GetEnumerator |
Visszatér egy IEnumerator-al a tömbhöz. |
GetLength |
Visszaadja az elemek számát a tömbben. |
GetLowerBound |
Megadja a tömb alsó korlátját. |
GetUpperBound |
Megadja a tömb felső korlátját. |
GetValue |
Visszatér a megadott indexű elem értékével. |
IndexOf |
Visszaadja az egy-dimenziós tömbben az első érték indexét. |
Initialize |
Egy értéktípusú tömbben minden elemre meghívja az elemek alapértelmezett konstruktorát. |
LastIndexOf |
Visszaadja az egy-dimenziós tömbben az utolsó érték indexét. |
Reverse |
Megfordítja a tömb vagy tömbrészlet bejárási irányát. |
SetValue |
A megadott elemet beteszi a megadott helyre a tömbben. |
Sort |
A tömbön rendezést hajt végre. |
Az enum kulcsszó és a felsorolás neve után kapcsos zárójelben soroljuk fel az értékeket.
enum Color { Red, Blue, Green };
A név után opcionálisan megadható a felsorolás típusa (pl: hogy kisebb helyet foglaljon). Illetve az egyes értékhez rendelt érték (alapesetben 0-tól indexelődik és egyesével nő).
enum A :byte {a=1,b=4} ;
Bizonyos esetekben előfordulhat, hogy egy változó kezdeti értékét nem tudjuk, vagy egyszerűen nem akarjuk meghatározni. Tipikus példa erre egy olyan felhasználói felület, ahol vannak opcionálisan megadható bemeneti paraméterek is. Ekkor nem csak a felhasználó által megadott értéket kell tárolnunk, hanem azzal az esettel is számolnunk kell, hogy a felhasználó esetleg nem is adott meg semmilyen értéket. Másik gyakori példa az adatbátisból történő olvasás, ahol bizonyos attribútumok a speciális NULL értéket is felvehetik. Referencia típusok esetén nem jelenthet gondot a NULL értékek tárolása, hiszen ebben az esetben a kérdéses változóknak egyszerűen a null értéket kell adnunk. Érték típusok azonban csak a reprezentáns értékek valamelyikét vehetik fel, így azok nem lehetnek null-ak. A kérdés tehát az, hogy miként jelölhetnénk a meg nem határozott értéktípusokat.
Egyik lehetséges megoldás az, ha egy speciális értékkel, például az int típus esetén a felvehető legkisebb vagy legnagyobb értékkel jelöljük azt, hogy az érték nem meghatározott. Azonban ez az érték is eleme a reprezentáns értékek halmazának, így fennáll annak a valószínűsége, hogy a felhasználó pont ezt az értéket kívánja megadni (amit a program tévesen úgy fog érelmezni, hogy nem adtak meg értéket). Másik lehetőség lehet egy további bool típusú mezőt felvétele k a kérdéses változó mellé, amelyben jelöljük, hogy egyáltalán meghatározott-e az érték. Ez a módszer azonban kissé nehézkes lehet a programozó számára.
C# esetében az igazi megoldás az úgynevezett Nullable típus, amely valójában egy generikus osztály, és sablon paraméternek a változó értékének típusát várja. Nullable típust azonban a "?" operátor segítségével is megadhatunk. Az alábbi példán keresztül mutatjuk be a nullable típusok használatát:
Nullable<double> d = 0.1;
int? i = null;
if (i.HasValue == true)
{
System.out.println(i.GetValueOrDefault());
}
A T? szintaxis tehát a Nullable
A GetValueOrDefault() metódus a Nullable változóhoz rendelt értéket adja vissza, ha az létezik. Ha a vátozóhoz semmit sem rendeltünk, akkor a típus alapértelmezett értékével tér vissza, illetve referencia típus esetén null-al. A Value property hasonló az előzőhöz, azzal a különbséggel, hogy ha a Nullable változónak nincs értéke, akkor InvalidOperationException kivételt kapunk. Azt, hogy van-e egyáltalán értéke a Nullable változónknak, a HasValue property segítségével kérdezhetjük le.
A Nullable típusok kezelésére egy másik hasznos szintaktikai könnyítés is rendelkezésre áll: a "??" operátor. Ha az operátort egy Nullable típusú változón alkalmazzuk, akkor a változó értékével tér vissza, amennyiben az létezik. Ha nem, akkor egy általunk megadott érték lesz az eredmény. Az operátor használatát a következő példa szemlélteti:
int? x = null;
int y = x ?? -1;
A fenti pédában az y változó értéke -1 lesz.
A Nullable típusokkal kapcsolatban fontos megjegyeznünk, hogy a beágyazott Nullable típusok nem megengedettek, tehát a következő kód nem fog lefordulni:
Nullable<Nullable<int>> i;
C#-ban a típusok csomagolása (boxing and unboxing) teremti meg a kapcsolatot az érték és a referencia típuskategóriák között. A becsomagolás (boxing) segítségével egy érték típust implicit módon egy csomagoló osztályba másolhatunk. A másolat így - objektum révén - referencia típusú lesz.
int i = 123;
object box = i;
Ugyanaz, mintha a második sor helyett ezt írnánk:
object box = new int_Box(i);
Ahol int_Box a következő:
class int_Box
{
int value;
int _Box(int t)
{
value = t;
}
}
Persze maga az int_Box nem jön létre.
A kicsomagolás (unboxing) segítségével egy már becsomagolt érték típust másolhatunk vissza érték típusúvá. A kicsomagolás csak explicit módon hajtható végre, azaz
object box = 123;
int i = (int)box;
vagyis meg kell adni a konvertálandó típus nevét. Ha a forrás null referencia volt, vagy inkompatíbilis a típus, akkor System.InvalidCastException kivételt kapunk.
A generikus programozás nyelvi támogatottsága a C# 2.0-ban jelenik meg. A generic-ek megengedik a típus-biztos programozást anélkül, hogy ez a kifejezőerő, vagy a teljesítmény rovására menne. A generic-et csak egyszer kell definiálni, és ez után bármely típussal használható. A típus paramétert <> közé kell tenni. Természetesen egyidejűleg több paramétert is lehet használni. Egy egyszerű példa:
public class Stack<T> // ezt nevezzük server-nek { T[] m_Items; public void Push(T item) {...} public T Pop() {...} }
Stack<int> intstack = new Stack<int>(); // ezt pedig client-nek
A C# generic-ek szintaktikailag hasonlók ugyan, mint a C++-os template-ek, de lényeges eltérések tapasztalhatók a megvalósításban. A C++ template-ekhez képest fokozott biztonsági funkciókkal rendelkezik, de ez a korlátozás is egyben. A .NET 2.0-ban a generic-eknek natív nyelvi támogatást nyújt a köztes nyelv. Tehát a C# complier a generic-eket is ugyanúgy fordítja köztes kódra, mint a közönséges típusokat, habár a konkrét típusok helyett csak paramétereket tartalmaz, és a meta adatok a generic server-ről generikus információkat tartalmaznak. Majd mikor a client fordítása történik, akkor kerül sor a típusellenőrzésre.
A köztes nyelvről a gépi kódba történő fordítás attól függ, hogy a generic paraméter referencia vagy érték típus. Ha érték típus, akkor a köztes nyelv fordítója (JIT) egyszerűen behelyettesíti a paramétert az adott érték típussal. Azonban a JIT nyomon követi, hogy már előzőleg milyen helyettesítéseket végzett el, és ha már egyszer elvégzett egy adott helyettesítést és még egyszer meg kellene tennie, a C++-al ellentétben csak egy referenciát ad vissza az előzőleg fordított kódrészletre. Ha viszont a paraméter típusa referencia, akkor a JIT egyszerűen behelyettesíti az Object típust a köztes nyelvbe, majd ezt fordítja gépi kódra és a többi referencia típusra is ezt a kódrészletet helyettesíti be.
Alapértelmezésben a generic server kódjában csak az Object típuson való metódusok és property-k engedélyezettek. Ha más metódusokat is szeretnénk használni, akkor a generic fejlécében jeleznünk kell, hogy a paraméter típusnak milyen osztály vagy leszármazottja, esetleg milyen interfészek felelnek meg, ezt a where kulcsszó segítségével tehetjük meg.
pl: public class Stack<T> where T : IComparable
Viszont abban az esetben, ha ezt a példát használnánk fel egy érték típusú paraméterrel minden egyes összehasonlításnál az érték típusból Object típust kellene generálni, és ez néha súlyos teljesítmény csökkenéshez vezethet. Ezért a C# 2.0-ban az összes olyan típusra, amelynek a C# 1.1 –ben az IComparable interfészt valósították meg, elkészítették hozzájuk az IComparabe<T> generikus interfészt. Amelynek már nincs szüksége a boxing/unboxing mechanizmusra.
A változók tárolási helyeket reprezentálnak. Minden változónak típusa van, ami meghatározza, hogy milyen értékeket lehet benne tárolni.
A változónak lehet kezdeti értéke, de ha nincs neki, akkor a program összes végrehajtási szálán értéket kell kapnia, hogyha a változó értékét valahol fel akarjuk használni. Minden változó alapértelmezett értéke a <típus>.default érték.
A C#-ban hétféle változótípust különböztethetünk meg: statikus változó, példányváltozó, tömbelem, értékparaméter, referenciaparaméter, kimeneti paraméter, lokális változó.
class A
{
public static int x;
int y;
void F(int[] v, int a, ref int b, out int c)
{
int i = 1;
c = a + b++;
}
}
Ebben a példában x statikus változó, y példányváltozó, v[0] tömbelem, a értékparaméter, b referencia paraméter, c kimeneti paraméter, i lokális változó.
A statikus változót a static kulcsszóval kell jelezni. Az ilyen változó egészen addig létezik, amíg a tartalmazó alkalmazásrész létezik. A statikus változónak mindig van kezdeti értéke (a típusának megfelelő).
A példányváltozó a static kulcsszó nélküli változó. Osztályban annak létrehozásakor jön létre, és megsemmisítésekor szűnik meg. Struktúrában hasonlóan, a struktúra létezésével egyidőben létezik.
A tömbelem a tömb példánnyal egyidőben létezik, van kezdeti értéke.
Értékparaméterek azok, melyek előtt nem szerepel a ref vagy az out kulcsszó. Metódus hívásakor kapja meg a függvénynek átadott értéket, így magától értetődik, hogy van kezdeti értéke.
A referencia paramétereket a ref kulcsszóval jelöljük. A referencia paraméter nem igényel új tárolási területet, ugyanarra a területre hivatkozik, mint a paraméterként átadott változó. Ha tehát a referencia paramétert változtatjuk, akkor az ugyanúgy látszik az átadott változón is. Mielőtt egy változót referencia paraméterként átadunk, értéke kell, hogy legyen.
Kimeneti paramétert az out kulcsszóval írhatunk elő. Nem igényel új tárolási területet, hasonlóan, mint a referencia paraméter. Hasonlóan is működik, a különbség, hogy az átadott paraméternek nem kell kezdeti értékének lenni, és a metódus normális befejeződése előtt értéket kell kapnia.
Lokális változó blokkra lokális, életciklusa az őt deklaráló blokk futásideje.