A C# programozási nyelv

Típusok, típuskonstrukciók

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

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

Egyszerű típusok

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

Logikai értékek

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ó.

Egész számok

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ámok

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

Karakter literálok

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

Szövegek

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:

Szöveg
eredmény
string a = "hello, world";
 hello, world
string b = @"hello, world";
 hello, world
string c = "hello \t world";
 hello        world
string d = @"hello \t world";
 hello \t world
string e = "Joe said \"Hello\" to me";
 Joe said "Hello" to me
string f = @"Joe said  ""Hello"" to me";
 Joe said "Hello" to me
string g = "\\\\server\\share\\file.txt";
 \\server\share\file.txt
string h = @"\\server\share\file.txt";
 \\server\share\file.txt

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.

Struktúrák

[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.

Referencia típusok

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

Class

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.

Különbségek a class és a struct között

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.

Interface

[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.

Bővebben

Delegate

A delegate típusról az Új nyelvi elemek fejezetben lesz szó.

Tömbök

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.

Felsorolás

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} ;

Nullable típus

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 egy rövidebb formája. A példából látható, hogy a Nullable típusokhoz is - hasonlóan a hagyományos típusokhoz - az "=" operátor segítségével tudunk értéket rendelni.

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;

Típusok csomagolása

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.

Generikus programozás

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.

Változók

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.