Nagy, átláthatatlan alkalmazás belső állapotának felderítését megönnyítheti egy jó debugger. A Visual Studio debuggere kifejezetten a könnyen használható kategóriában van, de ez a gyakori előny néha kegyetlenül meg tudja keseríteni az ember életét.
Tegyük fel, hogy munkánk során egy idegen dll-lel kell dolgoznunk (azért idegen, mert hibás), melyben található egy Adat nevű osztály:
public class Adat
{
public byte[] Content
public int ContentLength
}
Állítólag egy adatbázisból töltöget be tartalmat amikor szükséges. Elég egyszerű: van egy Content tulajdonság a tartalom elérésére és egy ContentLength tulajdonság a tartalom méretének lekérdezésére.
Próbáljuk ki. Mondjuk megpéldányosítom és kiírom az adat hosszát:
static void Main( string[] args )
{
try
{
Adat adat = new Adat();
// ...
Console.WriteLine( "A tartalom hossza:" );
Console.WriteLine( adat.ContentLength );
}
catch( Exception e )
{
Console.Error.WriteLine( e );
}
Console.ReadLine();
}
Nem túl bonyolult kód, elsőre futnia kéne, nyomom az F5-öt:
Elszállt az alkalmazás, állítólag a 16. sorban. Akkor nézzük meg melyik is a 16. sor?
Console.WriteLine( adat.ContentLength );
Ez nem egy bonyolult sor, valószínűleg az adat lesz null, vagy az adat.ContentLength repül el. Semmi gond, megnézhetem a Visual Studio debuggerével: beteszek egy töréspontot, és megnézem a watch ablakban, hogy mi a null.
Azt írja, hogy az adat szépen példányosítva van, az adat.ContentLength pedig 3. Akkor inkább továbbengeden, fusson le nyugodtan.
Kicsit kellemetlen, de ezúttal lefutott. Megismétlem még néhányszor a fenti lépéseket, és az eredmény mindig ugyanaz: Törésponttal, watch ablakkal nincs hiba, egyébként NullReferenceException.
A fenti példán érezni, hogy csak most találtam ki. Mert nem merem megmondani, hogy milyen fejlesztés közben jött elő. Akkor rengeteg forráskódunk volt, és valami magic-re gyanakodtunk (tudod: amikor páratlanszor mész át a hídon, de mégis mindig ugyanazon az oldalon ébredsz), végül elkértük az 'Adat' osztály forrákódját:
public class Adat
{
private byte[] _content;
public byte[] Content
{
get
{
if( this._content == null )
this._content = LoadContent();
return this._content;
}
}
public int ContentLength
{
get
{
return this._content.Length;
}
}
private byte[] LoadContent()
{
// tartalom betoltese peldaul adatbazisbol
return new byte[] { 0x00, 0x00, 0x00 };
}
}
Áhá. Szóval ez egy lusta betöltést implementáló őrület. Van a _content tagváltozó, aminek akkor töltik fel a tartalmát, ha megnézed a Content tulajdonságot. Ez egész jó ötlet, különösen, hogy a _content private. Tehát a tartalom elérésekor nem lehet probléma.
De mi a helyzet a ContentLength tulajdonsággal? Ez közvetlenül a _content tagváltozó Length tulajdonságával tér vissza. Amikor a _content nem null! Nézzük csak vissza a programomat:
Adat adat = new Adat();
// ...
Console.WriteLine( "A tartalom hossza:" );
Console.WriteLine( adat.ContentLength );
Szóval úgy akartam kiírni a tartalom hosszát, hogy előtte nem nyúltam hozzá a Content tulajdonsághoz, tehát az Adat példányban a _content inicializálatlan volt, és NullReferenceException-nel elszállt.
Szuper, de akkor miért nem jött elő a hiba Visual Studio debug + watch kombinációval? Mert a watch ablakban megnéztük az adat változót, és a tulajdonságait:
Ahhoz, hogy a Studio ki tudja írni a ContentLength tulajdonság értékét meghívta a hozzátartozó gettert. Így persze példányosodott a _content tagváltozó, és máris nem volt mi NullReferenceException-t dobjon. (Az ábrán még azt is meg lehet figyelni, hogy amikor a _content tagváltozó értékét írta ki, amihez nem kellett meghívni a gettert akkor a _content még null volt.)
Elég vicces, de az az igazság, hogy debuggolással olyan állapotba vihetjük a megfigyelt alkalmazásunkat, ahová az debugger nélkül sosem jutna el! Ebben az esetben ez azért történt meg, meg olyan tulajdonság értékére voltunk kíváncsiak, amelyhez tartozó getternek mellékhatása is volt.