Szumma #095 – 2018 6. hét

Az elmúlt hét legfontosabb/hasznosabb linkjeit szedtem össze.

“Szumma #095 – 2018 6. hét” olvasásának folytatása

Reklámok

Generikus metódus hívása futásidejű típusparaméterrel

//NOTE: a devportal OneTime bindinggal húzza be a postokat. 🙂 Mivel elég gyakori, hogy a publikálás után veszem észre, hogy összekevertem a télapó és a gestapo szavakat, hiába javítom, a devportalon nem változik. Javaslom, hogy aki neadjbéla arra vetemedik, hogy szerény kis agymenéseimmel múlatja az időt, kattanjon a postok alján lévő wordpress linkre, ott lesz a friss változat. Danke sün!

A generikus metódusokat/típusokat szeretjük, mert nagy flexibilitást, jobb teljesítményt és nagyobb típusbiztonságot adnak. Azonban semmi sincs ingyen – van, ahol nagyon meg tudja keseríteni az életünket az átállás generikus metódusokra. Vegyük a következő példát…

Jussunk el a problémáig!

Adott egy Arena nevű osztály, mely rendelkezik egy Harc nevű metódussal, amely két típusparaméterrel rendelkezik, továbbá megköti, hogy ezen T1 és T2 típusoknak kell, hogy legyen paramétermentes konstruktoruk, és meg kell valósítaniuk az IHarcképes interfészt. C#-ul mondva:

class Arena
{
    public string Harc<T1, T2>()
        where T1 : IHarcképes, new()
        where T2 : IHarcképes, new()
    {
        T1 t1 = new T1();
        T2 t2 = new T2();

        int delta = t1.GetHarcérték() - t2.GetHarcérték();
        if (delta > 0) return typeof(T1).Name + " az erősebb.";
        if (delta < 0) return typeof(T2).Name + " az erősebb.";
        return "Döntetlen.";
    }
}

interface IHarcképes
{
    int GetHarcérték();
}

Ahogy látható, a metódus létrehoz két objektumot, és az interfészmetódus segítségével megvizsgálja, melyik nyerne egy harcban. Tegyük fel, hogy sok osztályunk van, amelyeknek harcolniuk kell majd. Emellett szeretnénk ezeket párokba rendezni, mégpedig olyan módon, hogy a párokba rendezés dinamikusan történhessen, tehát ne legyen előre kőbe vésve. Néhány osztály, amivel el tudunk indulni:

class Katona : IHarcképes
{
    public int GetHarcérték() { return 1; }
}

class Ellenség : IHarcképes
{
    public int GetHarcérték() { return 2; }
}

class Lábas : IHarcképes
{
    public int GetHarcérték() { return 1; }
}

class Ellenlábas : IHarcképes
{
    public int GetHarcérték() { return 2; }
}

Érezhető, hogy a katona ellensége az ellenség lesz, illetve beigazolódik az is, hogy a konyhában minden lábasnak van egy ellenlábasa. (Maksa ftw!) Normál esetben a harcértéket valamiből kiszámolnánk; ehelyett itt sony-típusú randomszámokat adunk vissza. A párokba rendezést bízzuk egy osztályra:

class HarcpárTároló
{
    Dictionary<Type, Type> pairs = new Dictionary<Type, Type>();

    public HarcpárTároló()
    {
        pairs.Add(typeof(Katona), typeof(Ellenség));
        pairs.Add(typeof(Lábas), typeof(Ellenlábas));
    }

    public Type GetPairTypeFromType(Type t)
    {
        return pairs[t];
    }
}

Ebből gyárthatunk több leszármazottat is, és mindegyik a saját kis logikája alapján pároztathatja párosíthatja a típusokat. Eddig szép és jó. De hogyan hívjuk meg a Harc metódust? Próbáljuk meg valahogy…

Arena arena = new Arena();
HarcpárTároló hpt = new HarcpárTároló();

object egyik = new Lábas();
Type masik = hpt.GetPairTypeFromType(egyik.GetType());

arena.Harc<egyik.GetType(), masik>();  //compile-time error

Ez ugye fájdalmasan csúnya. Olyannyira, hogy le se fordul. (Sőt, ha valaki megpróbálta begépelni, érezhette, hogy az IntelliSense mindent megtesz, hogy akadályozza.) A probléma ugyebár az, hogy a generikus metódus meghívásához fordítási időben rendelkezésre kellene állnia a típusparamétereknek. Mit lehet tenni?

Tükrözés!

Talán ismerős lehet, hogy ismeretlen típusoknál a reflection segítségével elérhetünk ismeretlen metódusokat. De természetesen arra is képes a rendszer, hogy ismert típusok ismert metódusait adja vissza. 🙂

A metódus kiválasztása után csak plusz egy lépést kell elvégezni ahhoz, hogy működjön a dolog. Mivel itt generikus metódusról van szó, a metódus kiválasztása csupán a “nyílt” metódust adja vissza. Ez a generikus sablon, amit tulajdonképpen megírtunk C#-ban. Ahhoz, hogy ezt két konkrét típussal használhassuk, rá kell venni a fordítót, hogy készítse el a metódus nem-generikus változatát. (Ha a nyílt metódust hívnánk, egy InvalidOperationExceptiont kapnánk, ami arról tájékoztatna, hogy ez egy nyílt generikus metódust, és mint ilyen, meghívhatatlan. :)) Végül pedig ugyanúgy hívhatjuk a metódust, mintha bármilyen nem-generikus metódust hívnánk reflectionön keresztül: a zárt metódus már nem generikus.

MethodInfo openMeth = arena.GetType().GetMethod("Harc");
MethodInfo closedMeth = openMeth.MakeGenericMethod(
    new[] { egyik.GetType(), masik });
closedMeth.Invoke(arena, null);

Nem olyan szép, mintha meglennének előre a típusok, de ez így képes feldolgozni, ha csak futásidőben kapjuk meg őket. Készen vagyunk.

Lehetne ezt máshogy?

Természetesen: senki nem kötelez minket arra, hogy generikus metódust használjunk ott, ahol valami miatt kényelmetlen. A Harc metódust megírhatjuk típusmentesre is. Picit bonyolultabb lesz.

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English
public string HarcNonGeneric(Type t_t1, Type t_t2)
{
    if (!t_t1.GetInterfaces().Contains(typeof(IHarcképes)) ||
        !t_t2.GetInterfaces().Contains(typeof(IHarcképes)))
    {
        throw new InvalidOperationException(
            "Mindkét paramétertípus meg kell valósítsa az IHarcképes interfészt.");
    }

    IHarcképes t1 = (IHarcképes)Activator.CreateInstance(t_t1);
    IHarcképes t2 = (IHarcképes)Activator.CreateInstance(t_t2);

    int delta = t1.GetHarcérték() - t2.GetHarcérték();
    if (delta > 0) return t_t1.Name + " az erősebb.";
    if (delta < 0) return t_t2.Name + " az erősebb.";
    return "Döntetlen.";
}

Jobb ez a metódus? Röviden: nem, a generikus mindig szebb. 🙂 De ha objektívebben nézzük: le kell mérni, melyik mit tud.

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

Egymillió hívásnál…

Lefuttattam a generikus és a nemgenerikus változat meghívását 1 milliószor, és a kiváncsiság kedvéért a nemgenerikusat reflectionön keresztül is. Ha csak a metódushívásokat nézzük, előre látható, hogy a generikus változat szanaszét veri a másik kettőt. (ms, az alacsonyabb érték a jobb.)

Generic, reflection: 12091
NonGeneric, reflection: 20399
NonGeneric, nonreflection: 16904

Ha mindig, vagy legalábbis sokszor kell ugyanazokkal a típusokkal dolgozunk, ugyanarra a típusra lefuttatnunk a metódust, akkor tehát megéri generikusan implementálni. A metódus elkészítésének overheadje csak egy egyszeri overhead. (A teljesség kedvéért megjegyzem, hogy a gép egyéb dolgaitól függően volt egy 1-3 mp-es kilengés a futtatások között, de az arányok mindig így alakultak.)

Mi van azonban, ha egy típusra (típuspárra) csak egyszer futtatjuk le, és megyünk is tovább a következő típusra/típuspárra?

Generic, reflection: 25157
NonGeneric, reflection: 22699
NonGeneric, nonreflection: 16916

Mivel minden egyes metódushívás előtt le kell kérni a metódust, és legyártatni a zárt változatát, megfordult a dolog: a nem-generikus metódus itt minden hátránya ellenére is sokkal gyorsabb, mint a generikus. Tehát ilyen esetekre jobban járunk, ha nem generikusat készítünk, hanem maradunk a “jól bevált” C# 1.0-s módszernél. Persze most Activatorral dolgoztunk – ha ennek a tudása nem elég. és ConstructorInfokon keresztül kell dolgozni, az okozhat némi lassulást a nemgenerikus változatban.

Mára ennyi, most pedig vissza kódolni.

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

ObjectDumper – fordítsuk ki Caerbannog gyilkos nyulát!

Akkor így a nyúl-, tojás- és sonkaünnep (szigorúan nem nyúltojás- és nyúlsonka!) vasárnapján megosztok egy pici, hasznosnak gondolt kis osztályt.

Az ObjectDumpernek elkeresztelt kis valami arra lesz képes, hogy egy objektum belső állapotát – azt is, ami tényleg belső, privát – egy kellő mértékben formázott stringként adja a külvilág tudtára. Elsősorban hibakeresésre szánjuk az osztályt: nem kell a locals ablakban turkálni, csak hívni egy Dump()-ot egy objektumon, és kész is. A munka során megismerkedünk néhány reflectionnel kapcsolatos dologgal.

Kezdjük szokás szerint egy testreszabott osztállyal, ami az állatorvosi ló szerepét tölti majd be. A jeles alkalomra való tekintettel a ló most legyen nyúl.

A nyúl

Mindössze egy pici osztályra lesz szükség, ami itt-ott “furán néz ki”. Hogy megfelelően tesztelhető legyen az ObjectDumper, olyan osztályra lesz szükség, ami pl. rendelkezik olyan tulajdonsággal, ami csak lekérdezhető, vagy épp csak beállítható. Rakjuk össze a nyulat!

public class Rabbit
{
    public string Name { get; private set; }
    public DateTime BirthDate { get; set; }
    private int killedKnights;
    public int KilledKnights
    {
        set { killedKnights = value; }
    }
    private string favColor;
    private string FavouriteColor
    {
        get { return favColor; }
    }
    public Rabbit(string name, DateTime bDate
        int killedKnights, string favColor)
    {
        this.Name = name;
        this.BirthDate = bDate;
        this.killedKnights = killedKnights;
        this.favColor = favColor;
    }
}

Látható, hogy a nyuszinak lesz egy neve, egy születési dátuma, illetve beállítható lesz rajta, hogy eddig hány lovagot ölt meg. Viszont ez kívülről nem kérdezhető le, így egy lovag, amikor először meglátja, nem fogja tudni, mivel áll szemben. Ezen felül pedig lesz egy kedvenc színe is a nyúlnak, azonban ez nem fontos, hacsak nem akar hidakon átkelni.

A nyúlon túl

Mielőtt még nekilátnánk az ObjectDumpernek, szükségünk lesz néhány enumerációra, amivel majd szabályozhatjuk a működést. Elsőként: mezőket vagy tulajdonságokat jelenítsünk meg? Aztán: publikus vagy nem publikus tagokkal dolgozzunk? Végül: milyen plusz információt jelenítsünk meg? (Például a tag típusa, illetve az, hogy egy tulajdonság milyen accessorökkel rendelkezik.)

[Flags]
public enum DumpTarget : byte
{
    Fields = 0x01,
    Properties = 0x02
}
[Flags]
public enum DumpVisibility : byte
{
    Private = 0x01,
    Public = 0x02
}
[Flags]
public enum DumpAdditionalInfo : byte
{
    None = 0x00,
    Type = 0x01,
    PropAccessors = 0x02
}

Egy kis magyarázat: A FlagsAttribute jelzi, hogy itt egy bitflagről van szó, vagyis össze akarjuk majd kapcsolni az egyes értékeit az adott enumerációnak. (Tehát pl egyszerre akarunk publikus és privát tagokat megjeleníteni.) Ez most még annyira nem lényeges, de ha később szeretnénk kiterjeszteni az enumerációt plusz tagokkal, jól jöhet.

A byte, mint “ősosztály” itt azt jelenti, hogy az enumeráció mögötti tároló egy byte legyen. Max. 256 értéket vehet tehát fel az enum, cserébe negyedannyi helyen elfér, mint az alapértelmezett integer.

Az enumok tagjai után lévő hexaszámok csak a címke mögötti konkrét értéket jelzik.

Mehetünk tovább – kezdjük el fejleszteni a lényeget. 🙂

Minden objektum hozzon magával még egy metódust!

Alapvetően nem a legjobb dolog, ha a System.Objectre rakunk bővítőfüggvényt (hogy miért, arról majd később), de megtehetjük, és nekünk most logikailag ez a legmegfelelőbb. A bővítőfüggvények (extension methods) a C# 3.0-ban jelentek meg. Segítségükkel bármilyen osztályra felrakhatunk  plusz metódusokat – látszólag. Valójában az ilyen módon az osztályra rakott metódus továbbra sem lesz ténylegesen az osztály része; pusztán a C# engedi nekünk, hogy szintaktikailag egy példányon meghívjuk, de a háttérben a hívás nem a példányon történik, hanem a bővítőfüggvényt tartalmazó osztályon.

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

Elég a szóból, hozzuk létre az osztályt, benne a függvénnyel. Rögtön adjuk meg a paramétereket is: az első nyilvánvalóan az objektum, aminek a tartalmát dumpolni akarjuk, utána következzen egy TextWriter, ami a kimenetet tárolja majd, majd jöhet a 3 fentebb készített enumeráció argumentumként való fogadása. A paraméterlista összeállításakor izomból felhasználjuk a C# 4.0 opcionális/nevesített paraméterezési képességeit, vagyis ahol lehet, rögtön default értéket adunk a paramétereknek.

public static class ObjectDumper
{
    public static void Dump(this object o,
        TextWriter output = null,
        DumpTarget targets = DumpTarget.Fields | DumpTarget.Properties,
        DumpVisibility visibility = DumpVisibility.Public,
        DumpAdditionalInfo additionalInfo = DumpAdditionalInfo.None)
        {
        }
}
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish
AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

Tekintve, hogy bővítőfüggvényről van szó, egy public static osztályban public static módosítókkal kell létrehozni a metódust. A kódot picit később írjuk meg.

AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDetect languageDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddishAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBulgarianCatalanChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekHaitian CreoleHebrewHindiHungarianIcelandicIndonesianIrishItalianJapaneseKoreanLatinLatvianLithuanianMacedonianMalayMalteseNorwegianPersianPolishPortugueseRomanianRussianSerbianSlovakSlovenianSpanishSwahiliSwedishThaiTurkishUkrainianUrduVietnameseWelshYiddish

English (auto-detected) » English

Hogy a formázást szeretnénk egyszerűen megoldani, érdemes lesz ezt is az osztály szintjén definiálni, így egy központi helyen lesz állítható. Csupán néhány stringet kell létrehozni:

public static string FieldsHeader = "---Fields";
public static string PropertiesHeader = "---Properties";
public static string PublicHeader = "\t---public";
public static string PrivateHeader = "\t---nonpublic";
public static string IndentChars = "\t";
public static string EqualitySign = " = ";

Most, hogy megvagyunk a körítéssel, írjuk meg a Dump metódus logikáját!

Először is ellenőrizzük, hogy nem null-referencián hívták-e a metódust. Ha igen, a metódus dobjon egy ArgumentNullExceptiont. Mindenki döntse el maga, hogy ez a legmegfelelőbb kivétel-e ebben a szituációban. Mivel bővítőfüggvényről van szó, amelynek első paramétere okozta a hibát, hajlok arra, hogy inkább ezt javasoljam, mint egy NullReferenceExceptiont.

Ha nem nullt kaptunk, kérdezzük le a kapott objektum típusát. A típusinformációk segítségével fogjuk felderíteni az objektum belsejét.

Ezután jöhet a StringBuilder létrehozása. Mivel nem akarjuk, hogy fél mega szemét maradjon a memóriában a metódus lefutása után, jobb lesz, ha nem stringeket konkatenálunk, hanem egy StringBuilderrel rakjuk össze a kimenetet. Ebbe az objektumba rögtön írjuk is be, hogy milyen típusú objektumot “dumpolunk” éppen.

if (o == null) throw new ArgumentNullException(
    "o", "Dump cannot be called on a null reference.");
Type t = o.GetType();
StringBuilder sb = new StringBuilder("Dumping ");
sb.AppendLine(t.Name);

Ezek után jöhet a mezők és a tulajdonságok felsorolása attól függően, hogy a hívó mit kért. Ezt ugye a targets paraméter alapján tudjuk eldönteni.

Tekintve, hogy lehetséges, hogy több értéket is megjelölt a hívó, nem tudunk egyszerű értékösszehasonlítást végezni, helyette a HasFlag metódust kell meghívni. Ugyanezt kell elvégeznünk a visibility paraméter vizsgálatakor is.

Ha megvolt a vizsgálat, indulhat a foreachelés egy megfelelően lekérdezett FieldInfo tömbön. A megfelelően lekérdezett itt azt jelenti, hogy a típusobjektumon meghívott GetFields metódusnak a BindingFlags enumeráción keresztül megmondjuk, hogy pontosan milyen láthatóságú tagokat kérünk. A munka oroszlánrészét, vagyis a kiíratást egy privát metódus oldja majd meg, melyet a következő lépésben definiálunk.

Mivel a tulajdonságok lekérdezésének ugyanez a logikája, csupán az információs osztályok típusa, illetve a belső logika változik, erre nem térek ki külön.

if (targets.HasFlag(DumpTarget.Fields))
{
    sb.AppendLine(FieldsHeader);
    if (visibility.HasFlag(DumpVisibility.Private))
    {
        sb.AppendLine(PrivateHeader);
        foreach (FieldInfo fi in t.GetFields(
            BindingFlags.NonPublic | BindingFlags.Instance))
        {
            getFieldString(sb, o, fi, additionalInfo);
        }
    }
    if (visibility.HasFlag(DumpVisibility.Public))
    {
        sb.AppendLine(PublicHeader);
        foreach (FieldInfo fi in t.GetFields(
            BindingFlags.Public | BindingFlags.Instance))
        {
            getFieldString(sb, o, fi, additionalInfo);
        }
    }
}

Kész is. A fenti elágazást – a megfelelő helyeken józan paraszti ész alapján módosítva – lemásoljuk, és ezzel lekérdezzük a tulajdonságokat is.

Már csak annyi van hátra a sok útelágazódás után, hogy a metódus végére telepakolt StringBuilder tartalmát a kapott TextWriter objektumba pakoljuk. Tekintve, hogy az output default értéke null, ezt le kell ellenőriznünk, mert valószínű, hogy lesz, aki így használja. Ha nullt kaptunk, kimenetnek megadjuk a konzolt. Ezek után pedig kiírjuk az sb változó tartalmát.

if (output == null) output = Console.Out;
output.WriteLine(sb.ToString());

A tartalom lekérdezése

A következő lépés a getFieldString metódus, illetve párja, a getPropertyString metódus megírása lesz. (A név egy kicsit megtévesztő lehet: nem stringgel térnek vissza, hanem az eredeti StringBuildert töltik fel további adatokkal.)

private static void getFieldString(StringBuilder output,
    object dumpedObj, FieldInfo fInfo, DumpAdditionalInfo adtnl)
{
    output.Append(IndentChars);
    if (adtnl.HasFlag(DumpAdditionalInfo.Type))
        output.AppendFormat("[{0}]\t", fInfo.FieldType);
    output.AppendFormat("{0}{1}{2}{3}", fInfo.Name, EqualitySign,
        fInfo.GetValue(dumpedObj) ?? "{null}", Environment.NewLine);
}

Mit csinálunk itt?

Először is behúzzuk a sört. Csak, hogy jobb legyen a kedv. (Ja, húsvét van?) Aztán a sort, hogy olvashatóbb legyen a kimenet.

Ezután, ha a a hívó kérte, a FieldInfo FieldType tulajdonságának segítségével kiírjuk a mező típusát. Ez opcionális.

Utolsó körben pedig jöhet lényeg: szépen formázva kiírjuk a mező nevét, majd tartalmát, majd törjük a sort. Közben annyira kell még ügyelnünk, hogy ha esetleg a mező tartalma null lenne, írjunk ki helyette egy normál stringet. (null coalescing op ftw!)

Nagyon hasonló, amit a tulajdonságokkal el kell játszani, de van néhány lényeges különbség.

A tulajdonságok meglehetősen összetettek. Gyakorlatilag 1 vagy 2 metódusunk van – elképzelhető olyan szituáció is, amikor csak set van, vagyis a tulajdonság kívülről nem kérdezhető le, csak beállítható. Ráadásul ezek a metódusok nem feltétlenül publikusak; saját láthatóságuk van. Ezen felül akár még kivételt is dobhatnak…

Lássuk, mit tehetünk!

private static void getPropertyString(StringBuilder output,
    object dumpedObj, PropertyInfo pInfo, DumpAdditionalInfo adtnl)
{
    output.Append(IndentChars);
    if (adtnl.HasFlag(DumpAdditionalInfo.Type))
        output.AppendFormat("[{0}]\t", pInfo.PropertyType);
    output.AppendFormat("{0}{1}{2}", pInfo.Name, EqualitySign,
        pInfo.GetGetMethod() != null ? pInfo.GetValue(dumpedObj, null)
        ?? "{null}" : "{no public get accessor");
    if (adtnl.HasFlag(DumpAdditionalInfo.PropAccessors))
    {
        output.AppendFormat(" [{0}{1}]",
            pInfo.GetGetMethod(true) != null ? "get;" : string.Empty,
            pInfo.GetSetMethod(true) != null ? "set;" : string.Empty);
    }
    output.AppendLine();
}

Csak egy kicsit bonyolultabb, na. 🙂 Az elejére ne vesztegessünk szót, nem sok változás van a korábbiakhoz képest.

Az érték kiírásánál már figyelnünk kell arra, hogy egyáltalán lekérdezhetjük-e az értéket. Ha nincs get accessor, meg sem próbáljuk.

Ezek után, ha a hívó kérte, kiírjuk, hogy milyen accessorökkel rendelkezik a tulajdonság. A PropertyInfo GetGetMethod és GetSetMethod metódusának lekérdezése éppen megfelelő lesz erre.

Készen vagyunk, teszteljük le, mit alkottunk!

“Egy nyúlpörkölt rendel.”

Készítsünk egy nyulat, és hívjuk meg rajta a metódust!

Rabbit bugs = new Rabbit("Killer Rabbit of Caerbannog",
    new DateTime(1975, 4, 3), 90, null);
bugs.Dump(visibility: DumpVisibility.Private | DumpVisibility.Public,
additionalInfo: DumpAdditionalInfo.Type |
DumpAdditionalInfo.PropAccessors);

Futtassuk meg, és nézzük meg, mit rejtett a nyúl belseje!

Dumping Rabbit
---Fields
        ---nonpublic
        [System.Int32]  killedKnights = 90
        [System.String] favColor = {null}
        [System.String] k__BackingField = Killer Rabbit of Caerbannog
        [System.DateTime]       k__BackingField = 1975.04.03. 0:00:00
        ---public
---Properties
        ---nonpublic
        [System.String] FavouriteColor = {no public get accessor} [get;]
        ---public
        [System.String] Name = Killer Rabbit of Caerbannog [get;set;]
        [System.DateTime]       BirthDate = 1975.04.03. 0:00:00 [get;set;]
        [System.Int32]  KilledKnights = {no public get accessor} [set;]

Egész jó. Vajon hol vannak az easter eggek (=bugok)? 🙂 (Aki talál, kap csokitojást. Viccen kívül.)

Update: elfelejtettem letölthetővé tenni a kész kódot. Íme. 🙂

http://cid-0b0abf2a681faf43.office.live.com/embedicon.aspx/.Public/chevenix.wordpress.com/ObjectDumper.zip

További lehetőségek

Az osztály korántsem 100%-os, de arra bőven jó, hogy elinduljunk. Én olyan 85%-osnak mondanám, tekintve, hogy a formázás testreszabása sokkal bonyolultabb is lehetne. 🙂

Mit lehetne még beépíteni? Például a statikus tagokat abszolúte kihagytuk – elvileg ez nem gond, mivel mi egy konkrét példányt dumpolunk, de ha érdekel valakit, esetleg ezt is bele lehet rakni. Aztán a tulajdonságoknál lehetne még bonyolítani a dolgot jócskán. Valamint például beépíthetnénk elvárt/kizárt értékek megadását. Vagy a tagok attribútumait is beolvashatnánk. Jelezhetnénk, hogy az adott tulajdonságot ezen a típuson definiálták, vagy örökölte.

A végtelenségig lehetne tuningolni ezt a metódust és osztályt. Viszont még van pár másik program, amire rá kell néznem, és amelyekért talán pénz is jön, úgyhogy tartva magam ahhoz, ami lassan itt mottóvá válik…

…most vissza kódolni. 🙂