Statické třídy, extension metody a fluent API
Tento článek byl napsán v roce 2015. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.
Na poslední přednášce Novinky v C# jsem zjistil, že mnoho vývojářů neví o možnosti využití statických tříd k psaní extension metod a fluent API. V tomto článku napíšu něco málo o statických třídách a jejich využití.
Statické třídy obecně
Statické třídy jsou v jazyce C# od verze 2.0, která byla vydána před 10 lety. Využíváme je prakticky neustále, aniž bychom si to uvědomovali. Chceme-li vytvořit statickou třídu, stačí k tomu použít modifikátor static
. Důležité logické "omezení" je, že pokud vytvoříme statickou třídu, pak všechny její členy musí být také statické. Stejně tak, pokud máme statickou metodu, pak všechny její členy musí být statické. V neposlední řadě je každá statická třída automaticky sealed
. Nejjednodušší statická třída může vypadat například takto:
public static class Calculator { static Calculator() { } public static double Sum(double x, double y) { return x + y; } }
Členy statické třídy voláme bez použití operátoru new.
public class TestMethod { double total = Calculator.Sum(5, 6); // returns 11 }
Tady je vhodné poznamenat, že voláním Calculator.Sum()
jsme nevytvořili instanci třídy. Před provedením metody Sum()
je proveden statický konstruktor Calculator()
. V tento moment se alokuje paměť pro členy statické třídy a to sice v části heapu zvané High Frequency Heap. Toť vše.
Nestatické třídy se statickými členy
Jak je to s nestatickými třídami? Kdyby třída Calculator
vypadala následovně:
public class Calculator { public int number; public Calculator() { } static Calculator() { } public static double Sum(double x, double y) { return x + y; } }
pak volání Calculator.Sum()
by sice metodu provedlo ale ani v tomto případě by se nevytvořila instance a proměnná number
by nebyla nikde v paměti alokována. To by se stalo až při vytvoření instance třídy:
Calculator calc = new Calculator();
V tomto druhém případě se provede statický konstruktor, následně konstruktor třídy a také se alokuje paměť pro proměnnou number
s výchozí hodnotou 0.
Helpery
Typické využití statických tříd je pro mnoho vývojářů vytváření helpers. Jsou to obvykle užitečné snippety kódu sdílené celým řešením, které nepracují se stavem (nebo neměly by) ale jednoduše provádí statický kód kdekoliv je potřeba. Představme si například, že chceme převádět typ png Image na pole bytů. Řešit to lze například takto:
public static class ImageHelpers { private static byte[] ImageToByteArray(Image image) { MemoryStream ms = new MemoryStream(); image.Save(ms, System.Drawing.Imaging.ImageFormat.Png); return ms.ToArray(); } }
Jedná se plně funkční řešení, které lze aplikovat následovně:
public void TestMethod() { byte[] imageAsByteArray = ImageHelpers.ImageToByteArray(image); }
Extension methods
Extension metody jsou statické metody uvnitř statických tříd, jejichž první parametr je reference na vlastní objekt, který danou metodu volá. Prakticky např.:
public static class ImageExtensions { public static byte[] ToByteArray(this Image image) { MemoryStream ms = new MemoryStream(); image.Save(ms, System.Drawing.Imaging.ImageFormat.Png); return ms.ToArray(); } }
klíčové slovo this
odkazuje na objekt, který danou extension metodu volá. Pokud je jím hodnotový typ, dostaneme jen hodnotu, něco s ní provedeme a pak opět hodnotu vrátíme. Je-li tímto objektem referenční typ, pak se vrací reference. Vrátit samozřejmě můžeme něco úplně jiného jako v příkladu výše. Využití je pak mnohem intuitivnější:
public void TestMethod() { byte[] imageAsByteArray = image.ToByteArray(); }
Můžeme k řešení přidat i parametr, aby bylo více znovupoužitelné.
public static class ImageExtensions { public static byte[] ToByteArray(this Image image, ImageFormat format) { MemoryStream ms = new MemoryStream(); image.Save(ms, format); return ms.ToArray(); } }
Použití je poté:
public void TestMethod() { byte[] imageAsByteArray = image.ToByteArray(ImageFormat.Png); }
Reference vs hodnota
Doteď jsme vraceli z extension metody hodnotu. Důležité však je, že můžeme pracovat s referenčním typem a měnit jeho vnitřní stav. Pak v podstatě není nutné ani nic vracet.
public class Human { public string Firstname { get; set; } public string Lastname { get; set; } } public static class HumanExtensions { public static void ChangeName(this Human obj, string firstname, string lastname) { obj.Firstname = firstname; obj.Lastname = lastname; } }
Použití je intuitivní:
Human human = new Human {Firstname = "Miroslav", Lastname = "Holec"}; human.ChangeName("Peter", "Falk");
Zavoláním metody nad objektem typu Human
jsme změnili jeho stav. Extension metoda přitom nic nevrací.
Fluent API
Další využití statických tříd a metod je tzv. fluent api zápis, který známe například z LINQu:
public IQueryable<Dog> GetDogs() { return dbContext.Dogs.Skip(10).Take(5); }
A to je umožněno díky tomu, že návratový typ metody je stejný jako typ uvedený u klíčového slova this
, v tomto případě zřejmě IQueryble<T>
.
Pokud se vám líbí tento zápis, není problém jej využít a udělat si vlastní užitečnou extension metodu pracující třeba nad listem.
public static class DogExtensions { public static List<Dog> OlderThan(this List<Dog> dogs, int age) { return dogs.Where(x => x.Age > age).ToList(); } public static List<Dog> YoungerThan(this List<Dog> dogs, int age) { return dogs.Where(x => x.Age < age).ToList(); } }
Tu lze pak zavolat následně:
public void TestMethod() { List<Dog> dogs = new List<Dog>(); dogs = dogs.OlderThan(10).YoungerThan(20); }
Statická třída vs. Singleton
Pokud vznikne nevyhnutelná potřeba udržovat si stav čehokoliv v paměti a tento stav sdílet v aplikaci, lze použít buď statické třídy nebo singleton. Životnost takových dat není samozřejmě nekonečná. V případě webové aplikace stav ztratíme v moment recyklace aplikačního poolu. Podstatný rozdíl mezi singletonem a statickou třídou je ve správě paměti. Singleton je ukládán na heapu (vytváří se vždy instance třídy), zatímco vlastnosti statické třídy jsou na zásobníku. Se singletonem se obecně lépe pracuje a je ze zásady testovatelnější (možnost realizace rozhraní). Statické třídy se pro uchování stavu zkrátka moc nehodí.