C# – Alapok


Microsoft .NET Framework

A Microsoft a kilencvenes évek végén elindította a Next Generation Window Services projektet, amelyből megszületett a .NET (ejtsd: dotnet) keretrendszer. Minden Windows rendszer részét képezi, és számos .NET könyvtár is épít rá. Maga a .NET platform a Microsoft, a Hewlett Packard, az Intel és mások közreműködésével megfogalmazott CLI  (Common Language Infrastructure) egy megvalósítása. A CLI egy szabályrendszer, amely maga is több részre oszlik.

Nincs .NET virtuális gép, helyette ún. felügyelt kódot használ, vagyis a program teljes mértékben natív módon, közvetlenül a processzoron fut, mellette pedig ott a keretrendszer, amely felelős pl. a memóriafoglalásért vagy a kivételek kezeléséért. A .NET nem programozási nyelv, hanem környezet. Gyakorlatilag bármelyik programozási nyelvnek lehet .NET megvalósítása.

A “hagyományos” programnyelveken (mint pl. a C++) megírt programok ún. natív kódra fordulnak le, vagyis a processzor számára (kis túlzással) azonnal értelmezhetőek. A .NET (akárcsak a Java) más úton jár, a fordító először egy köztes nyelvre (Intermediate Language) fordítja le a forráskódot. Ez a nyelv a .NET világában az MSIL (MicroSoft Intermediate Language), illetve a szabványosítás után a CIL (Common Intermediate Language), különbség csak az elnevezésben van.

A natív programok ún. gépi kódra fordulnak le, míg a .NET forráskódokból egy CIL nyelvű futtatható állomány keletkezik. Ez a kód a feltelepített .NET keretrendszernek szóló utasításokat tartalmaz. Amikor futtatjuk ezeket az állományokat, először az ún. JIT (Just–In–Time) fordító veszi kezelésbe, és lefordítja őket gépi kódra, amit a processzor már képes kezelni.

Amikor “először” fordítjuk le a programunkat, akkor egy ún. Assembly (vagy szerelvény) keletkezik. Ez tartalmazza a felhasznált, illetve megvalósított típusok adatait (ez az ún. Metadata), amelyek a futtató környezetnek szolgálnak információval (pl. osztályok szerkezete, függvényei stb.). Egy Assembly egy vagy több fájlból is állhat, tipikusan  .exe (futtatható állomány) vagy .dll (osztálykönyvtár) kiterjesztéssel.

A .NET Framework telepítésével a számítógépre kerül (többek között) a BCL (Base Class Library), ami az alapvető feladatok (fájl olvasás/ írás, adatbázis-kezelés, adatszerkezetek, stb) elvégzéséhez szükséges eszközöket tartalmazza. Az összes többi könyvtár ezekre épül.

Objektumorientált programozáselmélet (OOP)

A C# (ejtsd: szí-sárp) a Visual Basic mellett a .NET fő programozási nyelve. 1999-ben Anders Hejlsberg vezetésével kezdték meg a fejlesztését. A C# tisztán objektumorientált, típusbiztos, általános felhasználású nyelv. A tervezésénél a lehető legnagyobb produktivitás elérését tartották szem előtt. A nyelv elméletileg platform független (létezik Linux és Mac fordító is), de napjainkban a legnagyobb hatékonyságot a Microsoft megvalósítása biztosítja.

A korai programozási nyelvek nem az adatokra, hanem a műveletekre helyezték a hangsúlyt, mert akkoriban még főleg matematikai számításokat végeztek a számítógépekkel. Ahogy aztán a számítógépek széles körben elterjedtek, megváltoztak az igények, az adatok pedig túl komplexekké váltak ahhoz, hogy a procedurális módszerrel kényelmesen és hatékonyan kezelni lehessen őket. Az OOP már nem a műveleteket helyezi a középpontba, hanem az egyes adatokat (adatszerkezeteket) és a közöttük levő kapcsolatokat (hierarchiát). Az OOP tervezés elősegítésére hozták létre az UML-t (Unified Modelling Language). Ez egy általános tervezőeszköz (grafikus), a célja, hogy egy minden fejlesztő által ismert közös jelrendszert valósítson meg.

Osztály (class)

Az OOP világában egy osztály adatok (tulajdonságok) és függvények összessége. Egy osztály ilyen tulajdonságok összessége, sablonja. Nem egz konkrétan létező, megfogható valami, csak egy sablon. A sablon alapján tudunk létrehozni konkrét példányokat. Nézzünk erre egy egyszerű pédát. Legyen az osztályunk egy téglatest, ezt a matematikából már jól ismerjük. A téglatestnek lehetnek különböző tulajdonságai, illetve függvényekkel kiszámolhatjuk a felszínét, térfogatát és tömegét. Ez igaz, minden egyes téglatestre.

Tulajdonságok: Hosszúság, Szélesség, Magasság, Szín, Anyag, Anyagsűrűség
Függvények: Felszín, Térfogat, Tömeg

Az alábbi képen az előbbi, téglatest modellt (osztályt) láthatjuk

Ha a programunkban téglatestekkel szeretnénk dolgozni, ahhoz az adott osztályból létre kell hoznunk egy példányt, ezt példányosításnak nevezzük. Az osztály és példány közötti különbségre jó példa a téglatest (osztály) és az alábbi, zöld téglatest (példány). A példányokat objektumnak is nevezik. Az alábbi képen egy konkrét, zöld téglatest objektumot láthatunk. Rendelkezik minden olyan tulajdonsággal, amit a téglatest osztály leír.

Az általános téglatest osztályhoz képest, ez a zöld téglatest már konkrét értékekkel rendelkezik. Amikor példányosítjuk, akkor egy általunk, előre leírt szabály alapján tudja ezeket az értékeket felvenni. (lásd később: konstruktor)

Tehát, ez a zöld téglatest a programunkban már egy konkrétan létező, “megfogható” valami, ellenben a téglatest osztállyal, mely csak egy absztrakt fogalom.

Láthatóság

Az egyes tulajdonságokat és függvényeket nem feltétlenül kell közszemlére bocsátani. Az OOP egyik alapelve, hogy a felhasználó csak annyi adatot kapjon meg, amennyi feltétlenül szükséges. Az ős OOP szabályai háromféle láthatóságot fogalmaznak meg (ez nyelvtől függően bővülhet):

  • Public – mindenki láthatja
  • Private – csak az osztályon belül elérhető
  • Protected – csak az osztályon belül elérhető, de a leszármazott osztályok is láthatják

A C# még kettő láthatóságot ismer

  • Internal – csakis a tartalmazó assemblyn belül látható
  • Protected internal – a protected és az internal keveréke

Egységbe zárás

Az OOP egyik alapelve, hogy egységbe zárja az adatokat és a hozzájuk tartozó műveleteket / függvényeket. Az osztály kívülről meghívható függvényeinek összessége az osztály interface-e / “felülete”. Az interface segítségével végezhetünk azon bármilyen műveletet, akár egy adat kiolvasását is.  Ennek egyik nagy előnye, hogy egy adott osztály belső szerkezetét gond nélkül megváltoztathatjuk, mindössze arra kell figyelni, hogy a felület ne változzon. ( Ez a SOLID egyik alap ajánlása: wikipedia / SOLID )

Öröklődés

Az öröklődés vagy származtatás az új osztályok létrehozásának egy módja. Egy (vagy több) már létező osztályból hozunk létre egy újat úgy, hogy az minden szülőjének tulajdonságát örököli, vagy átfogalmazza azokat.

Konstruktor

Minden esetben, amikor egy osztályt példányosítunk, egy speciális függvény, a konstruktor fut le. Feladata, hogy „beállítsa” az osztály értékeit. A konstruktor neve meg kell, hogy egyezzen az osztály nevével, és semmilyen visszatérési értéke nem lehet. Ahhoz, hogy a programunkban ezt meg tudjuk hívni, a konstruktornak “public”-nak kell lennie. (Léteznek nem publikus konstruktorok is, de ezekre nem térünk most ki.)

Destruktor

A destruktorok a konstruktorokhoz hasonló speciális függvények, amelyek az osztály által használt erőforrások felszabadításáért felelősek. A .NET ún. automatikus szemétgyűjtő rendszert használ, amelynek lényege, hogy a hivatkozás nélküli objektumokat a keretrendszer automatikusan felszabadítja.

Névtér (namespace)

A .NET Framework osztálykönyvtárai szerény becslés szerint is legalább tízezer nevet, azonosítót tartalmaznak. Ilyen nagyságrenddel elkerülhetetlen, hogy a nevek ne ismétlődjenek. Ekkor egyrészt nehéz eligazodni közöttük, másrészt a fordító sem tudná, mikor mire gondolunk. Ennek a problémának a kiküszöbölésére hozták létre a névterek fogalmát. Egy névtér tulajdonképpen egy virtuális doboz, amelyben a logikailag összefüggő osztályok, függvények stb. vannak. Nyilván könnyebb megtalálni az adatbázis-kezeléshez szükséges osztályokat, ha valamilyen kifejező nevű névtérben vannak. Ilyen névtereket akár mi is létrehozhatunk magunknak.

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 alul vonás jel lehet, a többi karakter szám is. Lehetőleg kerüljük az ékezetes karakterek használatát. Konvenció szerint a változónevek kisbetűvel kezdődnek. Amennyiben a változónév több szóból áll, akkor célszerű azokat a szóhatárnál nagybetűvel “elválasztani” (pl. pirosAlma, vanSapkaRajta stb.). Ezt hívják “camelCase”-nek.

Deklaráció és definíció

Egy változó (illetve lényegében minden objektum) életciklusában megkülönböztetünk deklarációt és definíciót. A deklarációnak tartalmaznia kell a típust és azonosítót, a definícióban pedig megadjuk az objektum értékét. Értelemszerűen a deklaráció és a definíció egyszerre is megtörténhet. Például:

int x; <– deklaráció
x = 10; <– definíció
int y = 11; <– deklaráció és definíció

Típusok

A C# erősen típusos nyelv, ami azt jelenti, hogy minden egyes változó típusának ismertnek kell lennie fordítási időben, ezzel biztosítva, hogy a program csakis olyan műveletet hajthat végre, amire valóban képes. 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:

A C# 3.0-tól kezdve már lehetséges egy függvény hatókörében deklarált változó típusának meghatározását a fordítóra bízni. Általában olyankor tesszük ezt, amikor hosszú típusnévről van szó, vagy nehéz meghatározni a típust. Ezt az akciót a var kulcsszóval kivitelezhetjük. Ez természetesen nem jelenti azt, hogy úgy használhatjuk a nyelvet, mint egy típustalan környezetet! Abban a pillanatban, amikor értéket rendeltünk a változóhoz (ráadásul ezt azonnal meg is kell tennünk), az úgy fog viselkedni, mint az ekvivalens típus. Az ilyen változók típusa nem változtatható meg, de a megfelelő típuskonverziók végrehajthatóak. Például:

int x = 10; <– int típusú változó
var y = 10; <– int típusú változó

Lokális és globális változók

Egy blokkon belül deklarált változó lokális lesz a blokkjára 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 blokkjára terjed ki). Egy ilyen blokk lehet pl. egy adott osztály, vagy függvény.

Globális változónak azokat az objektumokat nevezzük, amelyek a program bármely részéből elérhetőek. A C# nem rendelkezik a más nyelvekből ismerős globális változóval, mivel deklarációt csak osztályon belül végezhetünk.

Referencia- és értéktípusok

A .NET minden típusa direkt vagy indirekt módon a System.Object nevű típusból származik, és ezen belül szétoszlik érték- és referencia-típusokra. A kettő közötti különbség leginkább a memóriában való elhelyezkedésben jelenik meg.

Értéktípusok:

  • Az összes beépített numerikus típus (int, byte, double, stb.)
  • A felsorolt típus (enum)
  • Logikai típus (bool)
  • Karakter típus (char)
  • Struktúrák (struct)

Referencia-típusok:

  • Osztályok (class)
  • Interfész típusok (interface)
  • Delegate típusok (delegate)
  • Stringek
  • Minden olyan típus, amely közvetlen módon származik a System.Object–ből vagy bármely class kulcsszóval bevezetett szerkezetből

Az alapvető különbség egy referencia és egy érték típusú változó között, hogy amikor ezekkel a változókkal dolgozunk, azaz értéket adunk nekik vagy átadjuk őket egy függvénynek paraméterként (lásd később), akkor az értéktípusú változók valóban lemásolódnak / duplikálódnak a memóriában, azonban a referencia változók esetében csak egy új mutató / pointer jön létre. Ez azt jelenti, hogy ha csak simán létrehozunk egy új referencia változót az eredetiből (akár a korábbi téglatest példa alapján), majd azzal műveletet végzünk, akkor az eredeti példányban is változtatásokat hajtottunk végre (általában szándékunk ellenére). Amikor referencia típusnak adunk értéket, akkor ő csak a memória címet kapja meg. Ekkor úgynevezett “shallow copy” történik, és nem ténylegesen másolódik le a változó (“deep copy”).

A C# nyelvben a keretrendszer által adott adatszerkezetek nagy részénél ez a működés meg van írva az “=” operátorra. Például, ha egy stringet hozunk létre az eredeti példányból, akkor nem fogjuk az előbb említett furcsaságot tapasztalni. Erről fontos tudni, és mindig figyeljünk rá.

Konstansok

A const típusmódosító kulcsszó segítségével egy objektumot konstanssá, megváltoztathatatlanná tehetünk. A konstansoknak egyetlenegyszer adhatunk (és ekkor kötelező 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. Például:

const int y = 10;

Kulcsszavak

Szinte minden programnyelv definiál kulcsszavakat, amelyek speciális jelentőséggel bírnak a fordító számára. Ezeket az azonosítókat a saját meghatározott jelentésükön kívül nem lehet másra használni, ellenkező esetben a fordító hibát jelez. A legtöbb fejlesztőeszköz (így a Visual Studio is) megszínezi a kulcsszavakat, ezért könnyű elkerülni a fenti hibát. A C# 5.0 már 77 kulcsszót ismer:

Ezeken kívül létezik még 23 azonosító, amelyeket a nyelv nem tart fenn speciális használatra, de különleges jelentéssel bírnak. Amennyiben lehetséges, kerüljük a használatukat “hagyományos” változók, osztályok létrehozásánál:

Megjegyzések

A forráskódba megjegyzéseket tehetünk. Ezzel egyrészt üzeneteket hagyhatunk (pl. egy függvény leírása) magunknak vagy a többi fejlesztőnek, másrészt a kommentek segítségével dokumentációt tudunk generálni, ami szintén az első célt szolgálja, csak éppen élvezhetőbb formában.

// Egysoros komment
/* Többsoros komment */

A C# szintaktikája

Amikor egy programozási nyelv szintaktikájáról beszélünk, akkor azokra a szabályokra gondolunk, amelyek megszabják a forráskód felépítését. Ez azért fontos, mert az egyes fordítóprogramok csak ezekkel a szabályokkal létrehozott kódot tudják értelmezni. Ha a forráskód szintaxisa nem megfelelő, a program nem fordul le. A C# úgynevezett C-stílusú szintaxissal rendelkezik (azaz a C programozási nyelv szintaxisát veszi alapul), ez három fontos szabályt von maga után:

  • Az egyes utasítások végén pontosvessző ( ; ) áll.
  • A kis- és nagybetűk különböző jelentőséggel bírnak
  • A program egységeit (osztályok, függvények stb.) ún. blokkokkal jelöljük ki, kapcsos zárójelek ({ }) segítségével

Operátorok

Amikor programozunk, utasításokat adunk a számítógépnek. Ezek az utasítások kifejezésekből állnak, a kifejezések pedig operátorokból és operandusokból, illetve ezek kombinációjából jönnek létre:

i = x + y;

Az előbbi példában az x és az y operandusok, a + jel pedig az operátorAmikor több operátor is szerepel egy kifejezésben, a fordítónak muszáj valamilyen sorrendet felállítani közöttük, hiszen az eredmény ettől is függhet. Például:

10 * 5 +1

Ennél a kifejezésnél – sorrendtől függően – az eredmény lehet 51 vagy 60. A jó megoldás az előbbi (51), az operátorok végrehajtásának sorrendjében a szorzás és az osztás előnyt élvez (természetesen érvényesülnek a matematikai szabályok). A legelső sorrendi helyen szerepelnek pl. a zárójeles kifejezések, utolsón pedig az értékadó operátor. Ha bizonytalanok vagyunk a végrehajtás sorrendjében, akkor mindig használjunk zárójeleket, ez a végleges programra nézve semmilyen hatással nincs (és a forráskód olvashatóságát is javítja). A fenti kifejezés így nézne ki helyesen zárójelezve:

(10 * 5) + 1

A C# nyelv precedencia szerint 14 kategóriába sorolja az operátorokat (a kisebb sorszámút értékeli ki hamarabb a fordító):

  1. Zárójel, adattag hozzáférés (pont (‘.’) operátor), függvény hívás, postfix inkrementáló és dekrementáló operátorok, a new operátor, typeof, sizeof, checked és unchecked
  2. Pozitív és negatív operátorok (x = -5), logikai és bináris tagadás, prefix inkrementáló és dekrementáló operátorok, explicit típuskonverzió
  3. Szorzás, maradékos és maradék nélküli osztás
  4. Összeadás, kivonás
  5. Bit-eltoló (>> és <<) operátorok
  6. Kisebb (vagy egyenlő), nagyobb (vagy egyenlő), as, is
  7. Egyenlő és nem egyenlő operátorok
  8. Logikai ÉS
  9. Logikai XOR
  10. Logikai VAGY
  11. Feltételes ÉS
  12. Feltételes VAGY
  13. Feltételes operátor ( ? : )
  14. Értékadó operátor, illetve a “rövid formában” használt operátorok (pl: x +=y)

Értékadó operátor

Az egyik legáltalánosabb művelet, amit elvégezhetünk az, hogy egy változónak értéket adunk. A C# nyelvben ezt az egyenlőségjel segítségével tehetjük meg:

int x = 10;

Matematikai operátorok

  1. + összeadás
  2. – kivonás
  3. * szorzás
  4. / maradék nélküli osztás
  5. % maradékos osztás (az osztás maradékát írja ki)

Relációs operátorok

A relációs operátorok segítségével egy adott értékkészlet elemei közötti viszonyt tudjuk lekérdezni. Relációs operátort használó műveletek eredménye vagy igaz (true) vagy hamis (false) lesz. A numerikus típusokon értelmezve van egy rendezés reláció:

  • x > y (x nagyobb, mint y)
  • x >= y (x nagyobb vagy egyenlő, mint y)
  • x < y (x kisebb, mint y)
  • x <= y (x kisebb vagy egyenlő, mint y)
  • x == y (x egyenlő y-nal)
  • x != y (x nem egyenlő y-nal)

Logikai és feltételes operátorok

A C# logikai típusa (bool) két értéket képes felvenni: igaz (true) és hamis (false). Ennek a típusnak a segítségével elvégezhetőek a szokásos logikai műveletek, ezeket nézzük most meg.

  • && (ÉS operátor)
  • || (VAGY operátor)
  • ! (Tagadás operátor)

Rövid forma

Az értékadás rövidebb, szebb és hatékonyabb. Az összes aritmetikai operátornak létezik rövid formája. Nézzünk egy példát, az alábbiakban az x változó értékét megnöveljük 10-zel:

x += 10;

Ugyanezt hosszabban így tudtuk volna megoldani:

x = x + 10;

Az 1-el való növelésre, illetve csökkentésre van önálló operátor, ráadásul kétféle verzió is létezik.

  • ++x illetve –x (prefixes – elöl)
  • x++ illetve x– (postfixes – hátul)

Attól függően, hogy növeljük vagy csökkentjük az operandust, inkrementáló illetve dekrementáló operátorról beszélünk. Ez az operátor használható az összes beépített numerikus típuson, valamint a char illetve enum típusokon is, mivel ezeknek van numerikus megfelelőjük.