Miroslav Holec
Premium

C# reflexe - praktické snippety

Miroslav Holec   21. prosince 2014  update 29. března 2016

Tento článek je již zastaralý. Článek nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

V minulosti jsem psal některé konzolové aplikace, které za běhu byly schopné pracovat s nově připojenými assemblies, načítat různé programy a nad nimy následně spouštět různé metody. Řekl bych, že jsem si na těchto aplikacích zopakoval všechny možnosti práce s reflexí. V tomto článku shrnu pár užitečných snippetů, které se při práci se C# reflection zaručeně hodí.

Co je C# reflexe

C# reflexe poskytuje celou řadu objektů a jejich operací pro práci s assemblies, moduly a typy. Hodí se v situacích, když je potřeba za běhu připojovat různé DLL knihovny, připojovat a instancovat typy, odpalovat různé metody nebo například v situacích, kdy je jen potřeba zjistit informace o určitém datovém typu. Klíčovým jmenným prostorem je System.Reflection. Většina vývojářů reflexi poprvé použije až když ji skutečně potřebuje a mnoho vývojářů reflexi používá aniž o tom ví (při nejmenším ji například implicitně používá nějaký IoC kontejner).

Nevýhody reflexe

Reflexe má v podstatě jedinou nevýhodu (z pohledu odpůrců) a to je (oprávněně) její rychlost. Zpracování kódu pomocí C# reflection je podstatně pomalejší než v případě statického kódu. Na druhou stranu reflexe nabízí celou řadu možností, které tuto jedinou nevýhodu vymazávají (když odmyslíme fakt, že jindy to zkrátka jinak než bez reflexe nejde). Reflexe je také záteží pro procesor a její použití by mělo být omezeno v případě aplikací, u kterých se předpokládá běh na skutečně nevýkonných zařízeních.

Výhody reflexe

Výhody reflexe vychází z její podstaty. Tím, že umožňuje procházet vlastnosti typů zároveň umožňuje plno užitečností.

  • připojování assemblies za běhu a práci s připojenými typy
  • procházení všech properties instance objektu (a provedení operace nad nimy - například práce s custom attributes)

Reflexe je běžně používána IoC kontejnery nebo procesy pro spouštění Unit testů. Právě u Unit testů se využívá vyhledávání tříd, které jsou dekorovány custom atributem TestClass a metod dekorovaných atributem TestMethod.

Konvence a doporučení

Ačkoliv použití C# reflexe je psaní C# kódu jako každého jiného, doporučuji za sebe důsledně dbát na názvy proměnných a uvádění typů (nepoužívat klíčové slovo var). Při reflexi se pracuje s typy, vlastnostmi typů, atributy vlastností typů nebo s objekty a je maximálně důležité zachovat v kódu pořádek. Přesto, že jsem odpůrcem zbytečného komentování kódu, v případě C# reflection bych vzhledem k úrovni abstrakce doporučil udělat výjimku a zároveň se snažit kód maximálně separovat do miniaturních a dobře pojmenovanných metod.

Příklady pro práci

Základní informace o datovém typu si zkusíme získat z třídy Article s následující strukturou.

public class Article : IArticle
{
    [Required]
    public string Title { get; set; }
    public DateTime Created { get; set; }
    public int Version { get; set; }

    public Article()
    {

    }

    public Article(string title, DateTime created, int version)
    {
        Title = title;
        Created = created;
        Version = version;
    }
}

public interface IArticle
{
    string Title { get; }
    DateTime Created { get; }
    DateTime? Modified { get; }
    int Version { get; }

    void Update(string title);
}

Není to nic extra ale pro názornou ukázku to bude stačit. Teď se konečně podíváme na samotné možnosti reflexe.

Informace o typech

// zakladem je ziskat informace o typu, se kterym pracujeme
Type articleType = typeof (Article);

// obcas se nam muze hodit name (Article)
string articleTypeName = articleType.Name;

// nebo název vcetne namespacu (ConsolerExampler.Examples.Reflection)
string articleTypeFullName = articleType.FullName;

// mohl bych chtit informaci o jedne property
PropertyInfo articleTitleProperty = articleType.GetProperty("Title");

// nactu si uplne vsechny properties
PropertyInfo[] articleTypeProperties = articleType.GetProperties();

// nasledne je projit
foreach (var articlePropertyInfo in articleTypeProperties)
{
    // a opet se muzu podivat na nazvy properties ("Title", "Created" atd...)
    string articlePropertyName = articlePropertyInfo.Name;

    // toto jsou customattributes, napriklad by tu mohly byt Data Annotations pouzivane EF Code First
    IEnumerable<CustomAttributeData> articlePropertyCustomAttributes = articlePropertyInfo.CustomAttributes;
    foreach (var articlePropertyCustomAttribute in articlePropertyCustomAttributes)
    {
        // v mem prikladu do tohoto cyklu spadnu pri zkoumani property Title, protoze ma
        // datovou anotaci [Required]

        if (articlePropertyCustomAttribute.AttributeType == typeof (RequiredAttribute))
        {
            // a sem to spadne take, protoze typ anotace je skutece Required
        }
    }
}

// jeste jsme zapomneli na Author, ktery neni property ale member, muzeme ho zkusit najit takto
MemberInfo[] authorMember = articleType.GetMember("Author");

// muzeme se podivat na prvni member (vime ze tam je) a bude typu field
if (authorMember[0].MemberType == MemberTypes.Field)
{
}

// takto se dostaneme k metode update
MethodInfo authorUpdateMethod = articleType.GetMethod("Update");

// nebo si vylistujeme vsechny... nedostaneme vsak jen ty nami explicitne vytvorene (jsou mezi nimi napiklad i getter metody)
MethodInfo[] authorMethods = articleType.GetMethods();

// pomuzeme si linqem a vyloucime implicitne vytvorene metody
// takze zbyde Update a metody navesene na typu object
foreach (var authorMethod in authorMethods.Where(x => !x.IsSpecialName))
{
}

Instance

Teď už se nám bude hodit pracovat s instancí. Pro ukázku si představte, že máme dvě třídy, které realizují jeden společný interface. Chceme tyto třídy instancovat třeba i za běhu celé aplikace. Tzn.: že si můžeme nakopírovat do projektu nějaké DLL, které si načteme a následně v něm najdeme potřebný typ (Article nebo Post). Pro zjednodušení jsou Article i Post zcela totožné, jen se liší názvem. (Tedy Post : IArticle)

// načtu si assembly, se kterou zrovna pracuji... mohl bych si přes Assembly.Load načíst nějakou i z disku
Assembly assembly = Assembly.GetExecutingAssembly();

// tady si jen náhodně nastavím Article nebo Post (aby byla větší sranda)
string randomTypeName = new Random().Next(1111, 9999) % 2 == 0 ? "Article" : "Post";

// najdu si opět typ Article/Post a přepínač true mi zajistí, že pokud se to nepovede, vyhodí se mi výjimka
Type articleTypeFromAssembly = assembly.GetType(string.Concat("ConsoleExampler.Examples.Reflection.", randomTypeName), true);

// vytvorim si instanci toho typu, ktery jsem si vyse vytvoril... muze to byt Post nebo Article, to ted neni podstatne
IArticle articleInstance = (IArticle)Activator.CreateInstance(articleTypeFromAssembly);

// aktualizuji clanek a jako nazev pronesu nazev z vytvoreneho typu
articleInstance.Update(string.Concat("Nazev clanku bude nazev typu: ", articleTypeFromAssembly.Name));

Tady se moc vymýšlet nedá. Dobré na tomto příkladu je, že pracujeme s interfacem. Máme tedy k dispozici všechno podstatné. Až na autora :) O tom totiž interface nic neví, přitom se jedná o field, ke kterému bychom se mohli dostat.

Pokračování příště :)