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. 🙂

Advertisements

~ Szerző: Fülöp Dávid - 2011. április 24..

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

 
%d blogger ezt kedveli: