A C# programozási nyelv

C# 3.0 Újdonságok

C# 3.0 Új nyelvi elemek

A .NET 3.5 (vagy más néven .NET „Orcas”) változatának egyik legfontosabb (és talán leghangoztatottabb) újdonsága a LINQ (Language Integrated Query). A LINQ, mint képesség megvalósítása két fontos dolgot jelent a .NET 3.5-ben:

A C# 3.0 új elemei

A LINQ és a funkcionális programozás hatása az alábbi új elemeket hozta a C#-ba:

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.Now.Year - BirthDate.Value.Year) : 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.

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:

public int ReadOnlyNumber { get; private set; } public string WriteOnlyName { private get; set; }

É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:

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
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. 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; } }
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ú!

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.

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:

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 };

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 };
Azonban 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 }); //A legtömörebb leírást azonban a C# 3.0 biztosítja: List < int> primeNumbers = new List < int> { 2, 3, 5, 7, 11 };

Ö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.

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. 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.

Lokális típusfeloldás

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.Collections.Generic 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ékadások jobboldala eleve meghatározza, hogy a baloldalon szereplő változók típusa milyen lehet, azt mégis kiírjuk.

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"]; } }

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"]; } }

Névtelen típusok inicializálása még egyszer

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 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” szintaxis «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.

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.

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 First (this IEnumerable source, int count) { var result = new List (); 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.

Ó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 > " : instance.ToString()); }

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