Archívum

Archive for 2008. január

Bevezetés a C# 3.0 újdonságaiba(4.) – Bővítő függvények

január 17, 2008 4 hozzászólás

A Bevezetés a C# 3.0 újdonságaiba sorozat utolsó cikkében a bővítő függvényekről (extension methods) lesz szó. Ez volt számomra az az újdonság, melytől egy picit fáztam eleinte. Én megrögzött híve vagyok az OO világnak, és hogy őszinte legyek számomra a bővítő függvények kilógnak az "encapsulation" (egységbe zárás) szellemiségéből. Vagy mégsem?

Tegyük fel, hogy van egy A osztályunk. Szeretnénk kibővíteni a képességeit azáltal, hogy megírunk egy-két plussz függvényt hozzá. Ha nem tudunk hozzáférni, az A osztály kódjához, mert mondjuk csak egy DLL-ben áll rendelkezésünkre, akkor a OO paradigma szerint a természetes lépés, hogy származtatunk belőle egy B osztályt, és odapakoljuk az új függvényeinket. Nade mi történik akkor. ha az osztályt a készítője ellátta a sealed kulcsszóval? Akkor bizony nincs származtatás! Nem hagytak nekünk más választást, mint szögre akasztani OO elveinket és kétségbe esésünkben definiálni egy statikus osztályt, amiben elhelyezzük a statikus függvényeinket, melyek paraméterként kaphatják az A osztály egy példányát. Hát érezzük, hogy ez minden csak nem az igazi. Nem kapcsolódik a függvényünk az osztályhoz, senki más nem fog tudni a függvényeinkről (nincs intellisense), stb… Pedig nem járunk messze a megoldástól, első pillantásra a bővítő függvények is valami ilyesmit csinálnak.

Bővítő függvény alapok

A bővítő függvények segítségével kiterjeszthetjük egy osztály képességeit, származtatás nélkül. Bővítő függvények készítésekor az első paraméter-t megelőzi a this kulcsszó, ezzel jelezvén, hogy az első paraméterként megjelölt típus nem más, mint az a típus, amit kiterjeszt! Nézzünk egy egyszerű példát erre.

Ez a bővítő függvény az object osztályt terjeszti ki. Meghíváskor egyetlen paramétert vár(ConsoleColor), majd ezzel a színnel kiírja az adott elem ToString() által visszaadott eredményét a konzolra. Szándékosan mondtam, hogy egy paramétert vár, ugyanis az első paraméter az az a típus, amit kiterjeszt, jelen esetben a object osztály.

Nézzünk egy valamivel hasznosabb példát. Szeretnénk minden gyűjteményünkhöz egy konzolra kiirató metódust.

Azokat a típusokat tudjuk felsorolni, melyek implementálják az IEnumerable interface-t, így kézenfekvő, hogy bővítsük ki azt. This kulcsszó után jön a típus, majd a változónév. Ez a függvény, minden olyan objektumra meghívható, amely az IEnumerable-t megvalósítja, bejárja a gyűjteményt (list) és kiírja az elemeket a konzolra.  Elég egyszerű nem igaz?

Felmerül a kérdés, hogy ez miben más, mint az eredeti elképzelésünk? Vessünk egy pillantást az alábbi kódrészletre, ahol meghívjuk a fenti bővítő metódust!

Úgy hívtam meg, mintha egy tag függvény lenne. Sőt, az intellisense fel is ajánlotta nekem! A WriteToConsole()-t meghívhattam volna egy int tömbre, egy arraylistre, egy string listára, stb…  Így már nem is haragszunk érte anniyra, nem igaz?

Néhány fontos tényezőt szem előtt kell tartani, bővítő metódusok készítésekor:

  1. A bővítő metódusokat publikusnak és statikusnak kell deklarálni
  2. Csak statikus osztályban helyezhetünk ell bővítő metódusokat
  3. A bővítő metódusok csak a publikus adattagokhoz férnek hozzá

A compiler fordításkor átnézi az összes statikus osztályt bővítő függvények után kutatatva, és fordítás időben elvégzi a szükséges kiterjesztéseket. Érték és referencia típusokat is egyaránt bővíthetünk. Az ajánlás szerint érdemes egy külön namespace-ben egy statikus osztályba pakolni az ilyen bővítő függvényeket, és inkább usingolni a namespace-t a projektünkben.

Generikus bővítő függvények

Érdemes egy kicsit külön tárgyalni a generikus bővítő függvényeket. A fenti példákban mindenhol meghatároztuk a típust, amit kiterjesztünk, így azt, hogy minél több típusra tudjuk alkalmazni, azzal váltottuk ki, hogy egy egyáltalános típust terjesztettünk ki, amiből származnak az egyéb specifikusabb típusok. (pl Objectből, IEnumerable-t valósítanak meg, stb…)

Ennél azért lehetünk elegánsabbak és jóval gyorsabbak is, ha generikus szerkezetekben gondolkodunk. Például a fenti WriteToConsole függvényt átalakíthatjuk generikussá is.

Így a generikus IEnumerable<T> Interface-t bővítettük ki. Látható az alábbi ábrán, hogy az intellisense el is árul nekünk minden fontos információt a WriteToConsole függvényről.

 

Amit észre kell venni, hogy ezáltal nem egy típust bővítettünk ki, hanem azoknak egy halmazát! Ugyanis a bővítő metódusokat el kell készíteni IEnumerable<int>, IEnumerable<string>, IEnumerable<Books>, stb.. esetekre is. Ha az első WriteToConsoleWithColor függvényünket generikussá alakítanánk, és this object element helyett, this T element-et írnánk, akkor már is nem az object osztályt terjesztnénk ki, hanem egy tetszőleges típust (pl int, books, decimal, vagy akár object). (Az intellisense az írná nekünk, hogy (extension) void T.WriteToConsole<T>()

Tagfüggvények Vs Bővítő függvények

Jogosan merül fel a névütközés kérdése. Mi van akkor, ha van az A osztályunkban egy X függvény, és készítünk a B statikus osztályba egy X bővítő függvényt az A típushoz. Ha meghívjuk az A.X()-et, akkor mi fog lefutni az A osztály X tagfüggvénye, vagy a B osztály X bővítő függvénye? A válasz egyszerű, a tagfüggvény élvez elsőbbséget.

A System.Linq névtérben rengeteg bővítő függvényt készítettek el nekünk, hiszen a Where, Select, Sum, Count, stb… egytől egyig ilyen bővítő függvények.

A bővítő függvények használata biztonságos és kényelmes is. Fordítás idejű ellenőrzést kapunk használatukhoz, segítségükkel igen rugalmas megoldásokat készíthetünk. Azonban a kétkedés jogát fenntartom és azt tanácsolnám mindenkinek, hogy azért, mert ez a lehetőség megadatott, ne essünk neki mindennek és kezdjünk gőzerővel ilyen bővítő függvényeket gyártani. Akár hogy is, a bővítő függvények nagyszerű, ugyanakkor kicsit veszélyes eszközt is adtak a kezünkbe. Igyekezzünk továbbra is csínján bánni a használatukkal, próbáljuk megőrizni a kódunkban az átláthatóságot, a struktúráltságot és az egységbe zárást is próbáljuk szem előtt tartni, ugyanis a bővítő függvények kicsit talán kilógnak ebből az erős OO szemléletből.

Remélem sokan hasznosnak találtátok ezt a bevezető jellegű cikksorozatot.

Kategóriák:Uncategorized

Bevezetés a C# 3.0 újdonságaiba(3.) – Lambda kifejezések

január 10, 2008 2 hozzászólás

Az egyik, talán első ránézésre legijesztőbb újdonság azok számára, akiknek a funkcionális programozás idegen, a C# 3.0-ban bevezetett lambda kifejezések. Segítségükkel kódot tudunk paraméterként átadni. Sokaknak biztos fel is sejlik a delegate-ek fogalma. Alapvetően egy sokkal kényelmesebb és érthetőbb szintaxis-t bocsátottak rendelkezésünkre, aminek segítségével névtelen függvényeket készíthetünk. Nézzük meg ugyanarra a feladatra, mindkét variációt:

Az első esetben a Where függvény paramétereként egy delegate-et(lényegében függvénypointert) adunk át és azt a kódot, amire mutat, rögtön definiáljuk is. Ugye ez a C# 2.0-ban bevezetett névtelen metódus. Ugye az történik, hogy minden listaelemre (adatbázisosan gondolkodva, "minden sorra") meghívjuk ezt a kódrészletet, ami akkor tér vissza igazzal, ha a szerző " John Steinbeck".

A második esetben lambda kifejezést használtunk. Azt mondtuk, hogy csak azok a "b"-k (Book példányok) érdekelnek, melyekre igaz az, hogy az "Author"-juk "John Steinbeck". Tehát a szintaxis lényegében a következő: paraméterek => kifejezés

Ami feltűnhet, hogy explicite nem adtuk meg "b" típusát. Ezt a fordító találta ki a Where bővítő metódus (későbbiekben részletesen) alapján. Természetesen explicite is meg lehet határozni a típust:

A fenti lambda kifejezés ún. predikátum, nem más, mint egy logikai kifejezés. Egy adott típusú paraméterre a visszatérési értek bool.

A projekció olyan lambda kifejezés, mely egy adott típusú paramétertől eltérő típust ad vissza, és nem logikai kifejezés.

A fenti példa összegzi a könyvek árait. A Sum függvénynek lambda kifejezés segítségével meghatároztuk, hogy a b példány Price változója alapján végezze el az szummázást.

A szokásos linq-s, sql-hez hasonló szintaxis, ilyen függvény hívásokba, a feltételek pedig lambda kifejezésekbe fordulnak. Sajnos ezt is nagyon jól kell ismernünk, ugyanis az szokásos sql-szerű szintaxisnak vannak korlátai, nem minden lehetőség érhető el benne.

Az alábbi kódrészlet ugyanazt a lekérdezést valósítja meg, az egyik sql-szerű szintaxissal (query expression), a másik lambda kifejezésekkel és bővítő függvényekkel (extension methods).

A lambda kifejezéseket váltózóknak értékül adhatjuk felhasználva a Func delegate típust.

Előre deklaráltak nekünk jó pár ilyen Func delegate típust, hogy lambda kifejezéseink kellően rugalmasak lehessenek.

A lambda kifejezések delegate-ként történő lefordítása és működése nagyon kényelmes, és kézenfekvő, ha memóriában lévő adathalmazokat szeretnénk lekérdezni (pl listák). De képzeljük el az a szituációt, amikor LINQ to SQL használatakor nagy adatbázisokból, nagy táblákból szeretnénk lekérdezni.

Fejeltsük most el a Book osztályunkat, és képzeljük el, hogy van egy nagy adatbázisunk, és az egyik táblája a "Books" nevet viseli. Gondoljunk bele, hogy mi lenne, ha előbb betöltené memóriába az adatokat, és aztán kezdené a lekérdezéseket végrehajtani. Amit várunk az az, hogy a linq to sql a lambda kifejezéseinket fordítsa SQL lekérdezésekbe, majd azt küldje át az adatbázisnak, így csak az eredmény jusson el hozzánk. Ezt úgy érhetjük el, hogy ha Func típusú delegate-ek helyett kifejezés fákat (expression tree) alkalmazunk.

Látható, hogy nem sima Func-ként, hanem Expression<Func…>-ként deklaráltam a változót. Így nem készül IL kód belőle fordítás időben, csak futásidőben, így ki lehet értékelni, ahogy csak szükséges. Természetesen, ezt a LINQ to SQL elvégzi helyettünk. Az alábbi ábrán látható, hogy a Where bővítő metódus itt most már Expression<Func<Product,bool>> típusú predikátumot vár.

Fontos megjegyezni, hogy az Expression-ként deklarált kifejezést nem lehet közvetlenül futtatni, ellentétben a Func típusú delegate-ként deklarálttal.

Nem szabad megijedni a lambdakifejezések használatától. Valójában egy nagyon jól használható, egyszerű dologról van szó, amit csak meg kell ismerni, rászánni egy órát és magunkévá tenni. Természetesen el lehet bonyolítani a végtelenségig, így akárcsak a többi újdonságot, ezt is használjuk óvatosan, hogy a kényelem ne menjen az olvashatóság, érthetőség kárára.

Kategóriák:Uncategorized