C Sharp programozás/Változók
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.
Típusok
[szerkesztés]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ó