Publikoval Michal Kočí dňa 1.12.2007 o 03:14 v kategórii .Net
Extension methods, osobne ich prekladám ako rozšrujúce metódy, sú ďaľšia nová vlastnosť jazyka c# (niektoré ďaľšie som spomenul v predchádzajúcom príspevku Lambda výrazy a iné nové vlastnosti jazyka c#). Jedná sa o metódy, vďaka ktorým môžeme k existujúcim typom pridať nové metódy (napríklad k typu string by sme mohli pridať metódu Reverse, ktorá by nám vrátila jeho hodnotu odzadu). Jedná sa o silný mechanizmu, no napriek tomu bezpečný, keďže neporušuje základné princípi OOP. V príklade si ukážeme, ako sa dá veľmi jednoducho k ostatným LINQ rozširujúcim metódam pridať aj metóda na stránkovanie.
Bezpečnosť triedy
Logicky sa natísa otázka bezpečnosti. Ak rozširujeme ľubovolnú triedu o ďaľšiu metódu, pričom z tejto triedy nededíme, nie je to nebezpečné? Veď potom nám takáto metóda môže pristupovať aj k privátnym dátam a to je predsa zlé. Nie, nemôže - teda je to bezpečné. Rozširujúce metódy sú implementované ako statické metódy na úplne inej triedy než na tej, na ktorej budú neskôr volané. Preto sú vlastne pre cieľovú triedu externými metódami a prístup majú iba k verejným dátam a metódam.
Ďaľšia bezpečnostná otázka sa natíska hneď v zápätí - ak na objekte už existuje rovnaká metóda s rovnakou signatúrou (podpisom, t.j. počtom a typom argumentov), ktorá má prednosť? Prednosť má vždy originálna metóda triedy, nikdy nie rozširujúca metóda. Preto ak by ste napríklad vytvorili rozširujúcu metódu nazvanú ToString pre triedu string, tak táto nikdy nebude zavolaná, pretože trieda string už metódu ToString má.
Takže žiaden strach, na bezpečnosť bolo dostatočne myslené.
Ako to funguje
Funguje to jednoducho, a to tak, že programátor, ktorý tvorí rozširujúcu metódu vytvára statickú triedu (triedu, ktorá obsahuje iba statické metódy). Rozširujúca metóda je potom implementovaná ako statická, pričom jej prvý parameter je vlastne inštancia triedy, ktorú budeme rozširovať a je označená klúčovým slovom this).
Na druhej strane, programátor ktorý túto metódu chce použiť ju zavolá bodkovou notáciou nad triedou, ktorá bola rozšírená. Môže samozrejme zavolať aj statickú metódu, to však nemá zmysel ju implementovať ako rozširujúcu.
Hlavné však je, čo spraví kompilátor - kompilátor nespraví nič iné ako že na pozadí vygeneruje volanie statickej metódy. Čiže ako u iných noviniek v c#, ani táto nie je revolučná a neprináša niečo vyslovene nové, jednoducho len spríjemňuje prácu programátorovi a sprehľaduje zdrojový kód. Je ozaj príjemné nemusieť hľadať ani si pamätať triedu ktorá obsahuje tieto metódy, jednoducho ich voláte nad rozšírenou triedou (respektíve jej inštanciou).
A ešte jedna poznámka. Extension metódu môžete použiť až keď do using sekcie pridáte namespace v ktorom sa trieda s metódou nachádza, nie keď si do referencií projektu pridáte assembly, ktorá triedu obsahuje.
Implementácia
Ako som v úvode sľúbil, ukážka naimplementuje rozširujúcu metódu pre stránkovanie dát. LINQ používa dve rozhrania, ktoré rozširuje o rôzne metódy a to IEnumerable a IQueryable. Prvé z nich sa používa pri LINQ to Objects (kedy sa pracuje s objektmi v pamäti) a druhé z nich pri LINQ to ADO.Net. Výhoda druhého je, že dáta sa doťahujú až ked je to potrebné a teda z databázy sa vyberajú ozaj len tie dáta, o ktoré máme záujem (ako si nižšie ukážeme).
Takže si spravíme rozširujúcu metódu Subset, ktorá bude prijímať dva parametre a to poradové číslo prvého požadovaného záznamu a počet záznamov, ktoré chceme načítať. Ako bolo spomenuté vyššie, musíme pridať ešte jeden parameter (musí to byť prvý parameter), špecifikujúci rozširovanú triedu (uvedený klúčovým slovom this). Metóda bude interne používať už existujúce rozširujúce metódy Skip a Take. Metóda Skip preskočí stanovený počet záznamov a metóda Take vezme len stanovený počet záznamov.
Implementácia triedy a oboch metód potom vyzerá nasledovne:
public static class CustomExtenders { public static IEnumerable<TSource> Subset<TSource>(this IEnumerable<TSource> source, int from, int count) { return source.Skip(from - 1).Take(count); } public static IQueryable<TSource> Subset<TSource>(this IQueryable<TSource> source, int from, int count) { return source.Skip(from - 1).Take(count); } }
Použitie
Použitie je veľmi jednoduché, odteraz môžeme nad každou triedou implementujúcou jedno z dvoch rozšírených rozhraní zavolať metódu Subset:
List<Student> students = new List<Student>() { new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int> {97, 72, 81, 60}}, new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int> {75, 84, 91, 39}}, new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int> {99, 89, 91, 95}}, new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int> {72, 81, 65, 84}}, new Student {First="Debra", Last="Garcia", ID=115, Scores= new List<int> {97, 89, 85, 82}} }; var thirdPageOfTen = students.Subset(20, 10);
Dôkaz funkčnosti pri LINQ to ADO.Net
A na záver prikladám dôkaz fukčnosti aj pri LINQ to ADO.Net. Ak metódu subset pustíte na objekte implementujúcom IQueryable, ako napríklad ja nad tabuľkou Adresa a necháte si logovať SQL príkaz posielaný do databázy, uvidíte že stránkovanie naozaj funguje a funguje na strane databázového servera. Ide teda o veľmi jednoduchú (jednoduchú z pohľadu pracnosti implementácie a nie z pohľadu funkčnosti) implementáciu serverového stránkovania. No povedzte, nezamilovali ste si LINQ?
SELECT [t1].[adresa_id] AS [Id], [t1].[ulica] AS [Ulica], [t1].[cislo] AS [Cislo], [t1].[psc] AS [PSC], [t1].[mesto] AS [Mesto] FROM ( SELECT ROW_NUMBER() OVER (ORDER BY [t0].[adresa_id], [t0].[ulica], [t0].[cislo], [t0].[psc], [t0].[mesto] ) AS [ROW_NUMBER], [t0].[adresa_id], [t0].[ulica], [t0].[cislo], [t0].[psc], [t0].[mesto] FROM [dbo].[Adresa] AS [t0] ) AS [t1] WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1 ORDER BY [t1].[ROW_NUMBER] -- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [19] -- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [10]
Ak nechceš premeškať príspevky ako je tento, sleduj ma na Twitteri, alebo ak máš RSS čítačku, môžeš sledovať môj RSS kanál.