2009. március 12., csütörtök

System.Diagnostics.Debugger

A System.Diagnostics névtérben van egy osztály, a Debugger, aminek a segítségével a - micsoda meglepetés - debuggerrel kommunikálhatunk. A Break() metódus segítségével például "programozottan" helyezhetünk el breakpointokat a kódunkban.
Ha a programunk úgy fut, hogy a debugger már rá van akasztva - tehát F5-tel indítottuk a Studióból, vagy utólag ráakaszkodtunk a futó processzre -, a Debugger.Break() pont ugyanazt fogja csinálni, mint egy hagyományos breakpoint. Debugger nélkül (jellemzően: CTRL+F5) futtatva egy párbeszédablak fog figyelmeztetni, hogy egy user-defined breakpointhoz értünk, ami valószínűleg egy program error, és választhatunk a továbbfutás, vagy a debugolás között.

A bool Debugger.Launch() metódussal a debugger elindulását kényszeríthetjük ki: itt szintén egy párbeszédablak segítségével kell kiválasztani azt a debuggert (jellemzően CLR debuggert, már futó vagy új Visual Studio példányt), amit a hibát okozó processzhez csatolni szeretnénk. A metódus a visszatérési bool értékkel jelzi, hogy sikerült-e ez a művelet.
Az, hogy a debugger csatolva van-e már, a bool Debugger.IsAttached statikus property segítségével tudhatjuk meg.

Ennek a néhány low-level debugger API hívásnak a segítségével megkönnyíthetjuk az olyan jellegű hibáknak a felderítését, amikor valami elméletileg nem lehetséges ("ezt a metódust sose hívom null-lal!"), de a gyakorlat azt mutatja, hogy bizony mégis. Az alábbi példában, ha a paraméterként kapott id string null, megállunk, ha éppen úgy futtatjuk az alkalmazást, hogy debugolunk, egyébként pedig megdobjuk a kivételt (persze, ha a Break() után továbbmegyünk, akkor debug módban is megdobjuk, de ott már lehetőségünk van mindenféle aljasságra):

static bool IsValidId(string id)
{
    if (id == null)
    {
        if (Debugger.IsAttached)
            Debugger.Break();
        throw new ArgumentNullException("name", "Id cannot be null.");
    }
    return id.StartsWith("ID-") && id.Length == 10;
}

Elejét vehetjük annak a kínos szituációnak is, amikor egy breakpoint fölött vakarjuk a fejünket, idegesen húzkodva az egeret a változóink fölött, hogy ez most hogy a Jóistenbe', alattunk meg mellékbüntetésként eltimeoutol egy adatbázis- vagy webservice hívás:

SqlCommand command = new SqlCommand();
command.CommandTimeout = Debugger.IsAttached ? int.MaxValue : 30;

A [Conditional] attribútummal kidekorált metódusokkal kombinálva pedig ugyanabból a kódbázisból pedig könnyedén fordíthatunk produkciós és "debugger-indítós" verziókat.

2009. március 11., szerda

C# 3.0 parciális metódusok

A C# 3.0 egyik kevésbé sztárolt új nyelvi eleme a partial method, ami a C# 2.0-ban megismert partial class képességeit bővíti ki.
A parciális osztályok arra szolgáltak, hogy megkönnyítsék elviselni, hogy a fejlesztőkörnyezetünk segít nekünk: a kódgenerálás csodás dolog, hiszen attól menti meg a szoftverfejleszőt, amit az a legjobban utál (neki kell kézzel megcsinálni azt, amit a gép is meg tudna), ugyanakkor magában rejti a veszélyt, hogy esetleg megtörténik az, amit viszont meg a méglegeslegjobban utál, nevezetesen hogy a gép jobban tudja a frankót, mint ő, és (a robotika mindhárom törvényét megsértve) felülvágja kétnapi munkáját.

A parciális osztályokkal szépen szét lehetett választani a Visual Studio (vagy bármely más kódgeneráló tool) és a codeproject-ről máso izzadtságos munkával megírt saját kódot, anélkül, hogy az alternatív megoldások - pl. absztakt ősoszályok generálása, és abból leszármazás - kereszjeit a vállunkra vettük volna.
Az osztályok kódját szétszórhattuk több forrásfájlba, és ha mindkét helyen megjelöltük classunkat a partial kulcsszóval, akkor a fordító szépen összefésülte őket.

C# 3.0-tól az ily módon megjelölt osztályokban lehetőségünk van metódusokat is partial-ként megjelölni. Ezek a metódusok első megközelítésben talán az absztrakt metódusokra hasonlítanak leginkább: típusmódosító nélküli, a partial kulcsszóval megjelölt metódusdefiníciók:

// Auto-generated part:
public partial class PartialExampleType
{
    public void GeneratedMethod()
    {
        Console.WriteLine("GeneratedMethod() calls PartialMetod()");
        PartialMethod();
    }

    partial void PartialMethod();
}
A példában egy parciális osztály generált része látható (i'm a liar: valójában én írtam), ami áll egy egy implementációstul legenerált, és egy "absztrakt" parciális metódusból. Az is rögtön látható, hogy az absztraktokhoz hasonlóan a parciális metódusok is meghívhatók annak ellenére, hogy a metódus konkrét implementációja még nem ismert.

Nézzük az általunk írt részt:

// Developer-written part:
public partial class PartialExampleType
{
    partial void PartialMethod()
    {
        Console.WriteLine("PartialMethod (\"{0}\") called.");
    }
}
Itt egy azonos szignatúrájú, de konkrét megvalósítással is rendelkező metódust láthatunk - az azonos metódusszignatúra előállítását az Intellisense a partial kulcsszó begépelése után megtámogatja (hasonló módon az override-hoz).
A metódustörzset sajnos nekünk kell megírni.

A fordító, ahogy a parciális osztályokat, a parciális metódusokat és azok hívásait is szépen összefésüli.

Eddig jól elvontunk az absztrakt hasonlattal, de most lássuk a különbségeket: egy absztrakt osztályból való leszármazáskor kötelező megvalósítani az absztrakt metódusokat, különben futásidőben nem lenne mit meghívni. A párhuzam itt kezd derékszögesedni, ugyanis a parciális osztályoknál/metóduoknál egyrészt nincs öröklődés ("forráskód-megosztásról" beszélünk, azaz compile-time eldől minden), másrészt az ilyen parciális metódusokat nem kötelező "megvalósítani" - ha nincs meg a "absztrakthoz" való, szignatúrában megegyező pár, a fordító egyszerűen nem fordítja be a hívását.
Felmerülhet a kérdés, hogy oké-oké, nem fordítja be a hívást egy magában álló, PartialMethod(); jellegű hivatkozásnál, de mi van, ha a metódus visszatéréséri értékére mi logikát építünk (gondolok itt a if (PartialGetIsValid()) { ... } jellegű szerkezetekre)?
A helyzet nagyon hasonló, mint a [ConditionalAttribute]-tal megjelölt metódusoknál, amik egy-egy szimbólum definiáltságától függően fordítódnak be vagy sem - ott úgy oldották meg a dolgot, hogy a conditional metódusoknak kötelező void visszatérési értékkel rendelkezőknek lenniük, és annyit elárulhatok, hogy itt sem másképp.
Az out paraméterek is tilosak.

Ha egy kicsit más megközelítésből vizsgáljuk a parciális metódusokat - nevezetesen, hogy mégis, mire jók -, azt látjuk, hogy a kódgenerátoroknak lehetőségük van nem csak effektív kód generálására, hanem egyfajta metódus-templatek definiálására, illetve ezen template-ek hívásának beépítésére a generált kódba - mi pedig a parciális osztály ránk eső részében vagy kitöltjük ezeket az "üres templateket", vagy nem.
Ebben van egy kicsi "eseménykezelő" jelleg, egy ORM kódot generáló tool pl. valószínű fog nekünk generálni mindenféle Load, Save, és egyéb hasonló metódusokat, amiknek az elejére-végére egy-egy OnSaving(...), OnSaved(...), OnLoading(...) hívásokat rakhat. Minket, pedig ha érdekel az adott "esemény", akkor megvalósítjuk a "kezelőjét" (amit azért látni kell: ez a példa ugyanolyan sánta, mint az absztaktos, hiszen szó nincs futásidőben összehergelt eseményforrásról meg feliratkozóról, fordítási időben minden eldől).

Ha megvizsgáljuk, hogy mik voltak a C# 3.0 nyelvi újdonságait kitermelő mögöttes igények és okok, általában azt találjuk, hogy a világ jobb hellyé tétele, a fejlesztők életének megkönnyítése volt a cél, meg hogy bírjon működni a LINQ.
Ha esetleg a LINQ to SQL generált osztályaiban parciális metódusokkal találkozunk, az csak a véletlen műve lehet...

2009. március 10., kedd

GUID vs. Int kulcsok

A relációs adatbázisokban az adatokat táblákban tároljuk, ha pedig ezeket a táblákat valamilyen módon egymáshoz szeretnénk kapcsolni (naná, ettől lesz relációs az adatbázis), a táblák sorait jó ellátni valamilyen egyedi azonosítóval.
Eddig semmi nagy meglepetés.
Ha nincs olyan szerencsénk, hogy az elsődleges kulcs szerepére van egy jó jelöltünk, akkor kénytelenek vagyunk magunk egy mesterséges, bizonyosan minden rekordra egyedi azonosítót (surrogate key-t) csinálni. De milyen típusú legyen ez a kulcs? Többnyire egy automatikusan generált int lesz a jelölt, de használhatunk esetleg GUID-ot is. Milyen előnyökkel, illetve hátrányokkal jár egy globálisan egyedi azonosító?

Pró:

  • Könnyű merge és replikációs szenáriók: a kulcsaink tényleg egyedi kulcsok, nem csak az adott táblára, hanem az adatbázis többi táblájára, vagy más szervereken tárolt, akár teljes, akár részleges másolatokhoz képest is. Ezzel rendkívül egyszerűvé válnak az olyan feladatok, amikor az adatokat több táblába, több szerverre szeretnénk szétszórni, vagy a több táblában, több szerveren keletkezett adatokat szeretnénk egy helyre összegyűjteni (a duplikátumok kiszűrésével, nyilván).
    Ha nincs saját replikációs logikánk, hanem az SQL Server ilyen szolgáltatásait szeretnénk használni, akkor pedig mindenképp szükségünk lesz egy uniqueidentifier típusú mezőre (amit RowGuid-ként meg is kell jelölnünk).
  • Az Id generálás bárhol megtörténhet: az auto-increment integer azonosítók esetében az újonnan létrehozott rekordok (entitások, ha tetszik) azonosítója csak az adatbázisba való INSERTálás után képződik meg - ellenben maga az entitás általában "fönt", az alkalmazásunk üzleti logika rétegében. Amíg az új példányunk sikeresen perzisztálásra nem kerül, az Id-val nekünk kell bűvészkedni, esetleg bevállalni egy plusz DB roundtripet már példányosításkor (lehet, hogy fölöslegesen). Tranzakcionális mentéskor ROLLBACK esetén pedig külön öröm, hogy a már beállított Id-kat "vissza kell venni".
    Guid-nál ilyen gond nincs, maga az Id bárhol gerenálódhat, az biztosan egyedi lesz.

Kontra:

  • Nagyobb kulcsméret: míg az int 4 byte, a GUID adattípus négyszer akkora, 16 byte-nyi helyet foglal. Ennek természetesen ára van: tárhelyben, memória-igényben, hálózati sávszélességben, performanciában.
  • Nehéz olvasni, megjegyezni: Ez elsőre nagyon profánnak tűnhet, de gondoljunk bele, mennyivel egyszerűbb azt fejben tartani, hogy az Administrator Id-ja 1, mint azt, hogy {26D66F17-1297-44fd-AD83-FD102C8A41C9}. Vagy leírni egy olyan SELECT-et, hogy ...WHERE Id IN (1, 2, 3), mint egy olyat, hogy ...WHERE Id IN ('6FA9C7BC-A1F0-4B4E-A4CC-49EE3822E9E8', '47F1211C-61DB-4102-B103-3EED9AC8733F', 'B12A79BA-78B9-431C-BE71-0CF30DA37BF2'). Ha ezután az pont elolvasása után már kezd káprázni a szemünk a betűktől és a számoktól - pedig csak olvasásról van szó, gondoljunk bele, ha ezt be kell írni, ott pedig egy typo, és máris nem az történik, amit szeretnénk! -, valami kis képünk azért már lehet róla, hogy a GUID azonosítók árát fejlesztési élmény, hibakereshetőség, karbantarthatóság oldalról is meg kell fizetni.
    Ugyanennek a problémának egy másik vetülete, hogy ha az elemeink azonosítói megjelennek pl. egy webes alkalmazás URL-jeiben, akkor egy GUID azért lényegesen cifrábban néz ki, mint az, hogy "?Id=1" (ugyanakkor ez akár előny is lehet: egy támadási felülettel kevesebb, azt ugyanis nem nagy sakk kitalálni, hogy egy int kulcsos Users táblában lesz olyan user, akinek az azonosítója 0 vagy 1 - jó eséllyel pont az adminisztrátor -, míg GUID-nál ez nem triviális).
  • A GUID nem minden nyelven natív adattípus: bár a .NET támogatja a Guid típust, illetve az SQL uniqueidentifier típusának Guiddá- és visszaalakítását, ez nem minden nyelvben ilyen triviális. De még ha nem is tervezzük az adatbázis nem-dotnet nyelvből való kurkászását, könnyen lehet, hogy a kliensoldani JavaScript kódunkban szükség lesz az Id-kkal való varázslásra.
  • A GUID-ok nem szekvenciálisak, ellentétben az auto-generated integer kulcsokkal, amik szép monoton növekvőek (1, 2, 3, 4...).
    Ha egy ilyen mezőt megteszünk clustered key-nek, akkor az integer esetében az újonnal létrejövő entitások szépen az eddigiek után fűződnek, míg GUID esetében össze-vissza fognak a rekordok beszúródni a táblába, erős fragmentálódsához vezetve. Szerencsére ezen lehet segíteni.

De már csak egy későbbi alkalommal.

2009. március 7., szombat

RTFM

Történt, hogy egy saját Stream implementációt kellett készítenem. A Stream egy absztrakt osztály, ebből ugye leszármazunk, a Visual Studioban nyomunk egy CTRL+.-ot (akiket a szokásosnál lényegesen több ujjal áldott meg a Jóisten, azok ALT+SHIFT+F10-et), ENTER, és a Studio legenerálja nekünk a megvalósítandó metódusok vázát.
Az absztakt Stream elég sok ilyet tartalmaz, a legtöbbje elég triviális (pl. bool CanRead, CanWrite, CanSeek... tulajdonságok), ezeket csípőből implementálhatjuk, hamar le fog jönni, hogy a megvalósításunk igazi "lelke" a Read metódus lesz (meg a Write, de mivel én egy read-only stream implementációt csináltam, ezzel nem volt sok gond, "throw new NotSupportedException("The stream is read only.");", azt' szevasz).

A legenerált kód így néz ki:

public override int Read(byte[] buffer, int offset, int count)
{
    throw new NotImplementedException();
}
Ebből már magában elég jól lejön mit kell ebben a metódusban csinálni: kapunk egy buffert - egy byte tömböt -, abba kell belehányni count darab byte-ot, offset eltolással.
Meg is tettem, jöhetett a teszt: puff, OutOfMemoryException. Na hát ez szép. Debugger balról be, nézzük mi történik. Hát hívódik a Read, mint az őrült, sosincs vége, szépen eszi föl a memóriát. Ajaj.

No, egyet szerencsére tudtam: As a software developer, you are your own worst enemy. The sooner you realize that, the better off you'll be.
Vegyünk vissza gyorsan az arcból, és nézzük mit mond erről a Biblia: "When overridden in a derived class, reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read." - nos, hát igen.
Ez nem derült ki a metódus-szignatúrából, ezt vagy tudjuk, vagy azt hisszük, hogy tudjuk.

"Akinek nincs esze, legyen notesze!" - mondotta volt középiskolai osztályfőnököm az elnézést tanár úr, elfelejtettem típusú kifogásokra. És milyen igaz, annyival toldanám meg: akinek nincs esze, olvassa el a dokumentációt.