Amikor programot írunk, akkor szükség lehet tárolókra, ahová az adatainkat ideiglenesen eltároljuk. Ezeket a tárolókat változóknak nevezzük. A változók a memória egy(vagy több) cellájára hivatkozó leírók. Egy változót a következő módon hozhatunk létre C# nyelven:

Típus változónév;

A változónév első karaktere csak betű vagy alulvonás jel (_) lehet, a többi karakter szám is. Lehetőleg kerüljük az ékezetes karakterek használatát.

A C# erősen (statikusan) típusos nyelv, ami azt jelenti, hogy minden egyes változó típusának ismertnek kell lennie fordítási időben. A típus határozza meg, hogy egy változó milyen értékeket tartalmazhat illetve mekkora helyet foglal a memóriában. A következő táblázat a C# beépített típusait tartalmazza, mellettük ott a .NET megfelelőjük, a méretük és egy rövid leírás:

C# típus .NET típus Méret (byte) Leírás
byte System.Byte 1 Előjel nélküli 8 bites egész szám (0..255)
char System.Char 2 Egy Unicode karakter
bool System.Boolean 1 Logikai típus, értéke igaz(1) vagy hamis(0)
sbyte System.SByte 1 Előjeles 8 bites egész szám (-128..127)
short System.Int16 2 Előjeles 16 bites egész szám (-32768..32767)
ushort System.UInt16 2 Előjel nélküli 16 bites egész szám (0..65535)
int System.Int32 4 Előjeles 32 bites egész szám (–2147483647.. 2147483647).
uint System.UInt32 4 Előjel nélküli 32 bites egész szám (0..4294967295)
float System.Single 4 Egyszeres pontosságú lebegőpontos szám
double System.Double 8 Kétszeres pontosság lebegőpontos szám
decimal System.Decimal 16 Fix pontosságú 28+1 jegyű szám
long System.Int64 8 Előjeles 64 bites egész szám
ulong System.UInt64 8 Előjel nélküli 64 bites egész szám
string System.String NA Unicode karakterek szekvenciája
object System.Object NA Minden más típus őse

A forráskódban teljesen mindegy, hogy a “rendes” vagy a .NET néven hivatkozunk egy típusra.

Alakítsuk át a “Hello C#” programot úgy, hogy a kiírandó szöveget egy változóba tesszük:

using System;

class HelloWorld
{
       static public void Main()
       {
	      //string típusu változó deklarációja, benne a kiírandó szöveg
               string message = "Hello C#";
               Console.WriteLine(message);
               Console.ReadKey();
       }
}

A C# 3.0 lehetővé teszi, hogy egy metódus hatókörében deklarált változó típusának meghatározását a fordítóra bízzuk. Ezt az akciót a var szóval kivitelezhetjük. Ez természetesen nem jelenti azt, hogy úgy használhatjuk a nyelvet, mint egy típustalan környezetet, abban a pillanatban, hogy értéket rendeltünk a változóhoz (ezt azonnal meg kell tennünk), az úgy fog viselkedni mint az ekvivalens típus. A ilyen változók típusa nem változtatható meg, de a megfelelő típuskonverziók végrehajthatóak.

int x = 10; // int típusú változó
var z = 10; // int típusú változó
z = "string"; // fordítási hiba
var w; //fordítási hiba

Néhány speciális esettől eltekintve a var használata nem ajánlott, mivel nehezen olvashatóvá teszi a forráskódot. A két leggyakoribb felhasználási területe a névtelen típusok és a lekérdezés-kifejezések.

Lokális változók

szerkesztés

Egy blokkon belül deklarált változó lokális lesz a blokkra nézve, vagyis a program többi részéből nem látható (úgy is mondhatjuk, hogy a változó hatóköre a blokkra terjed ki). A fenti példában a message egy lokális változó, ha egy másik függvényből vagy osztályból próbálnánk meg elérni, akkor a program nem fordulna le.

Referencia- és értéktípusok

szerkesztés

A .NET minden típusa a System.Object nevű típusból származik, és ezen belül szétoszlik érték- és referenciatípusokra. A kettő közti különbség leginkább a memóriában való elhelyezkedésben jelenik meg. A CLR két helyre tud adatokat pakolni, az egyik a verem (stack) a másik a halom (heap). A verem egy ún. LIFO (last-in-first-out) adattár, vagyis az az elem amit utoljára berakunk az lesz a tetején, kivenni pedig csak a legfelső elemet tudjuk. A halom nem adatszerkezet, hanem a program által lefoglalt nyers memória, amit a CLR tetszés szerint használhat. Minden művelet a vermet használja, pl. ha össze akarunk adni két számot akkor a CLR lerakja mindkettőt a verembe és meghívja a megfelelő utasítást ami kiveszi a verem legfelső két elemét összeadja őket, a végeredményt pedig visszateszi a verembe:

int x = 10;
int y = 11;
x + y

A verem:
|11|
|10| -->összeadás művelet-->|21|

Ez azt is jelenti egyben, hogy függetlenül attól, hogy értékről vagy referenciáról van szó, valamilyen módon mindkettőt be kell tudnunk rakni a verembe. Az értéktípusok teljes valójukban a veremben vannak, míg a referenciák a halomban jönnek létre és a verembe egy rájuk hivatkozó referencia kerül. De miért van ez így? Általában egy értéktípus csak egy-négy bytenyi helyet foglal el, ezért kényelmesen kezelhetjük a vermen keresztül. Ezzel szemben egy referenciatípus sokkal nagyobb szokott lenni és a memóriában való megjelenése is összetettebb, ezért hatékonyabb a halomban eltárolni. A forráskódban jól megkülönböztethető a kettő, míg referenciatípust a new operátor segítségével hozunk létre, addig egy értéktípusnál erre nincs feltétlenül szükség. Ez alól a szabály alól kivételt képez a string típus.

Boxing és unboxing

szerkesztés

Boxing –nak (bedobozolás) azt a folyamatot nevezzük, amely megengedi egy értéktípusnak, hogy úgy viselkedjen, mint egy referenciatípus. Mivel minden típus (érték és referencia is) a System.Object típusból származik, ezért egy értéktípust értékül adhatunk egy object típusnak. Csakhogy az object maga is referenciatípus, ezért az értékadáskor létrejön a memóriában (a halomban, nem a veremben) egy referenciatípus karakterisztikájával rendelkező értéktípus. Ennek előnye, hogy olyan helyen is használhatunk értéktípust, ahol egyébként nem lehetne. Vegyük a következő példát:

int x = 10;
Console.WriteLine("X erteke: {0}", x);

Elsőre semmi különös, de elárulom, hogy a Console.WriteLine() metódus ebben a formájában második paraméteréűl egy object típusú változót vár. Vagyis ebben a pillanatban a CLR automatikusan bedobozolja az x változót. A következő forráskód megmutatja, hogyan tudunk “kézzel” dobozolni:

int x = 10;
object boxObject = x; //bedobozolva

Console.WriteLine("X erteke: {0}", boxObject);

Most nem volt szükség a CLR –re.

Az unboxing (vagy kidobozolás) a boxing ellentéte, vagyis a bedobozolt értéktípusunkból kivarázsoljuk az eredeti értékét:

int x = 0;
object obj = x; //bedobozolva
int y = (int)obj; //kidobozolva

Az object típuson egy explicit típuskonverziót hajtottunk végre (erről hamarosan), így visszanyertük az eredeti értéket,

Konstansok

szerkesztés

A const típusmódosító segítségével egy változót konstanssá tehetünk. A konstansoknak egyetlen egyszer adhatunk (és ekkor kell is adnunk) értéket, mégpedig a deklarációnál. Bármely későbbi próbálkozás fordítási hibát okoz.

const int x; //Hiba
const int x = 10; //Ez jó
x = 11; //Hiba

A konstans változóknak adott értéket/kifejezést fordítási időben ki kell tudnia értékelni a fordítónak.

Console.WriteLine("Adjon meg egy szamot: ");
int x = int.Parse(Console.ReadLine());
const y = x; //Ez nem jó, x nem ismert fordítási időben

A felsorolt típus

szerkesztés

A felsorolt típus olyan adatszerkezet, amely meghatározott értékek névvel ellátott halmazát képviseli. Felsorolt típust az enum kulcsszó segítségével deklarálunk:

enum Animal { Cat, Dog, Tiger, Wolf };

Ezután így használhatjuk:

Animal a = Animal.Tiger;

if(a == Animal.Tiger) //Ha a egy tigris
{
       Console.WriteLine("a egy tigris...");
}

A felsorolás minden tagjának megfeleltethetünk egy egész értéket. Ha mást nem adunk meg, akkor az alapértelmezés szerint a számozás nullától kezdődik és deklaráció szerinti sorrendben (értsd: balról jobbra) eggyel növekszik.

enum Animal { Cat, Dog, Tiger, Wolf }

Animal a = Animal.Cat;
int x = (int)a; //x = 0
a = Animal.Wolf;
x = (int)a; //x = 3

Magunk is megadhatjuk az értékeket:

enum Animal { Cat = 1, Dog = 3, Tiger, Wolf }

Azok a nevek amelyekhez nem rendeltünk értéket explicit módon az őket megelőző név értékétől számítva kapják meg azt. Így a a fenti példában Tiger értéke négy lesz.

Null típusok

szerkesztés

A referenciatípusok az inicializálás előtt nullértéket vesznek fel, illetve mi magunk is jelölhetjük őket “beállítatlannak”:

class RefType { }

RefType rt = null;

Ugyanez az értéktípusoknál már nem működik:

int vt = null; //ez le sem fordul

Ez azért van, mert a referenciatípusok rengeteg plusz információt tartalmaznak, még az inicializálás előtt is, míg az értéktípusok memóriában elfoglalt helye a deklaráció pillanatában automatikusan feltöltődik nulla értékekkel. Ahhoz, hogy meg tudjuk állapítani, hogy egy értéktípus még nem inicializált egy speciális típust a nullable típust kell használnunk, amit a “rendes” típus után írt kérdőjellel (?) jelzünk:

int? i = null; //ez már működik

Egy nullable típusra való konverzió implicit (külön kérés nélkül) megy végbe, míg az ellenkező irányba explicit konverzióra lesz szükségünk (vagyis ezt tudatnunk kell a fordítóval):

int y = 10;
int? x = y; //implicit konverzió
y = (int)x; //explicit konverzió