A LINQ és a funkcionális programozás hatása az alábbi új elemeket hozta a C#-ba:
A kód leírásakor azt volt a szándékunk, hogy egy kontaktszemélyt azonosítóval, névvel és nem kötelezően kitöltendő születési dátummal lássunk el, kiolvashassuk az életkorát (ha ismerjük a születési dátumát). A fenti kód „nem az optimális megoldása” ennek az elvárásnak:
A mezők helyett tulajdonságokat bevezetve már meg tudunk felelni az elvárásoknak:
A tulajdonságokhoz (az Age kivételével) saját private mezőket kellett létrehoznunk és azok eléréséhez a megfelelő get és set elérési pontokat definiálnunk. Ebben az egészben a „legmunkásabb” rész a mezőkhöz tartozó tulajdonságok leírása: a get egyszerűen kiolvassa a mező értékét, a set pedig beállítja. „Hát ez az, amire majmot lehet idomítani” — mondják a lustább fejlesztők, már pedig a fejlesztők (köztük én is) lusták, ha gépelésről van szó. Az ún. refactoring eszközök egy jelentős része tényleg be is idomítja a majmot: kapunk olyan funkciót, amely létrehozza a tulajdonságokat.
A C# 3.0-ban a fordítóprogram segít ebben nekünk az ún. automatikus tulajdonságok [automatic properties] képességével. Ezt azoknak a tulajdonságoknak az esetében használhatjuk, amelyek ténylegesen egy mező értékét olvassák ki és/vagy állítják be. A Contact osztály kódját ez a képesség az alábbi módon egyszerűsíti:
Kicsit rövidebb, könnyebben olvasható lett a programunk! A fenti szintakszis nyomán a fordítóprogram automatikusan létrehoz egy private mezőt és legenerálja a megfelelő get és set elérési pontokat.
Az automatikus tulajdonságok segítségével csak írható vagy csak olvasható tulajdonságokat úgy tudunk létrehozni, hogy a get vagy set elérési pont elé a private kulcsszót odatesszük:
Értelemszerűen a ReadOnlyNumber egy csak olvasható, a WriteOnlyName egy csak írható tulajdonság lesz. Vegyük észre, ez csak az osztálypéldány külső elérése esetén igaz! Az osztályon belül a tulajdonság írható és olvasható!
Ez a megoldási mód azért született, mert az automatikus tulajdonságok mögött lévő mezőhöz nem lehet közvetlenül hozzáférni a C# nyelv szintaktikáján keresztül, csak a reflection modellen át. Ez azonban egyfelől nem támogatott (gondoljunk csak az automatikus tulajdonságok nevére, ahogyan azt az előző fejezetrészben is láthattuk), másfelől igen csak vitatható megoldás — ráadásul szükség sincs rá. A megfelelő elérési pont elé írt private kulcsszó ezt a problémát megoldja: az osztályon belül el tudjuk érni a tulajdonságot írásra és olvasásra, kívülről azonban csak olvasásra vagy csak írásra.
A private helyett a protected kulcsszó használatával lehetővé tehetjük a tulajdonságok elérését a leszármazott osztályokból is. A fordítóprogram nem engedi meg, hogy mindkét elérési pont kapcsán azok elérési hatókörét is definiáljuk. Nem írhatjuk le pl. az alábbit:
Hasonló módon, a tulajdonság elérési pontjainál csak szűkíteni tudjuk az elérést, tágítani nem, ezért hibát kapunk az alábbi definícióra is:
A C# nyelv „kényelmetlen” tulajdonságai közé tartozik még objektumpéldányok, listák tömbök és egyéb összetett típusok inicializálása. Nézzünk meg néhány egyszerű példát ezekre! Tegyük fel, hogy az alábbi osztály példányaival fogunk dolgozni:
A Person egy olyan példányának létrehozása, amely három tulajdonság értékét beállítja, négy programsorba és a newPerson változónév négyszeri leírásába került. „Hozzunk létre konstruktorokat!” — lenne az első gondolatunk. Igen ám, de milyeneket? Ha a Person példány Name vagy NickName tulajdonsága közül elegendő az egyiket kitöltenünk, az Email kitöltése nem kötelező, akkor vajon milyen mezőket tölthet ki az a konstruktor, amelynek két string paramétere van? Nehéz ebben az esetben megfelelő explicit konstruktorokat létrehoznunk, hiszen az első három tulajdonság mind string típusú!
A C# 3.0 új, alternatív szintaktikát biztosít az objektumok inicializálására. A fenti példának megfelelő alapértékek beállítása az alábbi módon történhet:
Fontos, hogy az új szintaktika alkalmazásánál a tulajdonságok inicializálása balról jobbra, pontosan a felsorolásuk sorrendjében történik, ezt a fordítóprogram nem változtatja meg! Ha a tulajdonságok nem ortogonálisak (vagyis valamilyen függőség van közöttük), az inicializálási sorrend megváltoztatása felborítaná a fejlesztő eredeti szándékát.
Az objektumpéldányok inicalizálása során az objektumtípus konstruktorait és a tulajdonságok inicializálására használt listát kombinálhatjuk. Készítsük el például a Person típushoz az alábbi konstruktorokat:
Ekkor az alábbi módok mindegyike használható a kezdeti érték beállításához:
Az inicializáló listára nem csak konstans értékeket vehetünk fel, hanem tetszőleges kifejezéseket is, amint az alábbi példa illusztrálja:
Az objektumok inicializálásához hasonló problémát vet fel a tömbök inicializálása is. Nézzük meg pl. a következő egyszerű példát, amely az első 5 prímszámot egy tömbbe tölti:
Egy típust akkor nevezek összetett típusnak, ha elemei nem csak egyszerű (natív) .NET típusokat, hanem más (hierarchikus) típusokat is magukba ágyaznak. Például, a Person típust kibővíthetjük úgy az Employee típusra, hogy az tartalmazza alkalmazottaink nevét és kedvenc prímszámaikat is:
Az Employee típus inicializálásánál a beágyazott Address és NicePrimes tulajdonságokat az előző részekben bemutatott szintaktikát használhatjuk:
Az Address tulajdonság értékét a Zip, City és Street tulajdonságokból képzett Address objektumpéldányból állítjuk be, a NicePrimes értékét pedig a négyelemű listából.
Első hallásra talán furcsának tűnik, de a C# 3.0 szintaktikája megengedi ún. névtelen típusok létrehozását is. A névtelen típus valójában névvel is rendelkezik (anélkül nem is lenne a CLR-ben használható), de ezt a fordítóprogram jól eldugja előlünk. A névtelen típus az értelmét akkor nyeri el, amikor azt lokális típusfeloldással használjuk. Az előző részben bemutatott Employee típus newEmp példányát egy névtelen típusból is létre hozhatjuk:
Hát bizony a típus tényleg névtelen: a new kulcsszó után nem adtunk meg nevet! A fordítóprogram természetesen ad egy saját nevet a típusnak. A típus az inicializálási listán szereplő nevű tulajdonságokkal jön létre (azok típusait a listából következteti ki a fordítóprogram). A fenti mintapélda érdekessége, hogy nem csak a korábban használt Employee, de az Address típus adott példánya is névtelen példányként lett létrehozva.
Ebben a rövid példában kétszer is leírtuk a gyorsítótár típusát: egyszer a változó típusának deklarálásánál, egyszer pedig az inicializálásánál. Ha a későbbiekben szükségünk van a gyorsítótár egészének vagy egyetlen elemének lokális változóhoz rendelésére, az alábbihoz hasonló kódrészletet írunk le:
A „szintaktikai zajt” ebben a programrészletben a típusdeklarációk jelentik: az értékadások jobboldala eleve meghatározza, hogy a baloldalon szereplő változók típusa milyen lehet, azt mégis kiírjuk.
A C#3.0 ún. lokális típusfeloldás képessége ezt a „szintaktikai zajt” tűnteti el. A fordítóprogram a kifejezés jobboldalából kikövetkezteti (erre utal az angol névben az inference szó) a változó típusát és megszabadít bennünket attól a kényszertől, hogy azt ismételten leírjuk. A lokális típusfeloldás használatával a fenti programrészleteket az alábbi módon írhatjuk le:
A „lokális” szó nem véletlen: ezzel a konstrukcióval kizárólag lokális változók típusát tudjuk feloldani, mezőket már nem definiálhatunk így. Az alábbi kódrészlet például szemantikailag helytelen, mert egy osztály mezőjének a típusát szeretnénk feloldással kezelni:
Ezzel szemben az alábbi kódrészletben már szerepelhet a típusfeloldás és a kód helyes is lesz:
Az objektumok és gyűjtemények inicializálásához kapcsolódó C# 3.0 újdonságoknál már bemutattam egy rövid példát egy névtelen típusra:
Ebben a kódban a newEmp változó egy névtelen típus példánya, sőt ennek a típusnak az Address tulajdonsága egy másik névtelen típus. Ez a fenti deklaráció szintaktikailag helyes, de teljesen elveszítjük belőle az erős típusosságot, ugyanis a newEmp típusa System.Object. Az erős típusosság elvesztése az alábbi nehézségeket jelenti:
A lokális típusfeloldás gyógyírt jelenthet problémáink egy részére:
A var kulcsszó hatására a newEmp már nem System.Object típusként kezelődik, hanem egy konkrét típusként (még akkor is, ha ennek nincs neve). Magához a típushoz nem férhetünk ugyan hozzá, de annak jellemzőihez igen! Át tudjuk másolni például egyik példány értékét egy másikba, hozzáférhetünk a példány tulajdonságaihoz (azok értékéhez), sőt a tulajdonságok által lefedett típusokhoz is!
Az empClone változó típusa (és értéke) megegyezik a newEmp változóéval. Az empClone erősen típusos jellegét mutatja, hogy pl. az Address tulajdonságához közvetlenül hozzáférhetünk. Az addr változó típusa megegyezik az Address tulajdonság (egyébként névtelen) típusával.
A névtelen típusok további létértelmet nyernek, amikor azokat LINQ lekérdezésekkel együtt használjuk: létrejöhetnek olyan névtelen típusok is, amelyek konténerek és így elemeiket be is járhatjuk, meg is változtathatjuk!
A String osztálynak olyan metódusai vannak, amelyek segítségével gyorsan és érthetően le tudok írni egyszerű szöveges műveleteket. Például:
Ez a leírásmód igen kényelmes és olvasható is. A String osztályból azonban hiányzik néhány metódus, amely a munkámat még könnyebbé tenné. Ilyen például a Right metódus, amely az egyébként létező Left metódus párja lenne. Gyakran hasznos lenne egy Reverse metódus, amely a string karaktereit megfordítja, illetve egy Stuff metódus, amely a string egyes karakterei közé más stringeket tölt be.
Ezeket a metódusokat legegyszerűbben egy statikus osztály segítségével tudom megvalósítani:
A metódusokat igen egyszerű használatba venni:
Ahogyan már korábban is használtam ezt a kifejezést: szintaktikai zajt tartalmaz. Egyfelől zavaró az, hogy a StringExtensions típusnév ott szerepel mindenhol, hiszen a .NET-ben típus nélkül nem létezik metódus, másfelől az, hogy a Reverse és Right műveletek a leírás során balról jobbra írva szerepelnek, a végrehajtásuk sorrendje viszont jobbról balra van. Mennyivel szebben nézne ki az a leírás, amelyet a String használata kapcsán már egyébként is megszoktunk:
Nos helyben vagyunk! A C# 3.0-ban a bővítő metódusok pontosan azt a szintaktikai konstrukciót teszik lehetővé, amelyet az alcím fölötti rövid példában leírtam. A bővítő metódusok olyan statikus metódusok, amelyek hívása szintaktikailag két módon is leírható:
A második leírásmód során a fordítóprogram elvégzi a feloldást: felismeri, hogy az Extend metódus a Helper osztály statikus metódusa és annak hívását a „hagyományos” szintakszisnak megfelelő formában fordítja le.
Ha a fordítóprogram csak azokat a metódusokat fogadja el bővítő metódusként, amelyeknél a felhasználó explicit módon jelzi, hogy azokat bővítő metódusnak szánja. Ezt a jelzést a metódus első paraméterének típusa előtti this kulcsszó jelzi, például:
Az intelliSense technológia segítségével a szerkesztés során egy adott környezetben az IDE automatikusan felkínálja a bővítő metódusokat is.
A fordítóprogram értelemszerűen csak azokat a típusokat vizsgálja át, amelyek a „látóterében” vannak. Ez azt jelenti, hogy ha a bővítő metódust tartalmazó típusunk nincsenek ebben a látótérben, akkor a using szekcióban a megfelelő névteret szerepeltetnünk kell.
Természetesen, a fordítóprogram nem tudja feloldani azt az esetet, ha egy adott környezetben egynél több azonos nevű bővítő metódust is talál: ilyenkor jelzi, hogy nem tudja a név feloldását elvégezni.
A bővítő metódusok természetesen generikusak is lehetnek — ez abból, ahogyan a fordítóprogram kezeli őket, talán teljesen nyilvánvalónak is tűnik. Definiáljuk el például az alábbi bővítő metódust:
Az adott bővítő metódust az alábbi módon használhatjuk:
Mivel a List < string> és az object[] mindegyike megvalósítja az IEnumerable <> generikus interfészt, ezért mindkét típus esetében annak példányaira alkalmazhatjuk a First bővítő metódust, az természetesen lefordul és az elvárásoknak megfelelően működik.
A bővítő metódusok segítségével akár az object típushoz is készíthetünk kiegészítéseket, például az alábbi metódussal:
Mivel minden típus a System.Object-ből származik, ez azt jelenti, hogy a Dump metódust bármelyik érték és referenciatípus példánya esetében használhatjuk. Ez önmagában még nem baj. Minden bővítő metódus megjelenik az IntelliSense által felkínált listán, így a Dump is. Vigyázzunk, mert ha túl sok ilyen metódust definiálunk, azok el fognak lepni bennünket, de legalább is az IntelliSense listákat.
Ha sok bővítő metódust szeretnék létrehozni egy adott típushoz kapcsolódóan, akkor soroljuk azokat különböző statikus típusokba és különböző névterekbe is a lefedett (javasolt) felhasználási területtől függően. Egy adott környezetbe mindig csak azokat a névtereket „engedjük be” a using kulcsszóval, amely ténylegesen olyan bővítést tartalmaz, amely az adott környezetben hasznos lehet számunkra.
Forrás : Novák István Blogja