Tartalmi kivonat
Entity Framework és LINQ A C# 3.0 nyelv újdonságai Útmutató Változat: 0.8 GREPTON RT. 2006-2007 . Tartalomjegyzék 1. Bevezetés 3 1.1 A C# nyelv története dióhéjban 3 1.2 A C# 30 új elemei 4 2. Automatikus tulajdonságok [Automatic properties] 5 2.1 Az automatikus tulajdonságok szintaktikája 6 2.2 Amit a fordítóprogram végez 6 2.3 A tulajdonság és elérése 7 2.4 Egyebek 8 3. Objektumok és konténerek inicializálása 8 3.1 Egyszerű objektumok inicializálása 9 3.2 Tömbök és konténerek inicializálása 10 3.3 Összetett típusok inicializálása 11 3.4 Névtelen típusok inicializálása 12 3.5 Mitől konténer a konténer? 12 3.51 Az ICollection<T> interfész használata 12 3.52 Mi lesz veled jó öreg ArrayList? 13 3.53 Az Add metódus 13 4. Lokális típusfeloldás [local type inference] 15 4.1 Csökkentsük a zajt! 15 4.2 Névtelen típusok inicializálása: 2 felvonás 16 5. Bővítő metódusok [Extension methods] 17 5.1 A
bővítő metódusok definiálása 18 5.2 Hogyan fordulnak le a bővítő metódusok? 19 5.3 Bővítő metódusok és generikus típusok 20 5.4 Óvatosan a bővítéssel 20 6. Lambda kifejezések [Lambda expressions] 21 6.1 Programozási paradigmák 21 6.2 Az a bizonyos lambda 22 6.21 Mit jelent ez? 23 6.22 Még egy lépés: a funkcionális programozás 23 6.3 Lambda kifejezés és függvényreferencia 23 6.4 Ahogyan a fordítóprogram reagál 26 6.5 Többváltozós lambda kifejezések 26 6.6 Lambda kifejezések újrahasznosítása 28 6.7 Explicit típusos lambda kifejezések 28 6.8 Összetett lambda kifejezések 29 6.9 Egyebek 30 7. Összegzés 30 Változat: 0.8 (6) 2/30 1. Bevezetés A .NET 35 (vagy más néven NET „Orcas”) változatának egyik legfontosabb (és talán leghangoztatottabb) újdonsága a LINQ (Language Integrated Query), amelyet magyarra a „nyelvbe ágyazott lekérdezések” kifejezéssel tudunk talán a legjobban visszaadni. A LINQ, mint
képesség megvalósítása két fontos dolgot jelent a .NET 35-ben: A C# és VB.NET fordítóprogramjai új szintaktikai elemeket kezelnek, amelyek a LINQ lekérdezéseket a fordításidőben szintaktikailag és szemantikailag ellenőrzik, előkészítik azok futásidejű végrehajtását. A .NET alapobjektumkönyvtára olyan típusokkal bővült ki, amelyek futásidőben megvalósítják a LINQ szemantikáját, vagyis végrehajtják a lekérdezéseket. Ahhoz, hogy részletesen beszélhessünk a LINQ-ről mind a nyelvi elemeket, mind pedig az alapobjektumok könyvtárának bővítésével érdemes megismerkedni. Ebben a cikkben azokat a változásokat tekintem át, amelyek a C# nyelv szintaktikáját érintik és megalapozzák azokat a nyelvi képességeket, amelyek a LINQ-hez szükségesek. Mivel saját magam igen ritkán használom a VBNETet, így a VBNET nyelvi változásait nem fogom részletesen tárgyalni, biztos lesz olyan vállalkozó szellemű Olvasó, aki ezt
helyettem megteszi. A cikkből egyetlen C# nyelvi újdonság részletes tárgyalását fogom kihagyni: ez a C#-ba ágyazott nyelvi lekérdezések, vagyis a C# LINQ szintaktikája lesz. Mielőtt megrökönyödnél kedves Olvasó, el kell árulnom, hogy azért „becsempészem” a LINQ szintaktikáját ebbe a cikkbe, de a részletes leírását egy következő alkalomra tartogatom, amikor a LINQ felépítését és működését is bemutatom. 1.1 A C# nyelv története dióhéjban A C# programozási nyelv 1.0-ás változatát a Microsoft hivatalosan 2000 június 26-án jelentette be, ekkor a hozzátartozó referenciakönyvet [C# Language Reference] is publikálta. Akik ekkor megismerkedtek a nyelvvel, felfedezhették a abban Java, a C++ és a Delphi (Object Pascal) hatásait. Ez nem véletlen. A nyelv tervezője, Anders Hejlsberg , aki 1990-es évek elején a Turbo Pascal nyelv kialakítója, majd 1995-től a Delphi környezet megalkotója volt. Hejlsbergnek a Microsoftnál (1996ban
váltott) első munkának egyike a J++ nyelv kialakítása volt, amelyet a C# követett Bár a C# erősen a .NET-hez kötődik (és jelen pillanatban is a NET keretrendszerhez kapcsolódik az implementációja), a Microsoft a nyelvet szabványosításra beadta az ECMA (European Computer Manufacturesr Association) és az ISO (International Organization for Standardization) szabványügyi szervezeteknek egyaránt. Az ECMA 2001 decemberében, az ISO 2003 márciusában fogadta el a Microsoft szabványosítási kérelmét. A C# szintaktikájának jelentős részét a C++-tól örökölte, de jelentő szintaktikai egyszerűsítésen ment keresztül, főleg a C++-ban használt szimbólumok, operátorok és deklaratív követelmények tekintetében. Hejlsberg a nyelv megalkotásakor legfontosabb célként azt tűzte ki, hogy egyszerű, a kor kihívásainak megfelelő általános célú az objektumorientált programozási paradigmát támogató nyelvet alakítson ki ezt a célját a nyelvvel
elérte. A .NET keretrendszer megjelenése a C#-ot hamarosan az egyik leggyorsabban ismertté váló nyelvvé tette, köszönhetően annak is, hogy a C#-hoz a „.NET valódi nyelve” címke hozzáragadt 1 A nyelv mögötti közösségnek köszönhetően maga a nyelv is fejlődésnek indult. 1 Függetlenül attól, hogy volt-e komoly marketing hatás a háttérben, a szakma és a .NET közösség jelentős része ezt a címkét fel is vállalja. Mint a VB korábbi változatával hosszú ideig, majd azóta leggyakrabban C#-pal dolgozó szoftver mérnök, ezt a címkét én személyesen is felvállalom. Változat: 0.8 (6) 3/30 Nem sokkal az ISO szabványosítási folyamat után, már 2003. júliusában megjelent a C# 20 első béta változata, amely 2005. novemberében a Visual Studio 2005-ttel és a NET Framework 20-val egyidőben jelent meg. A C# 20 legjelentősebb újdonsága az ún generics képesség, vagyis a generikus típusok definiálásának és használatának
képessége volt; ez a nyelv egyéb újításait (nullable types, iterátorok támogatása az ún. yield konstruktcióval, illetve az ún anonymous delegates, stb.) súlyánál fogva szinte elfedte A C# 2.0 nem egyszerűen egy önálló nyelvi fejlődés eredménye volt: a generics képessége nem magába a programozási nyelvbe, hanem a futtatókörnyezetbe került be! A generikus típusok használatát tulajdonképpen a .NET CLR támogatja, a C# 20 ennek a támogatásnak a nyelvi „kivezetése” 2. Ahogyan az a C# 1.0 kapcsán történt, a C# 20 kibocsátása közben elindult a C# 30 változatának fejlesztése, jelenleg az Orcas Beta 1 változatnál járunk. A C# 3.0 fejlődési iránya nem a CLR változásainak kiszolgálásához kapcsolódik: az Orcas időkeretében a .NET CLR nem kerül megváltoztatásra A C# 30 kialakulását két fontos hatás határozza meg: Igény azokra a nyelvi képességekre, amelyek a C# alapvető imperatív (utasításalapú)
megközelítésmódját a funkcionális programozási paradigma konstrukcióival erősítik. A LINQ koncepciója, vagyis a nyelvbe ágyazott lekérdezések beépítése a C#-ba. Ez a két hatás közösen nyelvi újításokat eredményezett. Ezek egy része a nyelv szintaktikájának, használatának további egyszerűsödéséhez (tisztulásához vezetett), másfelől új szintaktikai elemeket vezetett be. 1.2 A C# 30 új elemei A LINQ és a funkcionális programozás hatása az alábbi új elemeket hozta 3 a C#-ba: A LINQ szintaktikai elemei [LINQ syntax]: A nyelv részévé válnak olyan szintaktikai elemek, amelyek segítségével az SQL nyelv select.fromwhere szintaktikájához hasonló módon a nyelvbe ágyazva végezhetünk lekérdezéseket. Lokális típusfeloldás [local type inference]: a fordítóprogram a környezet alapján kikövetkezteti, hogy milyen típust is használunk (szintaktikai egyszerűsítés, illetve lehetővé teszi az ún. névtelen
típusok [anonymous types] használatát) Objektumok és konténerek inicializálása [Object and collection initializers]: Az inicializáláshoz használt kód (inicializálás konstruktorok és közvetlen mezőértékek, és tulajdonságértékek beállításával) szintaktikai „megtisztítása”, olvashatóbbá, tömörebbé tétele. Névtelen típusok [anonymous types]: Egyszerűen létre tudunk hozni olyan lokális típusokat, amelyek a típusdeklaráció és típuspéldány inicializálás „fárasztó”, illetve „szószátyár” szintakszisától mentesítenek bennünket. Bővítő metódusok [Extensions methods]: Egy új, könnyen használható jelölés statikus metódusok hívására, a „szintaktikai zaj csökkentésére”. Lambda kifejezések [Lambda expressions]: A funkcionális programozás egyik alapelemének (lásd lambda calculus) megjelenése a C#-ban, a LINQ képességek egyik legfontosabb alapeleme. Automatikus tulajdonságok
[Automatic properties]: Objektumtípusokhoz tartozó tulajdonságok egyszerűsített definiálása. A cikk további fejezeteiben A LINQ szintaktikai elemeinek kivételével az összes nyelvi újítást részletesen bemutatom. Nem egyszerűen referenciakönyv jellegű leírást szeretnék adni, hanem azt is szeretném bemutatni, hogyan kapcsolódnak az újítások a .NET CLR-hez, hogyan kezeli ezeket a szintaktikai elemeket a C# fordítóprogram. Kedves olvasó, feltételezem, hogy ismered a C# nyelvet és 2 3 A .NET CLR képességét a VBNET is támogatja Mivel ezek az Orcas Beta 1 állapotában vannak, természetesen változhatnak. Nem valószinű, hogy szűkülnének Változat: 0.8 (6) 4/30 tisztában vagy a C# 2.0 (illetve a NET CLR 20) generics képességével is, s nem jelent számodra komoly problémát az MSIL kód alapelemeinek megértése. A C# új szintaktikai tulajdonságainak vizsgálatánál a .NET keretrendszer részét jelentő ILDASM segédprogramot és a
Reflector eszközt fogom felhasználni. Ez utóbbi letölthető az alábbi címről: http://www.aistocom/roeder/dotnet 2. Automatikus tulajdonságok [Automatic properties] Az OOP alapelvek és a javasolt .NET implementációs minták közé tartozik az a gyakorlat, hogy típuspéldányok jellemzőihez kívülről alapvetően tulajdonságokon és nem mezőkön keresztül férünk hozzá. Ha például egy kontaktszemély adatait használjuk, röviden le tudjuk írni az alábbi programrészletet: public class Contact { public int Id; public string Name; public DateTime? BirthDate; public readonly int? Age; } 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: Publikus mezőket használunk a kontaktszemély jellemzőinek elérésére.
Az Age mező readonly jellege miatt azt csak konstruktorban tudjuk beállítani. Nekünk arra volna szükségünk, hogy a BirthDate változásával együtt az Age jellemző értéke is ennek megfelelően változzon. A mezők helyett tulajdonságokat bevezetve már meg tudunk felelni az elvárásoknak: public class Contact { int Id; string Name; DateTime? BirthDate; public int Id { get { return Id; } set { Id = value; } } public string Name { get { return Name; } set { Name = value; } } public DateTime? BirthDate { get { return BirthDate; } set { BirthDate = value; } } Változat: 0.8 (6) 5/30 } public int? Age { get { return BirthDate.HasValue ? (int?)(DateTime.NowYear - BirthDateValueYear) : null; } } A tulajdonságokhoz (az Age 4 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. 2.1 Az automatikus tulajdonságok szintaktikája 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: public class Contact { public int Id { get; set; } public string Name { get; set; } public DateTime? BirthDate { get; set; } } public int? Age { get { return BirthDate.HasValue ? (int?)(DateTime.NowYear -
BirthDateValueYear) : null; } } 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. 2.2 Amit a fordítóprogram végez A mezők automatikus létrehozásáról könnyen meggyőződhetünk, ha lefordítjuk a kódot és az ILDASM program vagy akár a Reflector segítségével belenézünk a Contact osztály kódjába: .class public auto ansi beforefieldinit Contact 4 Az Age számítása természetesen a fenti módszerrel nem pontos, de a demonstrációs célnak megfelelően rövid. Változat: 0.8 (6) 6/30 { extends [mscorlib]System.Object .method public hidebysig specialname rtspecialname instance void ctor() cil managed {} // A Name és BirthDate és Age tulajdonságokat elhagytam. .property instance int32 Id { .get instance int32 CSharp3NewsContact::get Id() .set instance void CSharp3NewsContact::set Id(int32) } // Ez
az Id tulajdonsághoz generált mező! .field private int32 <>k AutomaticallyGeneratedPropertyField0 { .custom instance void [mscorlib]System.RuntimeCompilerServicesCompilerGeneratedAttribute::ctor() } } // A Name és BirthDate tulajdonságokhoz tartozó mező elhagytam. A fenti programkódban csak az Id tulajdonságot és a hozzátartozó, a fordítóprogram által létrehozott mezőt tüntettem fel, a többi tualjdonságot és mezőt elhagytam a könnyebb olvashatóság kedvéért. A kódról az alábbi érdekes dolgokat olvashatjuk ki: Az Id mezőhoz a fordítóprogram a <>k AutomaticallyGeneratedPropertyField0 nevű mezőt generálta (a többi mező neve is hasonló, a mező nevének végén szereplő index egyesével növekszik). Az Id mező get és set elérési pontjai valamint a hozzá tartozó generált mező mindegyikét a fordítóprogram ellátta a CompilerGenerated attribútummal. A Reflector programmal a Contact osztály vázát megjelenítve
ezt szintén láthatjuk: public class Contact { // Fields [CompilerGenerated] private int <>k AutomaticallyGeneratedPropertyField0; [CompilerGenerated] private string <>k AutomaticallyGeneratedPropertyField1; [CompilerGenerated] private DateTime? <>k AutomaticallyGeneratedPropertyField2; // Methods public Contact(); } // Properties public int? Age { get; } public DateTime? BirthDate { [CompilerGenerated] get; [CompilerGenerated] set; } public int Id { [CompilerGenerated] get; [CompilerGenerated] set; } public string Name { [CompilerGenerated] get; [CompilerGenerated] set; } 2.3 A tulajdonság és elérése 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: Változat: 0.8 (6) 7/30 public int ReadOnlyNumber { get; private set; } public string WriteOnlyName { private get; set; } Értelemszerűen a ReadOnlyNumber
egy csak olvasható, a WriteOnlyName egy csak írható tualjdonsá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: public int ReadOnlyNumber { protected get; private set; } // Hibás 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: private int ReadOnlyNumber { get; protected set; } // Hibás 2.4 Egyebek Ha a reflection alkalmazásával szeretnénk feltérképezni az automatikusan létrehozott tulajdonságokat, akkor abban a get és set elérési pontokhoz tartozó CompilerGenerated attribútum vizsgálata nyújthat segítséget. Ha az elérési pontokhoz nem létezik ilyen, akkor biztosan nem egy automatikusan generált tulajdonságról van szó. Ha létezik, akkor valószínűleg automatikus tulajdonságról (elvileg a CompilerGenerated attribútumot kézzel is be lehet írni). Az automatikus tulajdonságok a C# 3.0 legutolsó hivatalos
referenciakönyvében (2006 május) még nem szerepelnek. A Microsoft által a különböző rendezvényeken publikált névhez képest az Orcas Beta1 dokumentációjában már „automatikusan létrehozott tulajdonságok” [auto-implemented properties] néven kezeli ezt a nyelvi képességet. 3. Objektumok és konténerek inicializálása 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. Hogy miért is mondom ezt? 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: public class Person { public string Name { get; set; } public string NickName { get; set; } public string Email { get; set; } public bool KeepContact { get; set; } } Változat: 0.8 (6) 8/30 A Person osztály tulajdonságait automatikus tulajdonságként hoztuk létre. Ha szeretnénk a Person osztály egy példányát inicializálni, azt pl. az
alábbi módon tehetnénk meg: Person newPerson = new Person(); newPerson.Name = "Gipsz Jakab"; newPerson.NickName = "James"; newPerson.KeepContact = true; 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ú! 3.1 Egyszerű objektumok inicializálása 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: Person newPerson = new Person() { Name="Gipsz Jakab", NickName="James", KeepContact=true }; A Person alapértelmezett konstruktorrára való hivatkozásánál akár a „()”-t is elhagyhatjuk. A kapcsos zárójelben írhatjuk le, hogy az objektum melyik tulajdonságait milyen kezdeti értékekkel szeretnénk ellátni. Ezt a leírást a fordítóprogram egy olyan változatra alakítja át, amely a egyenként beállítja a tulajdonságokat a felsorolt értékekre. Erről meggyőződhetünk, ha a Reflector eszközzel megvizsgáljuk, hogy milyen MSIL kódot bocsátott ki a fordító, illetve azt rögtön vissza is fordítjuk C# nyelvre: Person <>g initLocal8 = new Person(); <>g initLocal8.Name = "Gipsz Jakab"; <>g initLocal8.NickName = "James"; <>g initLocal8.KeepContact = true; Person newPerson = <>g initLocal8; Amint láthatjuk, a fordítóprogram létrehozott egy lokális
változót (a „szép” <>g initLocal8 néven), hozzárendelt egy új Person példányt, amely tulajdonságait feltöltötte, majd ezt az osztály referenciát hozzárendelte a newPerson saját lokális változónkhoz. Ha a Person nem referencia típusú változó volna, a fordítóprogram akkor is ugyanígy járna el: az utolsó lépésben a newPerson változóba nem referenciát, hanem az ideiglenes lokális változó értékét másolná be. 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 5. 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: 5 A javasolt tervezési minta: ahol lehet, a tulajdonságokat tegyük ortogonálissá. Változat: 0.8 (6) 9/30 public Person(string name) { Name = name; } public Person(string name, bool keepContact) { Name =name; KeepContact = keepContact; } Ekkor az alábbi módok mindegyike használható a kezdeti érték beállításához: Person newPerson1 = new Person("Gipsz Jakab") { Name = "Gipsz Jakab", NickName = "James", KeepContact = true }; Person newPerson2 = new Person("Gipsz Jakab") { NickName = "James", KeepContact = true }; Person newPerson3 = new Person("Gipsz Jakab", true) { NickName = "James" }; A newPerson1 létrehozásánál a Name tulajdonságot kétszeresen (feleslegesen) is inicializáltuk:egyszer ezt a konstruktor tette meg, másodszor az inicializáló lista. Ez szintaktikailag és szemantikailag is helyes, még ha nem
feltétlenül célszerű is. 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: Person newPerson4 = new Person("Vég Béla") { KeepContact = !newPerson.KeepContact }; 3.2 Tömbök és konténerek inicializálása 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: int[] primeNumbers = new int[5]; primeNumbers[0] = 2; primeNumbers[1] = 3; primeNumbers[2] = 5; primeNumbers[3] = 7; primeNumbers[4] = 11; Az előző példa tükrében szinte biztos, hogy az olvasó maga is kitalálja, milyen új szintaktikai jelöléssel egyszerűsíti az inicializálást a C# 3.0: int[] primeNumbers = { 2, 3, 5, 7, 11 }; Nos, ezen a ponton a kedves olvasót jól megtévesztettem: ez a szintakszis nem a C# 3.0 újdonsága, a tömbök inicializálásának ezt
a módját már a C# 1.0-ban is megtalálhatjuk Gyakran azonban a tömbök helyett .NET gyűjteményeket, listákat, illetve egyéb konténereket szeretnénk használni, például az alábbi módon: List<int> primeNumbers = new List<int>(); primeNumbers.Add(2); primeNumbers.Add(3); primeNumbers.Add(5); primeNumbers.Add(7); primeNumbers.Add(11); Ezt rövidebben így is leírhatjuk: List<int> primeNumbers = new List<int>(); primeNumbers.AddRange(new int[] { 2, 3, 5, 7, 11 }); Változat: 0.8 (6) 10/30 A legtömörebb leírást azonban a C# 3.0 biztosítja 6: List<int> primeNumbers = new List<int> { 2, 3, 5, 7, 11 }; A fordítóprogram a háttérben az inicializálást átfordítja úgy, hogy a List<int> generikus típus Add metódusát használva hozza létre a kérésnek megfelelően inicializált listapéldányt. Erről a Reflector segítségével meggyőződhetünk visszafejtve a fenti kifejezés hatására generált kódot:
List<int> <>g initLocal9 = new List<int>(); <>g initLocal9.Add(2); <>g initLocal9.Add(3); <>g initLocal9.Add(5); <>g initLocal9.Add(7); <>g initLocal9.Add(11); List<int> primes = <>g initLocal9; 3.3 Összetett típusok inicializálása 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: public class Employee: Person { public Address Address { get; set; } public List<int> NicePrimes { get; set; } } public class Address { public string Zip { get; set; } public string City { get; set; } public string Street { get; set; } } 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: Employee newEmp = new Employee { Name = "Ragta Pasz", NickName = "Cellux", Address = new Address { Zip = "1116", City = "Budapest", Street = "Tölgy utca" }, NicePrimes = new List<int> { 11, 13, 17, 19 } }; 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. 6 Ezt még tovább fogjuk „zajtalanítani” a lokális típusfeloldás bevezetésével. Változat: 0.8 (6) 11/30 3.4 Névtelen típusok inicializálása 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 (lásd később). Az előző részben bemutatott Employee típus newEmp példányát egy névtelen típusból is létre hozhatjuk: object newEmp = new { Name = "Ragta Pasz", NickName = "Cellux", Address = new { Zip = "1116", City = "Budapest", Street = "Tölgy utca" }, NicePrimes = new List<int> { 11, 13, 17, 19 } }; 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. 3.5 Mitől konténer a konténer? Korábban a listák inicializálásánál említettem, hogy a fordítóprogram az Add metódusokat használja
fel (fordítja be) amikor a kezdeti listapéldányt létrehozza. Na jó, dehát honnan tudja a fordítóprogram hogy egy adott típus inicializálásánál használhatja az Add metódust? Ebben a részben ezt fogom részletesebben megvizsgálni. 3.51 Az ICollection<T> interfész használata A C# 3.0 egyik tavalyi CTP-je azoknak a gyűjteményeknek az inicializálását segítette az új, egyszerűsített szintakszissal, amelyek megvalósították az ICollection<T> interfészt. Ennek az interfésznek van egy Add(T item) szignatúrájú metódusa, a fordítóprogram ezeknek a hívását fordította be a kódba. Ez azt jelenti, hogy minden (akár a NET objektumkönyvtárában lévő, akár saját) típus esetében használhatjuk a C# 3.0 új szintakszisát, amely ICollection<T> „kompatibilis” Nézzük meg ezt egy példán: using System; using System.Collections; using System.CollectionsGeneric; namespace CSharp3News { public class SimpleCollection<T>:
ICollection<T> { private List<T> List = new List<T>(); public void Add(T item) { List.Add(item); } public void Clear() { List.Clear(); } public bool Contains(T item) { return List.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { CopyTo(array, arrayIndex); } public int Count { get { return List.Count; } } public bool IsReadOnly { get { return false; } } public bool Remove(T item) { return List.Remove(item); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return ListGetEnumerator(); IEnumerator IEnumerable.GetEnumerator() { return ListGetEnumerator(); } Változat: 0.8 (6) } 12/30 } } Teszteljük a fordítóprogramot, vajon mit szól az alábbi C# 3.0 utasításhoz? SimpleCollection<string> cars = new SimpleCollection<string> { "Trabant", "Wartburg", "Dacia", "Lada", "Zaporozsec" }; Kellemes meg(nem)lepetéséként érhet bennünket, hogy ez bizony ahogyan
vártuk sikeresen lefordul. A kellemes érzés mellett azért a kisördög folyamatosan rágja a fülünket: „megéri ezért 9 metódust is implementálni, amikor nekünk csak az Add-ra van szükségünk?” 3.52 Mi lesz veled jó öreg ArrayList? A .NET 20 bizony adott egy „gyomrost” az ArrayList osztálynak azzal, hogy megjelent benne a CLR szinten támogatott generikus típuskezelés, háttérbe szorítva a „hagyományos” gyűjteményeket, konténereket. A NET 20-val dolgozó fejlesztők gyakran hanyagolják el az ArrayList osztályt és helyette a List<T> generikus listát használják. A C# 30 tavaly (2006) CTP-je újabb ütést mért az ArrayList-re: a konténerek inicailizálása során az ArrayList a nem támogatott típusok közé tartozott. Ennek oka az volt, hogy az ArrayList amellett, hogy nem generikus osztály még csak nem is valósította meg az ICollection<T> interfészt, amely követelmény volt a gyűjtemények újszerű inicializálásához.
Szerencsére, a C# fejlesztői felismerték ezt a tényt és átgondolták, hogy mit is tekintenek olyan gyűjteménynek, amely a C# 3.0 szintakszisát használhatják kezdeti értékük beállítására Jelenleg a C# 3.0 gyűjteménynek tekint minden olyan típust, amely megvalósítja az IEnumerable (figyelem, ez nem ugyanaz, mint az IEnumerable<T> generikus interfész!) interfészt és rendelkezik olyan Add nevű metódussal, amelynek egyetlen bemenő paramétere van (a visszatérési értékének a típusa érdektelen). Az ArrayList is egy ilyen típus, így az Orcas Beta 1 verziójában a kezdeti érték beállítása kapcsán már újra egyenrangú lett a generikus gyűjteményekkel. Aki nem hiszi, próbálja ki, az alábbi kód fordul: ArrayList cars = new ArrayList { "Zsiguli", "Polski Fiat", "Volga", "Skoda", "Aro" }; 3.53 Az Add metódus Az IEnumerable interfész megvalósítása és egy Add metódus definiálása még
nem old fel minden problémát. Néhány kérdést éredemes még egy kicsit pontosabban körüljárnunk: Ha saját gyűjteményt készítünk, mit kell tudnia az Add metódusnak? Mi történik, ha több Add metódusunk is van? Jelenleg ezekre a kérdésekre még nincs hivatalos válasz, ezért a válaszok megtalálásához készítettem egy nagyon egyszerű osztályt, amely a jó öreg ArrayList-et használja: public class SimpleEnumerable: IEnumerable { private ArrayList List = new ArrayList(); Változat: 0.8 (6) 13/30 } public SimpleEnumerable() { } public SimpleEnumerable(ArrayList initialList) { List = initialList; } public void Add(object item) { List.Add(item); } IEnumerator IEnumerable.GetEnumerator() { return ListGetEnumerator(); } Az Add metódus object példányt fogad bemenetként, így gyakorlatilag az inicializálás során bármilyen objektumot megadhatunk a listában: SimpleEnumerable objects = new SimpleEnumerable { 1, 2, "három",
"four", 5, 6, "sieben" }; Leellenőrzihetjük, ezt bizony elfogadja a fordítóprogram! Azt, hogy az Add metódusra tényleg szükség van kipróbálhatjuk: kommentezzük ki a SimpleEnumerable osztály kódjából az Add metódust és próbáljuk lefordítani a programot! Egy hibalistát kapunk, amelyben pontosan hétszer fog szerepelni az alábbi üzenet: „’SimpleEnumerable’ does not contain a definition for ’Add’” Az inicializálási lista minden egyes tagjára megkapjuk ezt az üzenetet 7. „Csempésszük” vissza az Add metódus eredeti kódját a programba, majd írjuk azt át az alábbi módon: public void Add(int item) { List.Add(item); } Ha most próbálkozunk a fordítással, 6 hibát (3 pár) kapunk az „három”, „four” és „sieben” értékekre, azok ugyanis nem konvertálhatók int típussá. Feszítsük tovább a húrt, és adjunk két Add metódust a SimpleEnumerable osztályhoz: public void Add(int item) { List.Add(item); }
public void Add(string item) { List.Add(item); } Ugye nem meglepő: hiba nélkül lefordul a programunk. A kezdeti értékeket tartalmazó listára csempésszünk be egy long számot, pl. a „2”- át „2L”-re! A fordítás most nem működik, hiszen egy long számot nem tudunk veszteség nélkül int-re konvertálni. Továbbfolytatva a kísérletezést (implicit konverziók kipróbálása), választ kaphatunk az Add metódus felhasználásának a módjára: A fordítóprogram az inicializálási lista minden eleméhez egyesével megkeresi a legjobban illeszkedő Add metódust és annak a hívását fordítja be a kódba. Ha van olyan Add metódus, amely bemenő paramétertípusa megegyezik az inicializálási lista adott elemének típusával ez lesz a legjobban illeszkedő. Ha nincs ilyen, de van olyan típusú bemenő paraméterrel reldelkező Add metódus, amely típusára a listaelem implicit konverzióval átalakítható, ez a metódus lesz felhasználva. Ha
nincsmegfelelő Add metódus, hibát kapunk. Én magam még nem próbáltam ki, de akit érdekel próbálja ki, hogy implicit típuskonverziós operátort készítve hogyan működik az Add metódus, az eredményre magam is kíváncsi vagyok! 7 Az Orcas végleges változatában talán már csak egy üzenetet fogunk kapni. Változat: 0.8 (6) 14/30 4. Lokális típusfeloldás [local type inference] A C# 3.0 egyik fontos célkitűzése volt, hogy egyszerűsítse a nyelv szintaktikáját, mind a programok íróját, mind az olvasóját olyan szintaktikai konstrukciókkal támogassa, amelyek segítik a kód készítését, értelmezését. Az objektumtípusok deklarálása és inicializálás olyan „szintaktikai zajt” visz a programozásba, amely felesleges lehet. Tegyük fel például, hogy ügyfélcsoportokat helyezünk egy a memóriában lévő gyorsítótárba és azokat a csoportnév alapján szeretnénk címezni. Ezt a System.CollectionsGeneric névtér Dictionary
osztályával egyszerűen megtehetjük A gyorsítótár létrehozása valahogy így nézhet ki a programban: Dictionary<string, Dictionary<int, Customer>> cache = new Dictionary<string, Dictionary<int, Customer>> 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: Dictionary<string, Dictionary<int, Customer>> otherRef = cache; Dictionary<int, Customer> partners = otherRef["partners"]; A „szintaktikai zajt” ebben a programrészletben a típusdeklarációk jelentik: az értékedások jobboldala eleve meghatározza, hogy a baloldalon szereplő változók típusa milyen lehet, azt mégis kiírjuk. 4.1 Csökkentsük a zajt! 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: var cache = new Dictionary<string, Dictionary<int, Customer>> // . var otherRef = cache; var partners = otherRef["partners"]; 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: public class LocalTypeInference { var cache = new Dictionary<string, Dictionary<int, Customer>>(); // Hibás } public void DoSomething() { var otherRef
= cache; var partners = cache["Partners"]; } Változat: 0.8 (6) 15/30 Ezzel szemben az alábbi kódrészletben már szerepelhet a típusfeloldás és a kód helyes is lesz: public class LocalTypeInference { public void DoSomething() { var cache = new Dictionary<string, Dictionary<int, Customer>>(); // OK. // . var otherRef = cache; var partners = cache["Partners"]; } } A Reflector segítségével megnézhetjük, hogy a DoSomething metódus milyen kód generálására „késztette” a fordítóprogramot: public void DoSomething() { Dictionary<string, Dictionary<int, Contact>> cache = new Dictionary<string, Dictionary<int, Contact>>(); // . Dictionary<string, Dictionary<int, Contact>> otherRef = cache; Dictionary<int, Contact> partners = cache["Partners"]; } A „visszafordított” kódból láthatjuk, hogy pontosan azt tette a fordítóprogram, amit vártunk tőle: az értékadások
jobboldalából kikövetkeztette a baloldalon lévő kifejezések típusát és annak megfelelő típusú lokális változókat hozott létre. 4.2 Névtelen típusok inicializálása: 2 felvonás 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: object newEmp = new { Name = "Ragta Pasz", NickName = "Cellux", Address = new { Zip = "1116", City = "Budapest", Street = "Tölgy utca" }, NicePrimes = new List<int> { 11, 13, 17, 19 } }; 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 newEmp példány deklarációban megadott
tulajdonságait fordításidőben nem érjük el, így nem is hivatkozhatunk pl. a newEmpName tulajdonságra, illetve az IntelliSense sem működik Sem közvetlen, sem közvetett módon nem tudunk hivatkozni a newEmp mögött lévő típusra. A lokális típusfeloldás gyógyírt jelenthet problémáink egy részére: var newEmp = new { Name = "Ragta Pasz", NickName = "Cellux", Változat: 0.8 (6) 16/30 Address = new { Zip = "1116", City = "Budapest", Street = "Tölgy utca" }, NicePrimes = new List<int> { 11, 13, 17, 19 } }; 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! var empClone = newEmp; var addr = empClone.Address; addr.City = "Debrecen"; 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. Mindezek mellett még az IntelliSense is működik: 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! Ez azonban már egy másik cikk témája. 5. Bővítő metódusok [Extension methods] A C# 3.0-ban az ún bővítő metódusokat alapvetően a LINQ hívta életre, alkalmazási területük sokkal bővebb ennél. A bővítő metódusokat formális definíciójuk helyett sokkal egyszerűbben lehet
egy-egy példán keresztül megvilágítani: Nekem nagyon tetszik az, hogy 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: string shortSentence = "Egy egy rövid mondat."; string changedSentence = shortSentence.Left(10)ToLower()SubString(5); Console.WriteLine(changedSentence); Változat: 0.8 (6) 17/30 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 8. Ezeket a metódusokat legegyszerűbben egy statikus osztály segítségével tudom megvalósítani: static class StringExtensions { public static
string Right(string source, int count) { . } public static string Reverse(string source) { . } public static string Stuff(string source, string stuffing) { . } } A metódusokat igen egyszerű használatba venni: string shortSentence = "Egy egy rövid mondat."; string changedSentence = StringExtensions.Reverse(StringExtensionsRight(shortSentence, 12)); Console.WriteLine(changedSentence); A kiemelt kifejezés nekem nem tetszik. 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: string changedSentence = shortSentence.Right(5)Reverse();
5.1 A bővítő metódusok definiálása 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 Helper statikus osztály definiálja az Extend publikus és statikus metódust «type» instanceOfType; // „Hagyományos” szintakszis «type» otherInstance = Helper.Extend(instanceOfType); // „Bővítő metódus szintakszis «type» otherInstance = instanceOfType.Extend(); 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. 8 Az olvasónak nyilván még ezer ötelete van, amely ilyen hasznos metódus lehet. Változat: 0.8 (6) 18/30 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: static class StringExtensions { public static string Right(this string source, int count) { . } public static string Reverse(this string source) { . } public static string Stuff(this string source, string stuffing) { . } } 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. 5.2 Hogyan fordulnak le a bővítő metódusok? A metódusok fordítása során a fordítóprogram a System.RuntimeCompilerServices névtérben található Extension attribútumot ragasztja a metódushoz és ahhoz az osztályhoz is, amelyben a metódus szerepel. Olyan, mintha az alábbi programot írtuk volna le: using System.RuntimeCompilerServices; // . [Extension] static class StringExtensions { [Extension] public static string Right(string source, int count) { . } [Extension] public static string Reverse(string source) { . } [Extension] public static string Stuff(string source, string stuffing) { . } } Ezt a programot azonban hiába is próbáljuk lefordítani: a fordítóprogram sehol nem engedi meg az Extension attribútum használatát, felszólít bennünket, hogy használjuk inkább a this kulcsszót. Változat: 0.8 (6) 19/30 5.3
Bővítő metódusok és generikus típusok 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: public static class EnumerableExtensions { public static IEnumerable<T> First<T>(this IEnumerable<T> source, int count) { var result = new List<T>(); int counter = 0; foreach (var item in source) { result.Add(item); if (++counter >= count) break; } return result; } } Az adott bővítő metódust az alábbi módon használhatjuk: var items = new List<string> { "egy", "kettő", "három", "négy", "öt", "hat" }; foreach (var item in items.First(3)) { Console.WriteLine(item); } var elements = new object[] { "egy", 2, "három", 4, "öt", 6 }; foreach (var element in elements.First(3)) {
Console.WriteLine(element); } 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. 5.4 Óvatosan a bővítéssel 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: public static void Dump(this object instance) { Console.WriteLine(instance == null ? "<NULL>" : instanceToString()); } 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 Változat: 0.8 (6) 20/30 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. 6. Lambda kifejezések [Lambda expressions] Nem véletlenül hagytam a C# 3.0 nyelvi újdonságaival foglalkozó cikksorozat végére a labda kifejezések tárgyalását: ez az újdonság már egy új paradigmát vezet be a C# nyelv eddig is alkalmazott megközelítési módjai közé: a funkcionális programozás elemeit. Több Microsoft eredetű blogban is komolyabb fejtegetéseket találtam arról, hogy a C# szemléletébe komolyan és erőteljesen belekerült a
funkcionális programozás. Nos, ez így ebben a formában talán túlzás, helyesebb azt mondani, hogy a lambda kifejezésekkel funkcionális programozási elem is került a C# eszközkészletébe. 6.1 Programozási paradigmák Kedves olvasó, egy kicsit még „éheztetlek”, mielőtt elárulom, hogy mi is az a lambda kifejezés. Ahhoz, hogy a témát a megfelelő helyre tegyük, engedd meg, hogy egy kicsit összeszedjem azt, amit a programozási paradikmákról gondolok. Ha ezt feleslegesnek tartod elolvasni, akkor a következő részben már „istenbizony” rátérek a lényegre. A programozási nyelvek a fejlődésük során nem egyszerűen szintaktikai képződményekként jönnek létre, hanem valamilyen programozási paradigma mentén alakítják ki őket kitalálóik. A programozási paradigma azt a nézetet (ez általában absztrakt nézet, de fizikai is lehet) fejezi ki, ahogyan a fejlesztő egy adott program futását definiálja, látja, illetve érzékeli. Egy adott
programozási nyelv általában több paradigmát is támogat. Az alábbiakban felsorolok és nagyon tömören bemutatoknéhány fontos, gyakran használt programozási paradigmát: Imperatív (utasításalapú) programozás: A legtöbb programozási nyelv alapvetően az utasításalapú paradigmára épül, amely lényege, hogy a program működési elemeit utasítások és vezérlési szerkezetek (ciklusok, feltételek, elágazások, stb.) formájában írjuk le, vagyis azt mondjuk meg, hogyan kell az adott működést megvalósítani. Deklaratív programozás: Amíg az imperatív programozás a „hogyan kell csinálni” leírásával definiálja a program működési elemeit, addig a deklaratív programozás a „mit kell csinálni” szemléletmódjával ragadja meg egy rendszer működési elemeit. A deklaratív programozási paradigma egyik letisztult koncepciója a funkcionális programozás, amelyet egy néhány bekezdéssel lejjebb tárgyalok. Az SQL nyelvnek az
adatok lekérdezéséhez és manipulásához tartozó része (DML) ennek a paradigmának egy tiszta megvalósítása. Hasonló (ám kevésbé tiszta) példa a deklaratív programozásra az XSLT nyelv, amelyet XML dokumentumok transzformációjára használunk. Deklaratív elemeket fedezhetünk fel a NET nyelvek (pl C#, VBNET) által használt attribútumokban is. Objektumorientált programozás: A paradigma a programot egymással együttműködő, saját felelősségi és szerepkörrel bíró objektumok hálózataként írja le. Az objektumok és működésük definiálása során olyan fontos technikákat (tulajdonképpen ezek önmagukban is paradigmák) használhatunk, mint az információ beágyazása (encapsulation), öröklődés (inheritance), polimorfikus viselkedés (polimorphism) és modularitás. Az első valódi objektumorientált nyelv az Alan Key által létrehozott SmallTalk volt, de ma gyakrabban találkozhatunk az Ada, C#, C++Delphi (Object Pascal) és VB.NET
nyelvekkel Aspektus-orientált programozás: A fejlesztés során a legtöbb működési elemet modulok, objektumok, komponensek felelősségi körébe tudjuk sorolni. Ugyanakkor vannak olyan működési elemek (concerns), amelyeket nem tudunk ilyen egyszerűen valamely objektum felelősségi Változat: 0.8 (6) 21/30 körébe tartozik. Ilyenek például a naplózás, teljesítménymérés, authentikáció, authorizáció, egyéb ún. „háztartási” metódusok, stb Az aspektus-orientált programozás azt célozza meg, hogy ezeket a modulok között átívelő működési elemeket (cross-cutting concerns) a programok jól meghatározott helyén (egy helyen, minimális átfedésekkel) definiálhassa. Az aspektus-orientált programozást általában nyelvi kiterjesztések kezelik, ilyenek pl. a Xerox PARC által 2001-ben közzétett AspectJ kiterjesztés a Java-hoz, illetve ennek open source változatából kinőtt AspectC#. Funkcionális programozás: A funkcionális
programozási paradigma a deklaratív paradigma egy letisztult változata, vagyis egy rendszer működési elemeit a „mit kell csinálni” szemléletmóddal ragadja meg. Az utasításalapú programozással szemben, amelyben egy program állapotinformációkat kezel és változtat, a funkcionális programozás matematikai függvények használatával elkerüli azt, hogy állapotokat kelljen kezelnie vagy változtatnia. A legelterjedtebb funkcionális nyelvek közé tartozik a Haskell, a Matematica és az ML. A funkcionális programozást leggyakrabban olyan területen alkalmazzuk, amely komoly matematikai apparátus használatát teszi szükségessé (pl. numerikus analízis) 6.2 Az a bizonyos lambda Talán furcsának tűnik, de a lambda kifejezések fogalma a LISP-ben (a második legrégebbi programozási nyelv a FORTRAN után!) jelent meg. A fogalom a Church és Kleene által a 30-as években bevezetett lambda kalkulus fogalmán alapszik. Mivel én magam nem vagyok matematikus
és ezért nem is érzem magam rátermettnek, hogy bevezessem az olvasót ebbe az apparátusba, ezért itt csak a kalkulus alapvető elvét igyekszem bemutatni, kerülve annak formális leírását: Church és Kleene rámutatott, hogy a számítógépes nyelvekben megfogalmazható kifejezések mindegyike leírható az ún. lambda kalkulussal, azaz minden kifejezés helyettesíthető egy olyan függvénnyel, amely egyetlen bemenő paramétert fogad. Ennek a függvénynek az egyetlen bemenő paramétere és az értéke is egy olyan függvény, amely szintén egyetlen bemenő paramétert fogad. Erre a függvényre , annak bemenő paraméterére és értékére ezt a fajta definiíció a végtelenségig alkalmazhatjuk rekurzívan. Például az f(x) = x + 1 függvény a lambda kalkulussal az alábbi módon írhatjuk le: lambda<x:x+1>. Ha ezt a függvényt szeretnénk a 2 argumentumra alkalmazni, azt így írjuk le: lambda<x:x+1>(2). Magát a 2-t is leírhatjuk lambda
kalkulussal: lambda<n:n>(2) A kalkulus segítségével az alábbi műveletek mind egyenértékűek: lambda<x:x+1>(lambda<n:n>(2)) lambda<x:x+1>(2) 2 + 1 A lambda kalkulus segítségével többváltozós (több argumentummal rendelkező) függvényeket is le tudonk írni. Például az f(x,y) = x*y függvényt az alábbi módon írhatjuk le: lambda<x:lambda<y:x*y>> a 3*4 értéket ennek megfelelően az alábbi kifejezés jelölheti: lambda<x:lambda<y:x*y>(4)>(3) Anélkül, hogy további elemzésekbe, illetve a konstrukció bemutatásába belemennék, szeretném bemutatni, hogy mi ennek az egésznek a jelentőssége a programozás szempontjából, illetve hogyan is kerülünk ezzel közelebb a funkcionális programozáshoz valamint a C# 3.0-hoz Változat: 0.8 (6) 22/30 6.21 Mit jelent ez? A lambda kalkulus jelentősségét az adja, hogy nem kevesebbet mond, mint azt, hogy minden olyan kefejezés (figyelem, nem művelet!), amit a
programozási nyelvekben használunk, leírható „egyszerű” függvények alkalmazásával. Itt az „egyszerű” olyan függvényt jelent, amelynek nincsen mellékhatása (azaz programozási nyelvek irányából megfogva: csak lokális, a függvény belsejében lévő változókat módosít, a függvény külső környezetét pl. az abban lévő változókat nem módosítja) Ez az állítás azért hasznos, mert az egyszerű függvények hálózata azt a nagyszerű lehetőséget biztosítja, hogy egy adott kifejezés végeredményéig elvileg több úton is eljuthatunk, a kiértékelés során akár párhuzamosan több ágon is haladhatunk egyidejűleg. Mi következik ebből? A függvények hálózatának ismeretében a párhuzamosíthatóság maximális fokát is meg tudjuk mondani. Ha ismerjük, hogy az egyes függvények kiértékelése milyen költséggel és idővel jár, meg tudjuk mondani a kiértékelés minimális idejét, illetve költségét. Nézzünk egy
példát, értékeljük ki az alábbi kifejezést (nem alakítom át lambda kifejezéssé): f(x, y, z) = sin((x + cos(y)) * (x + z)) + sqrt(x x + y / z – y x) A kifejezésben a sin és sqrt függvények argumentumait párhuzamosan számíthatom ki, mielőtt a függvényértékeket összeadnám. A sin függvény argumantumában a szorzat két oldalán álló kifejezéseket szintén párhuzamosan számíthatom. Az sqrt argumentumában a három tagból álló összeg mindegyik tagjának számítását egymással párhuzamosíthatom. 6.22 Még egy lépés: a funkcionális programozás Ha tovább folytatjuk a lambda kalkulus következményeinek vizsgálatát, eljutunk addig a pontig, hogy az egyszerű függvények hálózatával leírt kifejezés esetében nincs szükség a végrehajtás módjának leírására! Meg kell neveznünk a kiindulási paramétereket, illetve azt a célfüggvényt, amely értékét elő kívánjuk állítani, és a végrehajtás (több lehetséges) módja
ebből már levezethető! Az alábbi, kifejezéssel egy eredményhalmazt SQL lekérdezéssel definiálok: f(x, y) = select Customer.ID, count(*) from Customer inner join Order on Customer.ID = OrderCustomerRef where Customer.City = x and Order.DateYear = y Ez az SQL kifejezés bizony leírható lambda kalkulussal (aki nem hiszi, járjon utána)! Az SQL lekérdezésekről pontosan tudjuk (hiszen már régóta használjuk őket), hogy nem mi mondjuk meg a végrehajtás módját, azt az adatbáziskezelő eszköz fogja helyettünk "kitalálni". Ezzel eljutattunk a funkcionális programozásig: A lambda kalkulus segítségével lehetséges az, hogy nem imperatív módon utasításokkal és vezérlési szerkezetekkel írjuk le egy feladat megoldását, hanem funkcionálisan, azaz függvényekkel: definiáljuk az argumentumokat és leírjuk a célfüggvényt. Ezt tettük a fenti SQL lekérdezésben is Nos, készen állunk a C# 3.0 lambda kifejezéseinek definiálására! 6.3
Lambda kifejezés és függvényreferencia A C# 3.0 nyelvben a lambda kifejezés nem más, mint egy „zajtalanított” függvénydefiníció a hozzátartozó referenciával együtt, amelyen keresztül a függvény meghívható. Hogy ez érthető Változat: 0.8 (6) 23/30 legyen, vizsgáljunk meg először egy rövid példát! Készítsünk egy bővítő metódust, amely az egyszerűség kedvéért a List<> példány elemeit képes lekérdezni egy paraméterként átadott delegate segítségével. A delegate a lekérdezésnél használt kiválogatási (szűrési) szempontot határozza meg. public delegate bool FilterMethod<T>(T item); public static class LambdaSampleExtension { public static List<T> Query<T>(this List<T> source, FilterMethod<T> filterMethod) { var result = new List<T>(); foreach (T item in source) { if (filterMethod(item)) result.Add(item); } return result; } } Ebben a példában a Query bővítő metódus egy
FilterMethod típusú delegate-re mutat, amely egy listabejegyzésről eldönti, hogy az benne van-e az eredményhalmazban és ezt a visszatérési értékében jelzi. A Query metódust egyszerűen használatba vehetjük: // . var sourceList = new List<int> { 123, 321, 234, 432, 345, 543, 456, 654 }; var resultList = sourceList.Query(new FilterMethod<int>(FilterForEvenNumbers)); foreach (int item in resultList) { Console.WriteLine(item); } // . private static bool FilterForEvenNumbers(int number) { return number % 2 == 0; } A programrészletet futtatva a FilterForEvenNumbers metódus a listából a 234, 432, 456, 654 értékeket fogja kiszűrni (azokra ad vissza true értéket). A C# 20 szintaktikája lehetővé teszi számunkra az ún. „anonymous delegate” használatát, ezzel a fenti programrészleten rövidíteni tudunk : var sourceList = new List<int> { 123, 321, 234, 432, 345, 543, 456, 654 }; var resultList = sourceList.Query ( delegate(int number) { return
number % 2 == 0; } ); foreach (int item in resultList) { Console.WriteLine(item); } Természetesen, ez az eljárásunk pontosan az előzőnek megfelelő eredményt fogja adni. A C# 30 lambda kifejezései ezt a leírási módot még erőteljesebben zajmentesítik: Változat: 0.8 (6) 24/30 var resultList = sourceList.Query(n => n%2 == 0); Első ránézésre furcsának tűnik, de működik! Mit is jelent az n => n%2 == 0 lambda kifejezés? A lambda kalkulus leírásmódjával megfogva ez a lambda<n:n%2==0> kifejezésnek felel meg, vagyis egy olyan függvény, amely a bemenő n paraméteréhez igaz értéket rendel ha az n egy páros szám, illetve hamis értéket rendel, ha n páratlan. A C# felől megközelítve a kifejezés egy olyan metódust jelent, amelynek egyetlen bemenő paramétere van (az n) és visszatérési értékként true-t ad vissza, ha n páros, false értéket, ha n páratlan. Vizsgáljuk tovább a C# jelentést! A => („nyíl”)
operátor köti össze a lambda kifejezés argumentumát a függvényértékkel. Ezt a kifejezést sokfajta módon lehet olvasni, pl: „n nyíl n%2 egyenlő nulla” „n, ahol n%2 egyelő nullával” „n-ből n%2 egyenlő nullával lesz” Nehéz a megfelelő kifejezést jól megtalálni, mert az a lambda kifejezés jelentésétől függően más és más lehet. Nekem ebben az esetben a második kifejezés a legszimpatikusabb A lambda kifejezést a korábbi változatokkal összevetve hiányérzetünk támad: hol marad az erős típusosság? Megnyugtatok mindenkit, a típusosság nem veszett el! Ugyanúgy, ahogyan a lokális típusfeloldásnál említettük, a fordítóprogram itt is a lambda kifejezés környezetéből kikövetkezteti az n típusát és így a lambda kifejezés függvényértékenek típusát is. Hogyan is történik ez? Vizsgáljuk meg a teljes kifejezést! var sourceList = new List<int> { 123, 321, 234, 432, 345, 543, 456, 654 }; var resultList
= sourceList.Query(n => n%2 == 0); Az első programsor alapján a fordítóprogram a sourceList változó típusát kikövetkezteti, az List<int> lesz. A második sorban a Query metódus hívásánál a fordítóprogram észreveszi, hogy a sourceList-nek nincs Query metódusa, ezért megvizsgálja a lehetséges bővítő metódusokat. A vizsgálat során azt találja, hogy a LambdaSampleExpression statikus osztály Query<T> metódusa illeszkedik a sourceList-re a T int helyettesítéssel, vagyis a fordítóprogram a második sort az alábbi módon látja: var resultList = LambdaSampleExpression.Query<int>(sourceList, n => n%2 == 0); A Query metódus teljes szignatúrája az alábbi: public static List<T> Query<T>(this List<T> source, FilterMethod<T> filterMethod) A FilterMethod az alábbi módon néz ki: public delegate bool FilterMethod<T>(T item); A fordítóprogram számára a Query<T> teljes kiegészítése:
List<T> Query<T>(this List<T> source, delegate bool FilterMethod<T>(T item)) Változat: 0.8 (6) 25/30 Ebből a fordítóprogram számára T int helyettesítés történik, vagyis ezek után már pontosan tudja, hogy a n => n%2 == 0 lambda kifejezésben az n típusa int. Az Olvasó számára innen talán már világos, hogy miért is írtam a fejezet elején azt, hogy a C#-ban a lambda kifejezés egy „zajtalanított” függvény: a jelölés segítségével igen letisztult formában ragadjuk meg a függvény lényegét: adott függvényparaméterhez milyen függvényértéket rendel. 6.4 Ahogyan a fordítóprogram reagál Magyarázatra szolgál még a „hozzátartozó referenciával együtt” rész a lambda kifejezés általam korábban használt definíciójában. Nos, a lambda kifejezéseket önálló függvényként nem használjuk Amikor a C# fordító egy lambda kifejezést talál, nem csak a függvény defincióját hozza automatikusan
létre, hanem egy delegate típust is definiál hozzá, amelyen keresztül a lambda kifejezést reprezentáló függvény meghívható. A programrész lefordított változatába a delegate hívása kerül A Reflector eszköz segítségével megnéztem, hogy mire fordul le az alábbi kifejezés: var resultList = sourceList.Query(n => n%2 == 0); A fordítóprogram a következő programrészt gyártja le ehhez: // --- A lambda kifejezést reprezentáló metódus [CompilerGenerated] private static bool <Main>b 1e(int n) { return n % 2 == 0; } // --- A delegate, amelyben a lambda kifejezéshez tartozó metódus tárolódik [CompilerGenerated] private static FilterMethod<int> <>9 CachedAnonymousMethodDelegate1f; // --- Az eljárás hívása <>9 CachedAnonymousMethodDelegate1f = new FilterMethod(<Main>b 1e); List<int> resultList = sourceList.Query<int>(<>9 CachedAnonymousMethodDelegate1f); Látható, hogy a fordítóprogram a lambda
kifejezésnek megfelelő függvény metódusát is létrehozta, illetve egy delegate változót is definiált hozzá, s mindkettőt megjelölte a CompilerGenerated attribútummal. A lambda kifejezés használata előtt előállítja a hozzátartozó delegate típust és azt adja át a Query metódusnak. Ez a működés az Orcas Beta 1-ben található, a végleges változatban ez akár meg is változhat. 6.5 Többváltozós lambda kifejezések A lambda kifejezések nem csak egyváltozós függvényekre, hanem nulla, egy vagy több változós függvényekre is használhatók. A nulla változós lambda kifejezések szintaktikai leírásánál a „()”-el szemléltetjük azt, hogy nincs argumentuma a függvénynek. Például: () => 42 () => DateTime.Now Gyakran szükségünk lehet egynél több argumentumot tartalmazó lambda kifejezésre. Nézzünk erre egy mintapéldát! Terjesszük ki a korábban is használt LambdaSampleExpression statikus osztály egy újabb metódussal, az
Aggregate-tel: Változat: 0.8 (6) 26/30 public delegate TSeed AggregateMethod<TSeed, TItem>(TSeed seed, TItem item); // . public static class LambdaSampleExtension { // . public static TAgg Aggregate<T, TAgg>(this List<T> source, TAgg initalSeed, AggregateMethod<TAgg, T> aggregator) { TAgg result = initialSeed; foreach (T item in source) { result = aggregator(result, item); } return result; } } Az AggregateMethod generikus delegate típus olyan metódusra vonatkozó referenciát ad, ahol egy ún. magértékből és egy növekményből állítunk elő egy újabb, a növekménnyel módosított magértéket. Az Aggregate bővítő metódus egy generikus listában lévő elemek aggregátumát állítja elő, ahol paraméterként a lista mellett át kell adnunk a magérték kezdő értékét illetve egy olyan metódusra való hivatkozást, a mely a magérték és a növekmény kezelését végzi. Az Aggregate használata során kétváltozós lambda
kifejezést használunk: // . var sourceList = new List<int> { 123, 321, 234, 432, 345, 543, 456, 654 }; int sum = sourceList.Aggregate(0, (seed, item) => seed + item); Console.WriteLine(sum); // . A programrészt futtatva visszakapjuk a listában szereplő számok összegét (3108). A lambda kifejezések további erejét mutatja, ha megváltoztatjuk az Aggregate által használt lambda kifejezést, például az alábbi módon: // . var sourceList = new List<int> { 123, 321, 234, 432, 345, 543, 456, 654 }; string concat = sourceList.Aggregate("", (seed, item) => seed + (seed == "" ? "" : "|") + item); Console.WriteLine(concat); Ez a kifejezés a listában lévő int értékek szöveges reprezentációját összefűzi és közéjük az „|” elválasztó karaktert illeszti. A fordítóprogram a többváltozó lambda kifejezéseket pontosan ugyanúgy kezeli, mint az egyváltozósakat. A fenti kifejezésekhez tartozó
metódusokat a Reflectorral megvizsgálva ráismerhetünk az általunk megadott lambda kifejezésekre: [CompilerGenerated] private static int <Main>b 1f(int seed, int item) { return seed + item; } // . [CompilerGenerated] private static string <Main>b 20(string seed, int item) { return seed + ((seed == "") ? "" : "|") + item; Változat: 0.8 (6) 27/30 } 6.6 Lambda kifejezések újrahasznosítása A fentiekben megfigyelhettük, hogy amikor leírunk egy lambda kifejezést, akkor abból a fordítóprogram egy metódust és egy arra vonatkozó hivatkozást (delegate) készít. Mi történik akkor, amikor többször használjuk ugyanazt a lambda kifejezést? Vizsgáljuk meg például az alábbi programrészletet: var sourceList = new List<int> { 123, 321, 234, 432, 345, 543, 456, 654 }; int sum = sourceList.Aggregate(0, (seed, item) => seed + item); int filteredSum = sourceList.Query(n => n % 2 == 0) Aggregate(0, (seed, item)
=> seed + item); Bár ugyanazt a lambda kifejezést használtuk fel mindkét alkalommal, azok között a fordítóprogram azonban semmilyen egyezést nem vizsgál. Ugyanahhoz a lambda kifejezéshez a két metódust is generál, amelyek törzse ugyanaz. Ezt a fordítóprogram jól is teszi 9 Adódhatnak olyan helyzetek, amikor szeretnénk egy lambda kifejezést több helyen is felhasználni a programban. A C# 30 erre is lehetőséget biztosít: létrehozhatunk delegate példányt egy lambda kifejezéssel inicializálva. Erre mutat megoldást az alábbi programrészlet, amely a fenti változatot alakítja át: var sourceList = new List<int> { 123, 321, 234, 432, 345, 543, 456, 654 }; AggregateMethod<int, int> sumMethod = (seed, item) => seed + item; int sum = sourceList.Aggregate(0, sumMethod); int filteredSum = sourceList.Query(n => n % 2 == 0)Aggregate(0, sumMethod); Úgy gondolom, ez a leírásmód elég egyszerű és jól szemlélteti a fejlesztői szándékot.
6.7 Explicit típusos lambda kifejezések Korábban már megfigyeltük, hogy a fordítóprogram a lambda kifejezéseket a hívási környezetbe helyezve automatikusan ki tudják következtetni a függvényargumentumok paramétereinek típusát. Ez jó dolog, de gyakran hasznos lenne, ha a fejlesztőnek a programkód olvasása közben nem kellene „fordítóprogramot” játszania, hogy a lambda kifejezések argumentumtípusait meghatározza. Sokkal egyszerűbb volna, ha azokat azonnal láthatná. Nézzük meg ezt egy példán, használjuk az alábbi delegate típust: public delegate int SimpleOperation(int a, int b); Amikor egy a fenti delegate-nek megfelelő műveletet szeretnénk definiálni, például az alábbi módon tehetjük meg: SimpleOperation concatOrAdd = (a, b) => a + b; Ez így teljesen érthető. Azonban egy hosszú programkód olvasása közben a SimpleOperation típus deklarációja és példányosítása szinte biztos, hogy különböző fájlokban lesz. A kód
olvasása közben a 9 Nyilvánvaló, hogy nem az egyszerűbb fordítóprogram írási feladatok közé tartozik két lambda kifejezés ekvivalenciájának a vizsgálata, azzal a csekély programméret optimalizálási lehetőséggel, amelyet így el lehet érni, a C# fordító nem is foglalkozik (a Beta 1 változatban, legalábbis). Változat: 0.8 (6) 28/30 fejlesztő nem biztos, hogy emlékszik a SimpleOperation pontos típusaira. Ha a kódot papíron olvassa, akkor még az IntelliSense segítségét sem veheti igénybe 10. Ez a konkrét esetben azért lehet probléma, mert a lambda metódus által definiált összeadás művelet egyaránt vonatkozhat numerikus típusokra abból is van jónéhány és string típusokra. Ahol nagy aszükség, közel a segítség: a lambda kifejezések szintakszisa lehetővé teszi, hogy explicit módon feltüntessük a típusokat. A concatOrAdd példány létrehozását az alábbi módon is írhatjuk: SimpleOperation concatOrAdd = (int a, int
b) => a + b; Az explicit típusleírásnál a fordítóprogram ellenőrzi az erős típusosságot. Kizárólag akkor fogadja el a lambda kifejezés definícióját, ha annak összes explicit típusa pontosan megegyezik a delegate-hez tartozó típussal. Fontos, hogy ha a lambda kifejezés bármelyik paraméterét explicit típussal látjuk el, akkor az összes többi paraméterhez is oda kell fűznünk azok explicit típusát! Az explicit típusdeklarációt bárhol leírhatjuk, nem csak akkor, amikor delegate példányoknak közvetlenül adunk értéket. A fejezet elején legelején szereplő szűrésre is alkalmazhatjuk: var resultList = sourceList.Query((int n) => n%2 == 0); Általános javaslat: ha bármilyen lambda kifejezés kapcsán az explicit típusparaméter segít az olvasásban, használjuk azt. 6.8 Összetett lambda kifejezések Egy metódus törzsében nem csak egyszerű kifejezéseket írhatunk le, hanem utasításokat, ciklusokat is. Az eddigi példákban
bemutatott lambda kifejezések egyszerűen egy-egy kifejezést tartalmaztak, amelyek a kifejezést értékét adták vissza függvényértékként. Mi a helyzet akkor, ha a függvényértéket nem lehet egyszerűen egy kifejezés értékeként visszaadni, hanem ahhoz több számítási műveletet is el kell végezni, esetleg ciklusokat is használni? Nos, a C# 3.0 lehetőséget ad több utasítást tartalmazó lambda kifejezések leírására is Például az alábbi kódrészlet egy olyan lambda kifejezést definiál, amely n-hez n! értékét számítja ki: UnaryOperation<int> factorial = n => { if (n < 0) throw new ArgumentException("Argument cannot be negative", "n"); int result = 1; for (int i = 1; i <= n; i++) result *= i; return result; }; Ez a konstrukció működik, a fordítóprogram elfogadja. Nem meglepő módon, ezt az egész programrészletet nemcsak egy delegate példány explicit inicializálása során használhatjuk, hanem egy
megfelelő metódus argumentumaként is. Ez utóbbi meglehetősen olvashatatlanná teszi a programjainkat. Az összetett több utasítást tartalmazó lambda kifejezések használatával kapcsolatosan én az alábbi konvenciók betartását javaslom: Összetett lambda kifejezéseket lehetőleg csak explicit inicializálásra használjunk. Ha a lambda kifejezés hosszabb kódot tartalmaz, azt inkább emeljük ki külön metódusba és a lambda kifejezés ezt a metódust hívja. 10 Hallottátok, hogy a Visual Studio Orcast követő változatában már a papírra nyomatott programkódra is működni fog az IntelliSense? Változat: 0.8 (6) 29/30 6.9 Egyebek A lambda kifejezések és használatuk még számtalan egyéb kérdést vet fel (pl. „tiszta” lambda kifejezések használata, optimalizációs lehetőségek, stb.) Ezek tárgyalása meghaladja ennek a cikknek a kereteit, de a későbbiekben mindenképpen vissza szeretnék térni azokra. 7. Összegzés A C# 3.0
sok szintaktikai és hozzájuk kötött szemantikai újítást tartalmaz Ezek az újítások önmagukban elég egyszerűek, tulajdonképpen „fordítóprogram trükköknek” is tekinthetők. A nyelv a segítségükkel sok helyen tisztábbá (letisztultabbá) válik azáltal, hogy ezek rengeteg szintaktikai zajt megszűntetnek. Máshol (pl lambda kifejezésekben) szokatlannak tűnő szintaktikai elemek jelennek meg (pl. a => operátor) Ezek az önmagukben egyszerűnek (és „ötletszerűnek”) tűnő módosítások komoly és hatékony rendszerré állnak össze abban a képességben, amit LINQ-nek hívunk. További cikkeimben megmutatom hogyan. Változat: 0.8 (6) 30/30