Nová verze jakyka C#, verze 3.0, přináší poměrně dost novinek. Většina z nich je ale jen syntaktický cukr a assemblies vytvořené pro .NET Framework 3.5 jsou binárně zpětně kompatibilní s verzí 2.0. Nedochází tedy ke změně CLR. Většina z novinek jde na ruku požadavkům vzešlých z praxe, především usnadňují integraci dotazovacího jazyka přímo do C#.
Implicitně typované lokální proměnné
Kolikrát se dostaneme do situace, kdy chceme napsat např. takovýto kód:
Dictionary<string, Dictionary<string, int>> dict = new Dictionary<string, Dictionary<string, int>>();
Jak je vidět, trošku se opakujeme. V C# můžeme tento kód zredukovat použitím klíčového slova var, které značí, že typ proměnné určí během překladu kompilátor podle toho, co do proměnné přiřazujeme. Kód tedy bude vypadat takto:
var dict = new Dictionary<string, Dictionary<string, int>>();
Klíčové slovo var můžeme použít pouze pro deklarování lokálních proměnných (nelze použít na deklaraci typů memberů) a spolu s deklarací musí být vždy uvedena inicializace. Následující kód je tedy neplatný, protože překladač by nevěděl, jakého typu má proměnná být.
var i;
Implicitně typovaná pole
Mírným zobecněním předchozího je to, že lze vytvořit pole, u něhož nespecifikujeme typ prvku pole, ale tento typ si domyslí překladač sám. Nutnou podmínkou je to, aby šly všechny prvky pole implicitně přetypovat na tentýž typ.
var array1 = new [] { 1, 2, 3.0 };
var array2 = new [] { "hi", 2, "bye" };
V prvním případě tedy překladač vyhodnotí typ prvku pole jako double a ve druhém případě vyhodí chybu při překladu – mezi intem a stringem totiž neexistuje implicitní konverze (a implicitní konverzi na object taky nemají, takže to překladač nevyhodnotí ani jako object).
Automaticky implementované properties
Nevím jak Vy, ale já se v kódu často setkávám s následující konstrukcí:
public class Point
{
public int X
{
get { return this.mX; }
set { this.mX = value; }
}
protected int mX;
public int Y
{
get { return this.mY; }
set { this.mY = value; }
}
protected int mY;
}
Protože se tato konstrukce asi neopakovala často jen v mém kódu, máme tu automatické properties, které nám ušetří zbytečné psaní:
public class Point
{
public int X
{
get;
set;
}
public int Y
{
get;
set;
}
}
Samozřejmě je možné getterům i setterům změnit viditelnost.
Anonymní typy
Můžeme vytvořit instanci třídy anonymního typu, přičemž taková třída obsahuje pouze properties, které specifikujeme.
var temp = new { Name = "Michal", Nick = "Augi", Age = 25 };
Proměnná temp bude anonymního typu (typu bez jména), který bude obsahovat property Name typu string, property Nick typu string a property Age typu int.
Protože se jedná o anonymní typ, nevěděli bychom jakého typu má být proměnná temp a proto zde krásně využijeme klíčové slovo var a necháme typ určit kompilátor. A nic nám nebrání vytvořit implicitně typované pole anonymních tříd 🙂
Inicializátory objektů
V praxi se často setkáme s tím, že po vytvoření objektu potřebujeme ihned nastavit některé properties nově vytvořeného objektu. Pokud tento nově vytvořený objekt ale pouze předáváme jako parametr nebo vracíme jako návratovou hodnotu, byli jsme nuceni použít nějakou pomocnou proměnnou, kam jsme si dočasně uložili referenci na objekt:
User u = new User();
u.Username = "Augi";
u.Password = "nepovim";
DoSomeBussinessOperation(u, 1, 3);
S využití inicializátoru (které byly zatím dostupné jen pro vytváření atributů) bude kód vypadat takto:
DoSomeBussinessOperation(new User { Username = "Augi", Password = "nepovim" }, 1, 3);
Inicializátory kolekcí
Při vytváření třídy implementující rozhraní System.Collections.ICollection<T> můžeme specifikovat její počáteční obsah jednoduše takto:
var list = new List<int> { 1, 2, 3 };
Extension methods
V praxi jsme občas nuceni pracovat s třídou, která je označena jako sealed a nelze od ní tedy odvodit vlastní třídu, která by přidala další funkcionalitu. Toto nám ale nyní umožní extension methods takto:
public static class Extensions
{
public static int GetSquare(this int i)
{
return i * i;
}
public static void GeneratePassword(this User u, int seed)
{
u.Password = GenerateRandomPassword(seed);
}
}
Tímto jsme přidali ke všem instancím typu int novou metodu GetSquare, která vrací druhou mocninu instance. Dále jsme přidali třídě User metodu GeneratePassword, která vygeneruje náhodné heslo pro uživatele.
Extension method je tedy metoda, která musí být označena jako statická, musí být umístěna ve statické třídě a musí mít minimálně jeden parametr, přičemž první parametr musí být uveden klíčovým slovem this. Typ tohoto prvního parametru udává, jakou třídu rozšiřujeme o novou funkcionalitu. Protože se jedná o „normální“ metodu, vidíme z této metody jen ty membery rozšiřovaného typu, které bychom viděli z jiné statické metody. Při rozšíření funkcionality typu není tedy možné využít např. protected membery, které bychom mohli použít při klasickém rozšíření pomocí dědění.
Výše uvedené metody můžeme použít stejně jako jiné instanční metody daných typů, tedy např. takto:
Console.WriteLine(5.GetSquare());
var u = new User { Username = "Augi", Password = "nepovim" };
u.GeneratePassword(Environment.TickCount);
Lambda funkce
Lambda funkce je vlastně jen takový méně ukecaný způsob zápisu anonymního delegáta. Pokud bychom chtěli v C# 2.0 zjistit, zda v daném seznamu čísel existuje číslo 5, mohli bychom to udělat takto:
List<int> l = new List<int>();
// ...
l.Exists(delegate(int i) { return i == 5; });
S využitím lambda funkce to můžeme přepsat takto:
List<int> l = new List<int>();
// ...
l.Exists(i => i == 5);
Takže syntaxe lambda funkce vypadá tak, že před => je seznam parametrů (pokud jich je více, tak v závorce a oddělené čárkou) a za => je výraz, který představuje návratovou hodnotu.
Expression tree
Doteď se jednalo pouze o syntaktický cukr, tedy o něco, co nám pouze zpříjemní práci. Expression tree už je ale něco nového (avšak stále kompatibilního s CLR 2.0). Jde o to, že když vytvoříme lambda funkci, tak můžeme nyní určit, co chceme do metody předat – buď si můžeme nechat poslat klasicky delegáta nebo expression tree, což je objektová reprezentace syntaktického stromu lambda funkce. Tedy tělo lambda funkce je naparserováno C# překladačem a syntaktický strom je poslán do naší metody, kde s ním můžeme dělat co je libo.
public void Method1(Func<string, bool> predicate) {}
public void Method2(Expression<Func<string, bool>> predicate) {}
V případě první metody dostaneme delegáta na metodu, která má jediný parametr typu string a vrací bool. V případě druhé metody ale už nedostaneme delegáta, ale syntaktický strom reprezentující předaného delegáta.
Partial methods
V C# 1.0 jsme museli vždy napsat celou třídu do jednoho souboru. V C# 2.0 se přišlo s partial class, což nám umožnilo rozdělit implementaci třídy do více souborů. To je užitečné hlavně v případě automaticky generovaného kódu. Když např. píšeme klasickou WinForms aplikaci, tak část třídy formuláře generuje designer. Pak se hodí mít jeden soubor, do kterého hrabe pouze designer a generuje si do něj kód, a druhý soubor, kam si píšeme např. vlastní event handlery.
C# 3.0 jde ještě dál a přichází s konceptem partial methods, který nám umožní rozdělit implementaci metody do více souborů. Podobně jako u partial class, stačí metodu uvést klíčovým slovem partial. Taková metoda je implicitně privátní (takže nemůže být virtuální), musí vždy vracet void a nesmí mít parametry předávané jako out. Na parciální metodu také není možné vytvořit delegáta. Tyto omezení vycházejí především ze skutečnosti, že není zaručeno pořadí volání jednotlivých částí metody.
A k čemu to může být dobré? Např. generátor kódu vygeneruje parciální metodu OnNameChanging (s prázdným tělem) a tu bude volat v setteru property Name. My pak můžeme v jiném souboru dopsat např. nějakou validaci jednoduše tím, že napíšeme parciální metodu se stejným jménem a signaturou a příp. v ní vyvoláme výjimku.
Samozřejmě tato funkcionalita by šla řešit i jinak, ale už by to nebylo takto jednoduché.
LINQ
Language Integrated Query je něco, co vnáší do jazyka unifikovaný způsob, jak se dotazovat do „libovolného“ datového zdroje pomocí syntaxe velice podobné SQL. Máme zde tedy operátory select, where, groupby, join atd. Takový dotaz může vypadat např. nějak takto:
var result = from i in l where i == 5 select new { Int = i, Name = "bla" };
Za klíčovým slovem from uvedeme identifikátor, který bude představovat jeden řádek tabulky, za in je uveden datový zdroj, za where je tělo lambda funkce, která určuje, zda daný řádek bude zahrnut do výsledku (jeho hodnota tedy musí být typu bool) a za select uvedeme, jak se vytvoří objekt reprezentující jeden řádek výsledku (zde používám anonymní třídu).
Jedná pouze o syntaktický cukr, protože select je „přeloženo“ na metodu Select, where na Where atp. Všechny objekty, na které se chceme dotazovat, musí tedy implementovat tyto LINQ metody. Přímo v .NET Frameworku máme statickou třídu System.Linq.Enumerable<T> (v assembly System.Core), která obsahuje extension methods pro rozhraní IEnumerable<T>, a v této třídě jsou všechny požadované LINQ metody, takže dotazy může provádět na vším, co implementuje IEnumerable<T>.
Kromě rozhraní IEnumerable<T> je nově dostupné také rozhraní IQueryable<T>, které je od IEnumerable<T> odvozené a které přidává především property Provider typu IQueryProvider. Pro IQueryable<T> také existuje implementace všech LINQ metod – a to ve třídě System.Linq.Queryable (opět z assembly System.Core). Tato implementace vypadá tak, že přijímá povětšinou expression tree a předává ho k vyhodnocení konkrétnímu providerovi (IQueryProvider). Zjednodušeně řečeno můžeme implementace LINQ metod nad rozhraním IQueryable<T> rozdělit na dvě skupiny – jedny volají ve finále na provideru metodu CreateQuery, která opět vrací IQueryable<T> a tyto metody tedy často vůbec nekomunikují s datovým zdrojem a slouží jen pro sestavení dotazu. Příkladem takových metod je například Select, Where nebo Join. Druhou skupinou metod jsou ty, které na providerovi volají metodu Execute – takové metody už vyžadují komunikaci s datovým zdrojem a vrací přímo nějaký výsledek. Typicky jsou to metody, které něco počítají nad všemi výslednými „řádky“, tedy např. metody Min, Max, Sum nebo Average.
U první skupiny metod je tedy dobré si uvědomit, že slouží jen k vytvoření dotazu, ne přímo k jeho vykonání. Dotaz je pak skutečně vykonán až tehdy, kdy je to skutečně potřeba, tedy např. při volání metody GetEnumerator (volá ji sám foreach). Pokud potřebujeme ale vyhodnotit dotaz ihned (např. proto, že nechceme nebo nemůžeme držet otevřené připojení do databáze), ale ještě nechceme enumerovat přes výsledek, musíme nějak donutit IQueryable<T> výsledek vrátit, tedy zavolat metodu Execute příslušného providera, která nám vrátí konkrétní výsledek (typicky dotazem do datového zdroje). To můžeme udělat např. pomocí extension metody ToList nebo ToArray, které vyenumerují všechny položky výsledku a uloží je do Listu, resp. pole.
Abychom mohli provádět dotazy nad vlastní třídou (to bude ale asi v praxi řešit málokdo), musí tedy tato třída implementovat rozhraní IQueryable<T>, což je IEnumerable<T> rozšířená především o property typu IQueryProvider. Musíme tedy implementovat ještě toto rozhraní, které je zodpovědné za vlastní vytváření a spouštění dotazů (metody CreateQuery a Execute).
Různé providery se nejčastěji označují jako „LINQ to …“. Existuje tak LINQ to IEnumerable<T>, které je obsaženo ve standardní třídě System.Linq.Enumerable a umožňuje dotazy nad čímkoliv, co implementuje rozhraní IEnumerable<T>. Dále tu máme LINQ to XML umožňující dotazování do XML dokumentu, LINQ to DataSets, které pracuje proti ADO.NET (a umožňuje tak pracovat vůči jakékoliv databázi), a hromadu dalších různých providerů od třetích stran – pro zajímavost např. Linq to Google.
LINQ to SQL
LINQ to SQL je provider, který nám umožní provádět všechny klasické DB operace nad MS SQL – a to díky LINQu silně typově a bez nutnosti psát přímo SQL kód specifický pro MS SQL. Dotazu v LINQu provider prostě přeloží na správný SQL dotaz, který je vykonán na databázi.
Aby bylo možné pracovat silně typově, je nutné nejprve vytvořit datový model reprezentující databázi, tedy vygenerovat třídy reprezentující tabulky databáze, vztahy mezi tabulkami atd. Důležité je, že toto mapování mezi databází a objekty, nad kterými pracujeme, je 1:1 – každé tabulce v databázi odpovídá jedna třída.
Entity Framework
Entity Framework je už skutečný OR mapper, který nám umožní vygenerovat takové třídy, které nemusí být v poměru 1:1 s databází. Idea je taková, že objektový model reprezentuje konceptuální schéma databáze („bussiness pohled na databázi“) a my musíme definovat mapování tohoto konceptuálního modelu na fyzickou databázi (např. dekompozice tabulky). Tím bude náš program odstíněn od fyzického uložení dat a jeho případná změna pro nás bude znamenat pouze změnit mapování – ne celou data access layer.
Mapování mezi konceptuálním a fyzickým modelem je možné provádět komfortně přímo ve Visual Studiu pomocí designeru a v případě nutnosti není problém sáhnout přímo do XML popisující toto mapování.
LINQ to Entity je pak provider, který umožňuje pracovat pomocí LINQu s objektovým modelem Entity Frameworku, tedy provádět dotazy přímo nad konceptuálním modelem databáze.
U LINQ to XML by bylo dobré zmínit, že je nekompatibilní s XML DOM. Třeba pro mě bylo dost nepříjemné zklamání, že nemůžu použít pěkné nové funkce nad starým kódem.
To se mi líbíTo se mi líbí
Jojo, to je pravda – musí se použít třídy ze System.Xml.Linq – XDocument, XElement atd.
To se mi líbíTo se mi líbí