Jazyk C# je spolu s jazykem Visual Basic .NET hlavním jazykem pro vývoj aplikací na platformě .NET. Po poměrně obsáhlé množině novinek, která k nám dorazila ve verzi jazyku C# 2.0, z nichž za nejvíce zásadní pokládám podporu pro generika, je nyní s novou verzí C# 3.0 k dispozici další řada novinek, které by vývojářům měly pomoci zefektivnit práci. Některé novinky jsou pro usnadnění implementace častých operací (automatické vlastnosti, inicializátory objektů), ale jiné jako například Lambda výrazy nabízí pro silně typový OOP jazyk nezvyklé možnosti tvorby anonymních metod, které připomínají spíše funkcionální programování.
Většina těchto změn byla ovšem do jazyku C# zanesena z důvodu existence ambiciózního projektu LINQ, který radikálně mění způsob práce s daty na platformě .NET. Bez novinek obsažených v C# 3.0 by bylo obtížné a někdy také nemožné využívat technologii LINQ efektivně.
I přestože některé změny vypadají radikálně, dobrou zprávou je, že všechny nové vlastnosti v C# 3.0 jsou pouze „syntaktickým cukrem“. To znamená, že kvůli těmto novým vlastnostem, nebyly provedeny žádné změny do specifikace CIL a výsledný CIL kód, který vznikne po kompilaci je stejný jako kdyby byl vytvořen kompilací ze C# 2.0. Pojďme se tedy na nové vlastnosti jazyku C# postupně podívat.
Automaticky implementované vlastnosti
Začneme zlehka novinkou s názvem automaticky implementované vlastnosti (Auto-implemented properties). V jazyku C# se již od první verze implementuje přístup k datovým členům pomoci vlastností. Není pochyb o tom, že je to nutná věc k zachování zapouzdření datových členů, nicméně v častých případech se v get ani set části nevykonává jiná operace než čtení respektive přiřazení datového členu.
Právě pro tyto případy jsou tu nově automaticky implementované vlastnosti, které nám umožní oprostit se od neustále se opakující definice datových členu a řízení přístupu k nim pomocí vlastností. Automaticky generované vlastnosti totiž tuto práci řeší za nás a na nás je pouze určit název, datový typ a možné režimy přístupu. To v praxi znamená, že již není nutné pro implementaci jednoduché vlastnosti psát následující kód:
public class Customer
{
private string firstName;
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
}
}
}
Implementace ale může být zjednodušena do následující podoby:
public class Customer
{
public string FirstName
{
get;
set;
}
}
V případě záměru používání této novinky je dobré vědět, že vždy musí být umožněno jak čtení vlastnosti tak její zápis, jinak obdržíme kompilační chybu. Samozřejmě je možné aplikovat na daný přístupový blok určitý modifikátor přístupu a tak zamezit nastavení vlastnosti externímu kódu, jak je v následujícím příkladu implementováno u vlastnosti ID.
public class Customer
{
public Customer()
{
ID = new Random().Next(1000);
}
public int ID
{
get;
private set;
}
}
Inicializátory objektů
Další s kolekce příjemných novinek v C# 3.0, které nás oprostí od zdlouhavého psaní kódu je nová možnost inicializace objektů. Pomocí této vlastnosti je možné nastavit viditelné datové členy nebo vlastnosti již při vytváření objektu bez nutnosti definovat speciální verzi konstruktoru. V praxi to znamená ušetření několika řádek kódu pro nastavení jednotlivých vlastností v případě neexistence vhodného konstruktoru, takže následující kód:
Customer cust = new Customer();
cust.FirstName = "Jan";
cust.Surname = "Novak";
...může být v C# 3.0 nahrazen tímto jedním řádkem:
Customer cust = new Customer() { FirstName = "Jan", Surname = "Novak" };
Samozřejmě je možné nových inicializátorů objektu použít v kombinaci s námi definovaným konstruktorem. Následující příklad ukazuje nastavení vlastnosti FirstName pomocí speciální verze konstruktoru a nastavení vlastnosti Surname pomocí nového inicializátoru objektu.
Customer cust2 = new Customer("Jan") { Surname = "Novak" };
Inicializátory kolekcí
Dalším často zdlouhavým kódem je kód implementující naplnění kolekce, kde musela být neustále volána metoda Add na objektu kolekce. Autoři jazyku C# mysleli v novinkách i na tento problém a umožnili definici prvků kolekce už při jejím vzniku. Tento zjednodušený postup naplnění kolekce při její inicializaci je možný na všech typech, které implementují rozhraní IEnumerable a obsahují metodu Add. Díky této nové vlastnosti je možné takovýto kód:
List<int> listOfInts = new List<int>();
listOfInts.Add(1);
listOfInts.Add(2);
...nahradit tímto jedním řádkem:
List<int> listOfInts = new List<int>() { 1, 2, 3, 4, 5 };
Tyto nové inicializátory kolekcí nejsou omezeny pouze na použití v kombinaci s jednoduchými typy jako výše ukázaný int, ale je možné inicializátory použít v kombinaci s kterýmkoli námi definovaným typem.
List<customer> listOfCustomers = new List<customer>(){
new Customer(){FirstName = "Petr", Surname = "Pus"},
new Customer(){FirstName = "Jaroslav", Surname = "Kavis"},
new Customer(){FirstName = "Jan", Surname = "Novak"}
};
Odvození typu lokální proměnné
Další novinkou, která možná mnohé na první pohled vyděsí je přítomnost nového klíčového slova var. Ano, je to opravdu tak, C# 3.0 obsahuje toto slovíčko, které je nechvalně známé z naprosto netypového javascriptu. Ale není potřeba se děsit. Jazyk C# je stále silně typovým jazykem a nové klíčové slovo var zapříčiní odvození typu lokální proměnné v době kompilace. Takže se jedná o další syntaktické cukrátko, které je ovšem nutné použít v kombinaci s novými anonymními typy, které si představíme o pár řádek níže.
Takže v nové verzi jazyku C# je možné lokální proměnnou nadefinovat bez toho, aby byl ve zdrojovém kódu explicitně uveden její typ.
var someVariable = "Hello World!";
//vypise se samozrejme String
Console.WriteLine(someVariable.GetType().Name);
Jelikož je typ lokální proměnné odvozen v době kompilace a místo slovíčka var se do výsledného kódu dostane odvozený typ proměnné (v příkladu výše by to byl typ String), není možné provést následné přiřazení hodnoty jiného datového typu, což je možné ve zmiňovaném javascriptu.
var someVariable = "Hello World!";
//nasledujici radek zpusobi kompilacni chybu
someVariable = 5;
Odvozený typ proměnné je možné v případě číselných datových typu do jisté míry ovlivnit použitím dostupných sufixů jak je možné pozorovat v následujícím kódu, který ukazuje jakým způsobem jsou kompilátorem odvozovány typy proměnných.
var s = "Hello"; //string
var i = 30; //int
var dbl = 30D; //double
var dec = 30M; //decimal
var l = 30L; //long
var f = 30F; //float
Extension methods
Zajímavá novinka se skrývá pod názvem Extension methods, nebo chcete-li rozšiřující metody. Pomocí této nové vlastnosti je možné rozšířit již existující typ o nové instanční metody bez toho, abychom jakýmkoli způsobem do daného typu zasáhli, nebo abychom od tohoto typu podědili (což v případě zapečetěných (sealed) tříd ani není možné). Této techniky je hojně využito v technologii LINQ, kde rozšíření stávajících typů (například IEnumerable) přineslo mnohem pohodlnější práci s daty.
Extension metody jsou implementovány jako speciální druh statických metod, úplně mimo od rozšiřovaného typu, s použitím klíčového slova this na první vstupní argument metody. Klíčové slovo this tedy v tomto případě nabývá nového významu, než tomu bylo doposud a označuje v extension metodě typ, který je rozšiřován.
Možná to zní komplikovaně, ale ukázka implementace to jistě objasní. Řekněme, že bychom chtěli rozšířit typ System.Object o novou metodu ToJson, která vrátí jeho reprezentaci v JSON formátu. V tom případě bychom museli implementovat následující extension metodu.
namespace CSharp_3_Examples.Extensions
{
internal static class Extensions
{
internal static string ToJson(this object obj)
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
return serializer.Serialize(obj);
}
}
}
To, že rozšiřujeme typ System.Object je řečeno právě novým použitím klíčového slova this na první (a v uvedeném příkladu jediný) vstupní argument statické metody. Přestože je metoda definována jako statická, tak její použití bude stejné jako použití kterékoli jiné instanční metody objektu. Aby mohla být extension metoda použita, musí být typ, který ji hodlá využít ve stejném jmenném prostoru jako třída, jež danou extension metodu obsahuje nebo tento jmenný prostor naimportovat.
using CSharp_3_Examples.Extensions;
namespace CSharp_3_Examples
{
public static class ExtensionMethods
{
public static void UseToJson()
{
Customer cust = new Customer() { FirstName = "Petr", Surname = "Pus" };
string json = cust.ToJson();
Console.WriteLine(json);
}
}
}
Extension metody mohou samozřejmě očekávat standardní vstupní argumenty, které následují po první argumentu s klíčovým slovem this a také mohou být přetížené.
internal static string AddString(this string str)
{
return AddString(str, "Hello ");
}
internal static string AddString(this string str, string stringToAdd)
{
return stringToAdd + str;
}
Dobré je vědet o poněkud zvláštním chování v případě null hodnot. Pokud je referenční proměnná, na které voláme extension metodu, rovna hodnotě null, tak není vyhozena známá výjimka NullReferenceException jak by asi všichni čekali, ale normálně se extension metoda zavolá, akorát bude její první argument s klíčovým slovem this roven hodnotě null.
string str = null;
//nebude vyhozena vyjimka, ale newStr bude hodnoty 'Hello '
string newStr = str.AddString();
Lambda výrazy
Lamda výrazy jsou významnou novinkou v jazyku C#, která vzala inspiraci ve funkcionálním programování. Pomocí lambda výrazů je možné tvořit anonymní metody, které obsahují jeden výraz nebo několik příkazů a použít je kdekoli kde je očekávána instance delegáta. S lambda výrazy přichází do jazyku C# nový operátor =>, který je nazýván „přechází v“. Použitím lambda výrazů docílíme téhož jako v C# 2.0 pomocí anonymních metod s použitím klíčového slova delegate ovšem s výrazně kratším zápisem, kde není nutné uvádět typy argumentů a klíčové slovo return (v případě jednoho výrazu). Takže pokud jsme v C# 2.0 psali vyhledání prvků v kolekci anonymní metodou nějak takto:
List<int> listOfInts = new List<int> { 1, 2, 3, 4, 5 };
List<int> result = listOfInts.FindAll(delegate(int i)
{
return i < 4;
});
...to samé bychom v C# 3.0 napsali pomocí lambda výrazu takto krátce:
List<int> result = listOfInts.FindAll(i => i < 4);
Datové typy argumentů není nutné uvádět, protože jsou stejně jako v případě použití klíčového slova var odvozeny v době kompilace na základě použitého typu delegáta, kterým je v příkladu na hledání delegát Predicate<int>, z čehož kompilátor jednoduše odvodí, že typ jediného vstupního argumentu lambda funkce musí být int.
Obecný tvar lambda výrazu je v této podobě:
(vstupní argumenty) => výraz
Závorky nejsou nutné pouze v případě, že vstupní argument je jediný, což demonstruje příklad na vyhledávání výše. Jelikož lze lambda výrazy použit všude, kde je očekávána instance delegáta, můžeme je použít i následujícím způsobem, kde je použit nový generický delegát Func<> z .NET frameworku 3.5, kde je formou generických argumentů specifikováno jaké jsou typy vstupních argumentů a také jaký je navratový typ.
//tvorba lambda vyrazu na ekvivalenci jako instance delegata
Func<int, int, bool> lambdaEquals = (x, y) => x.Equals(y);
//zavolani lambda vyrazu jako delegata
bool result = lambdaEquals(3, 3);
V případě, že lambda výraz nepotřebuje žádné vstupní argumenty, na místě vstupních argumentů budou pouze prázdné závorky.
Func<string> lambdaHello = () => "Hello lambda";
Pokud nechceme nechat odvození typu formálních argumentů na kompilátoru, což není vždy možné, můžeme tyto typy specifikovat. Mimo jiné to, dle mého subjektivního názoru, vede k lepší čitelnosti kódu.
Func<string, int, bool> lambdaLength = (string s, int l) => s.Length.Equals(l);
bool result = lambdaLength("hello", 5);
Pomocí lambda výrazů je možné implementovat i anonymní metody s více než jedním příkazem. V tomto případě se tyto lambda výrazy označují jako statement lambdas a rozdílem oproti standardním lambda výrazům jsou použité složené závorky a v případě potřeby navrátit hodnotu použítí klíčového slova return.
private delegate void TestDelegate(string str);
…
TestDelegate test = s => { s += " World"; Console.WriteLine(s); };
test("Hello");
Anonymní typy
Novinka nazvaná anonymní typy umožňuje definovat nové bezejmenné typy za běhu a rovnou vytvořit jejich instanci. Anonymní typy odstraňují nutnost explicitně definovat typ, který je potřeba pouze na jednom místě a díky tomu je této techniky použito v LINQ pro výběr pouze některých datových složek z existujících typů.
Jelikož název typu není nikde z kódu dostupný, je použití anonymních typů silně spjato s novým klíčovým slovem var. Anonymní typ je definován pomocí výčtu veřejných vlastností, kterým je rovnou přiřazena hodnota. Těmto veřejným vlastnostem může být přiřazena hodnota pouze při vytváření instance, protože jsou tyto vlastnosti vytvořeny pouze pro čtení a jejich následné modifikace v dalším kódu tedy nejsou povoleny. Typ veřejných vlastností je odvozen v době kompilace.
Následující kód ukazuje použití anonymního typu s dvěma vlastnostmi.
var myVarible = new { Name = "Petr Pus", ID = 1 };
//vlastnosti jsou pouze pro cteni
Console.WriteLine("{0} - {1}", myVarible.ID, myVarible.Name);
O anonymních typech je dobré vědět, že to jsou vždy třídy zděděné přímo od System.Object. Je jim kompilátorem přiřazeno jméno, které ovšem není z kódu dostupné a pokud jsou použity v anonymním typu stejné vlastnosti (typ, počet a pořadí) je jim vygenerováné jméno stejné a sdílejí stejné vygenerované typové informace.
Částečné metody
V C# 2.0 se objevilo klíčové slovo partial, které umožnilo tvořit částečné typy, které byly rozděleny do více souborů a umožnily tak dobré oddělení kódu generovaného vývojovým prostředím a kódu tvořeným vývojářem. V C# 3.0 je již možné aplikovat klíčové slovo partial nejen na typy, ale také na jednotlivé metody, čímž se dosáhne možnosti oddělit deklaraci metody od její implementace v jednotlivých souborech.
Toho je opět využito v kódu, který je generován vývojovým prostředím. Částečné metody totiž mohou být v jednom souboru deklarovány a v druhém souboru částečného typu implementovány, přičemž implementace není povinná. Kód vygenerovaný vývojovým prostředím může obsahovat volání těchto metod, a jelikož není povinná, jejich implementace je na vývojáři, který pokud má zájem, může danou metodu implementovat k provedení svých vlastních operací. To řeší problém, kdy je potřeba upravit chování generovaného kódu bez toho, aby do něj byly provedeny změny.
Částečné metody mohou být definovány pouze v částečných typech, nemají modifikátor přístupu a jsou pouze privátní, takže jejich jediné volání je možno v rámci daného typu. Praktické použití ukazuje následující příklad:
Soubor 1:
partial class PartialMethods
{
public void SomeMethod()
{
PartialMethodOne();
PartialMethodTwo();
}
//metoda je naimplementovana v druhem souboru castecneho typu
partial void PartialMethodOne();
//metoda neni implementovana
partial void PartialMethodTwo();
}
Soubor 2:
partial class PartialMethods
{
//implementace castecne metody
partial void PartialMethodOne()
{
Console.WriteLine("Partial method one ..");
}
}
Expression Trees
Expression trees jsou velmi zajímavou novinkou, která umožňuje pracovat s kódem nikterak jako se spustitelnými příkazy, ale jako s daty. Pomocí expression trees je možné v aplikaci vytvořit stromovou strukturu představující kód a tuto strukturu po té může jiný kód analyzovat, popřípadě vytvořit na jejím základě strom nový nebo strukturu převést do spustitelného kódu a spustit.
Pro tvorbu expression trees můžeme využít statických továrních metody třídy Expression ze jmenného prostoru System.Linq.Expressions. V tomto jmenném prostoru jsou také typy představující jednotlivé listy stromu, jako například ParameterExpression pro vyjádření vstupního parametru nebo MethodCallExpression pro vyjádření volání metody. Následující příklad ukazuje sestavení expression tree představující lambda výraz pro sečtení dvou celých čísel pomocí rozhraní třídy Expression.
//tvorba parametru pro lambda vyraz
ParameterExpression par1 = Expression.Parameter(typeof(int), "a");
ParameterExpression par2 = Expression.Parameter(typeof(int), "b");
//tvorba lambda vyrazu pro secteni dvou cisel
Expression<Func<int, int, int>> addExpression =
Expression.Lambda<Func<int, int, int>>
(Expression.Add(par1, par2), new ParameterExpression[] { par1, par2 });
Velmi dobrou zprávou je, že existuje i pohodlnější způsob tvorby expression tree a to využitím služeb kompilátoru, který je schopen na základě instance delegáta, jež je vytvožen například pomocí lambda výrazu, vytvořit expression tree za nás. Kompilátor totiž v případě použití typu Expression<T>, kde T je typ delegáta, rozpozná, že nemá s kódem zacházet jako se spustitelným, ale že má vytvořit expression tree.
Expression<Func<int, int, int>> addExpression = (a, b) => a + b;
Pokud máme k dispozici nějaký expression tree, můžeme jej pomoci jeho veřejných vlastností snadno analyzovat a zjistit tak všechny informace o daném výrazu. Následující kód jednoduše zobrazí parametry a tělo výrazu, nicméně možnosti analýzy expression trees jdou mnohem dále, takže příklad berte pouze jakomalou inspiraci.
Expression<Func<int, int, int>> exprTree = (a, b) => a + b;
Console.WriteLine("Parameters :");
//vypsani vstupnich argumentu a jejich typu
foreach (ParameterExpression parExpr in exprTree.Parameters)
{
Console.WriteLine("{0} - {1}", parExpr.Name, parExpr.Type);
}
//vypsani tela vyrazu
Console.WriteLine("Body : {0} ({1})", exprTree.Body.NodeType, exprTree.Body);
Jak bylo zmíněno výše, tak expression tree může být nejen analyzován, ale také zkompilován do spustitelné podoby a spuštěn jako standardní instance delegáta. To je jednoduše možné pomocí metody Compile.
Expression<Func<int, int, int>> addExpression = (a, b) => a + b;
//spusteni zkompilovaneho delegata
Console.WriteLine(addExpression.Compile()(2, 3));
LINQ
Jednou z nejdůležitějších novinek v .NET frameworku 3.5 je bezesporu projekt LINQ, který umožňuje implementovat dotazy nad datovými zdroji přímo na úrovni .NET jazyku. Datovým zdrojem v tomto případě může být cokoliv, pro co existuje implementovaný provider, například obyčejná kolekce, SQL databáze nebo XML dokument. Jednou z největších výhod vyplývající z toho, že schopnost provádět datové dotazy je integrována do .NET programovacího jazyku, je kontrola správnosti dotazu již v době kompilace.Viditelným výsledkem této integrované podpory pro dotazování do C# 3.0 je nová paleta klíčových slov, které dotazování maximálně ulehčují. Jedná se o klíčová slova select, from, where, join, orderby, group, into a let.
Tato nová vlastnost například umožňuje naprosto nový způsob vyhledávání prvků v nějaké kolekci, kde místo toho, abychom pomocí vlastního algoritmu prvky vyhledávali, seskupovali a třídili, využijeme těchto nových klíčových slov podobným stylem jako v jazyku SQL. Takže vyhledání všech zákazníků, kteří mají v příjmení obsaženo písmeno „s“ a seřazení výsledků podle příjmení může být naimplementováno například takto:
List<customer> customers = new List<customer>(){
new Customer(){FirstName = "Petr", Surname = "Pus"},
new Customer(){FirstName = "Jaroslav", Surname = "Kavis"},
new Customer(){FirstName = "Jan", Surname = "Novak"}
};
//vyhledani vsech lidi, kteri maji v prijmeni 's'
IEnumerable<customer> result = from cust in customers
where cust.Surname.Contains("s")
orderby cust.Surname
select cust;
foreach (Customer cust in result)
{
Console.WriteLine(cust.Surname);
}
Jak můžeme vidět, tak podpora pro dotazování integrovaná přímo do programovacího jazyku přináší výrazně jednodušší, efektivnější a přehlednější implementaci dotazování nad datovými zdroji. LINQ je velmi obsáhlé a zajímavé téma, takže mu v budoucnu určitě věnuji vlastní článek.
Závěr
Novinky v jazyku C# 3.0 přinášejí řadu syntaktických cukrátek, které nejen, že umožňují efektivnějí řešit typické implementační problémy pomocí vlastností jakými jsou automaticky generované vlastnosti či inicializátory objektů a kolekcí, ale také například díky lambda výrazům nabízí mnohem stručnější zápis anonymních metod. Pomocí expression trees je nově možné pracovat s kódem jako z daty a vlastní kapitolou, díky které bylo mnoho novinek do C# 3.0 implementováno, je projekt LINQ, který umožňuje provádět dotazy nad různými datovými zdroji přímo na úrovni jazyka C# s podporou odhalení chyb v době kompilace.