Tag Archives: Best practices

Money – kouzlíme s generiky II.

Generika jsou dobrá nejenom pro kolekce. Že se dají velmi dobře využít i v jiných situacích, dnes předvedu na třídě Money  (diskuze, kdy má být Money třída a kdy je lepší struct není předmětem tohoto článku). Uvidíte, že jdou dělat i takové věci jako generické operátory.

Zadání problému

Chceme řešit imlementaci třídy Money, třídy reprezentující peněžní datový typ skládající se z částky (Amount) a její měny (Currency). Naším cílem je dosáhnout následujího:

  • chceme zavést opakovaně využitelnou třídu Money (typicky do vlastní class-library) – ta má obsahovat vlastnost Amount (částku typu decimal) a vlastnost Currency (měnu), jejíž typ obecně naznáme a chceme ho ponechat na libovůli klientské aplikace (známe však pravidla pro obecné Money, které nám říkají, že bez kurzových převodů můžeme běžné operace provádět jen mezi operandy stejné měny – porovnávání, sčítání/odečítání, násobení/dělení číslem, dělení mezi sebou, atp.). Do class-library tedy chceme implementovat třídu Money<TCurrency>.
  • implementaci veškeré logiky chceme mít součástí této třídy Money<TCurrency> – v této třídě chceme definovat potřebné operace a centrálně je udržovat v class-library, chceme mít pokryté všechny základní operátory, atp. atp.
  • v jednoduchých svých projektech zadefinujeme třídu Currency (obvykle business-object odpovídající číselníku měn v DB s vlastnostmi Nazev, Zkratka, atp.) a budeme v nich třídu Money<TCurrency> používat přímo v kódu
Money<Currency> cena = new Money(150.20M, Currency.Czk);
cena = cena + new Money(20M, Currency.Czk);
cena = cena * 10;
MyLabel.Text = cena.Amount.ToString("n2") + " " + cena.Currency.Zkratka;
...
  • v rozsáhlejších našich projektech máme také třídu Currency, chceme však vytvořit odvozenou negenerickou třídu Money, která bude základní funkčnost obecnějšího Money<TCurrency> rozšiřovat o nějaké nové project-specific možnosti, např. ony kurzové přepočty, standardní výstup, atp. Použití zamýšlíme nějak takto:
Money cena = new Money(10M, Currency.Czk);
cena = cena.ToCurrency(Currency.Usd) + new Money(159.3M, Currency.Usd);
MyLabel.Text = cena.ToString("n2");
...
  • stále však trváme na tom, že implementace běžných operací (zejména operátorů) musí být použita z obecného Money<TCurrency>. Uvědomme si však základní problém – jak se vyhnout nové definici operátorů v negenerickém potomkovi Money : Money<Currency>, když operátory jsou statické, nejsou předmětem dědičnosti a v Money<TCurrency> budou vypadat nejspíš nějak takto:
public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
// implementace
}

…v potomkovi Money chceme přeci součet dvou Money a hlavně návratový typ Money, nikoliv Money<Currency>

public static Money operator +(Money money1, Money money2)
{
// znovu implementace??? Tomu jsme se chtěli vyhnout! Implementaci chceme mít v class-library.
}
Kouzlo první – generická implementace funkčnosti operátorů

Čeho chceme dosáhnout – mít centralizovanou implementaci funkčnosti operátorů tak, abychom ji mohli snadno udržovat pro všechny naše projekty. Kouzlo první spočívá v extrahování implementace operací do generických metod třídy Money<TCurrency>, kteréžto metody sama třída Money<TCurrency> ve svých operátorech využije, stejnětak jako je může využít libovolný potomek při definici svých typovaných operátorů.

Money<TCurrency>:

public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
    return SectiMoney<Money<TCurrency>>(money1, money2);
}

public static TResult SectiMoney<TResult>(Money<TCurrency> money1, Money<TCurrency> money2)
    where TResult : Money<TCurrency>, new()
{
    if (money1.Currency != money2.Currency)
    {
        throw ...
    }

    TResult result = new TResult();
    result.Amount = money1.Amount + money2.Amount;
    result.Currency = money1.Currency;
    return result;
}

Obdobně jako u BusinessObjectCollection využíváme kouzla generika s „cílovým typem“. Daní za tento způsob je nám nutnost vyžadovat constructor bez parametrů, abychom v generiku byly schopni vytvořit instanci cílového typu (možnost generika s constructorem předepsaných parametrů se asi v dohledné době nedočkáme, v .NET 3.5 pokud vím, nic takového stále není a po diskuzi s částí CLR teamu na letošním MVP Summitu v Redmondu už i chápu proč).

Project-specific potomek Money už pak může využívat této centrální implementace snadno:

public static Money operator +(Money money1, Money money2)
{
    return SectiMoney<Money>(money1, money2);
}
Kouzlo druhé – projektový potomek Money bez nové implementace operátorů

Řekněme, že se chceme v projektech, v project-specific třídách Money, úplně zbavit nutnosti implementovat operátory, byť už máme cestu jak v těchto operátorech využít centrální implementace funkčnosti. Projektový kód chceme mít co nejčistší, prostý takovýchto infrastrukturních redefinic, tak, abychom se v něm mohli přehledně věnovat jen nové funkčnosti Money oproti předkovi Money<TCurrency> – např. doplnit ony omílané kurzové přepočty, atp.

Jak na to?

Do řetězce dědičnosti mezi Money a Money<TCurrency> vložíme kouzelnou třídu MoneyImplementationBase, která nás dalším fíglem s generiky a operátory posune ke kýženému výsledku…

public abstract class MoneyImplementationBase<TCurrency, TResult>: Money<TCurrency>
    where TCurrency: class
    where TResult : MoneyImplementationBase<TCurrency, TResult>, new()
{
    public static TResult operator +(MoneyImplementationBase<TCurrency, TResult> money1, MoneyImplementationBase<TCurrency, TResult> money2)
    {
        return SectiMoney<TResult>(money1, money2);
    }
}

Tato třída samozřejmě patří do naší centrální class-library odkud ji budou využívat jednotlivé projekty.

A výsledek? Plně funkční projektové Money pak dostaneme takto:

public class Money : MoneyImplementationBase<Currency, Money>
{
    // toť vše!
    // sem už můžeme doplnit jen project-specific rozšíření Money, veškerou základní funkčnost nám poskytují předci
}

Hezké, že? Stejně jako u kouzlení s BusinessObjectCollection předhazujeme generickému předkovi sami sebe (pokud to bude potřeba, můžeme předhodit i svého potomka).

BusinessObjectCollection – kouzlíme s generiky

Po opakovaných přáních poodkrýt zákoutí našeho frameworku a generované business-vrstvy jsem se rozhodl zveřejnit vybraná témata a perličky. Dnes přicházím s BusinessObjectCollection, resp. s možným způsobem implementace kolekcí v business-vrstvě pomocí generik tak, abychom dosáhli maximální kompatibility typů a nemuseli bojovat s neustálou typovou neshodou kolekcí.

Čeho chceme u kolekcí business-objektů dosáhnout:

  1. Chceme jednu společnou bázovou třídu BusinessObjectCollection pro všechny kolekce business-objektů a v ní centrálně implementovat operace, které jsou společné pro všechny business-objekty. Pro příklad tam dáme primitivní metodu SaveAll(), která všechny business-objekty v kolekci uloží a její implementace je stejná, ať už se jedná o kolekci Faktur, nebo kolekci Subjektů.
  2. V bázové třídě BusinessObjectCollection chceme definovat i operace, které pracují přímo s business-objekty, pokud možno strong-type, typem co nejpřesnějším. Např. běžné Add(Order order), Remove(Subject subject), atp.
  3. V bázové třídě BusinessObjectCollection chceme definovat i operace, které pracují s kolekcemi business-objektů, pokud možno strong-type, typem kolekce co nejpřesnějším. Např. FindAll(), který vrací kolekci všech nalezených prvků odpovídajících nějaké podmínce, atp.
  4. Od této společné bázové abstraktní třídy BusinessObjectCollection chceme děděním vytvářet konkrétní potomky – třídy kolekcí pro jednotlivé business-objekty, např. OrderCollection, SubjectCollection, UserCollection, atp.

Jak tedy na to? Nejprve si zavedeme základní třídy, které budu v dalších příkladech používat:

public abstract class BusinessObjectBase
{
    public abstract void Save();
}

public class Order : BusinessObjectBase
{
    public decimal Cena { get; set; }
    public override void Save()
    {
        // implementace
    }
}

BusinessObjectBase je bázová třída pro všechny business-objekty, Order je příklad konkrétní implementace.

Způsob 1. – základní

Nejprve si ukážeme, jak vypadá běžná implementace kolekcí:

public abstract class BusinessObjectCollection<T> : Collection<T>
    where T : BusinessObjectBase
{
    public BusinessObjectCollection()
        : base(new List<T>())
    {
    }

    public void SaveAll()
    {
        foreach (BusinessObjectBase item in this.Items)
        {
            item.Save();
        }
    }

    public virtual List<T> FindAll(Predicate<T> match)
    {
        List<T> innerList = (List<T>)Items;
        List<T> found = innerList.FindAll(match);
        return found;
    }
}

public class OrderCollection : BusinessObjectCollection<Order>
{
    public decimal GetCelkovaCena()
    {
        decimal result = 0;
        // implementace
        return result;
    }
}

Co tato implementace ukazuje:

  • Jako úplný základ používáme generickou kolekci Collection<T>, která samotná už nám přináší všechny základní strong-type operace, např. Add(), Contains(),IndexOf(), Insert(), Remove(), RemoveAt() a indexer přes pořadí this[int].
  • Třída Collection<T> je implementačně pouze wrapperem nějaké vnitřní kolekce, kterou skrývá pod property protected IList<T> Items. Vnitřní implementaceCollection<T> používá jako tento datový nosič List<T>, v kontraktu této třídy to však popsáno není. Chceme-li tedy zaručit, že naše prvky budou fyzicky uloženy v datové struktuře typu List<T>, můžeme využít protected constructor odCollection<T>, který umožňuje předat instanci požadované datové struktury, která musí implementovat IList<T>. Nám se hodí právě List<T>, který zahrnuje spoustu dalších užitečných metod, které budeme chtít naše business-kolekce také naučit. V příkladu využíváme List<T>.FindAll().
  • Metoda SaveAll() provádí určitou operaci s prvky kolekce, její interface je vyhovující, není co namítat.
  • Metoda FindAll() ale už není tak krásná, jejím návratovým typem je totiž List<T> a tím se nám klientský kód pěkně zamotá:
OrderCollection orders = Order.GetAll();

List<Order> zeroOrders = orders.FindAll(delegate(Order item)
{
    return (item.Cena == 0);
});

zeroOrders.SaveAll(); //nejde
decimal d = zeroOrder.GetCelkovaCena(); //nejde
OrderCollection zeroOrders2 = (OrderCollection)zeroOrders; // nejde

Návratovým typem FindAll() je List<Order>, s kterým přicházíme o veškerou logiku, kterou jsme business-kolekce naučili, jak v bázové třídě BusinessObjectCollection, tak v konkrétní tříděOrderCollection.

Způsob 2. – Drobné vylepšení základní metody

Zlepšení na poli typů dosáhneme, pokud implementaci naší BusinessObjectCollection<T> upravíme takto:

public abstract class BusinessObjectCollection<T> : Collection<T>
    where T : BusinessObjectBase
{
    public BusinessObjectCollection()
        : base(new List<T>())
    {
    }

    public void AddRange(IEnumerable<T> source)
    {
        List<T> innerList = (List<T>)Items;
        innerList.AddRange(source);
    }

    public virtual BusinessObjectCollection<T> FindAll(Predicate<T> match)
    {
        List<T> innerList = (List<T>)Items;
        List<T> found = innerList.FindAll(match);

        BusinessObjectCollection<T> result = new BusinessObjectCollection<T>();
        result.AddRange(found);
        return result;
    }

    public void SaveAll()
    {
        foreach (BusinessObjectBase item in this.Items)
        {
            item.Save();
        }
    }
}
  • Metodu FindAll() jsme trošku typově vylepšili, namísto obecného List<T> ji necháme vracet alespoň datový typ BusinessObjectCollection<T>. Protože však musíme instanci BusinessObjectCollection<T> nějak získat, nezbývá než si založit novou a pomocí další vypropagované metody List<T>.AddRange() do ní prvky zkopírovat. Je to nepochybně určitý výkonový overhead, nicméně vzhledem k tomu, že kopírujeme pouze reference na existující instance jednotlivých business-objektů, jedná se o overhead zanedbatelný a naopak převáží výhoda příjemného vylepšení interface kolekcí.
  • Stále však nemůžeme být spokojeni:
OrderCollection orders = Order.GetAll();

BusinessObjectCollection<Order> zeroOrders = orders.FindAll(delegate(Order item)
{
    return (item.Cena == 0);
});

zeroOrders.SaveAll(); //HURÁ! Funguje!
decimal d = zeroOrders.GetCelkovaCena(); // stále nefunguje
OrderCollection zeroOrders2 = (OrderCollection)zeroOrders; // nejde

Stále nedostáváme od metody FindAll() kolekci typu OrderCollection. Sice jsme získali možnost využít operací implementovaných v BusinessObjectCollection, jako např. SaveAll(), stále však nedosáhneme na operace implementované v OrderCollection, nemáme GetCelkovaCena().

Způsob 3. – Kouzlíme s generiky

Generika v .NET Frameworku jsou udělána dobře a snesou všechno. Opakovaně jsem sám u generik předpokládal, že „tohle už přeci nemůže jít“. Generika opravdu unesou hodně:

public abstract class BusinessObjectCollection<TItem, TCollection> : Collection<TItem>
    where TItem : BusinessObjectBase
    where TCollection : BusinessObjectCollection<TItem, TCollection>, new()
{
    public BusinessObjectCollection()
        : base(new List<TItem>())
    {
    }

    public void AddRange(IEnumerable<TItem> source)
    {
        List<TItem> innerList = (List<TItem>)Items;
        innerList.AddRange(source);
    }

    public virtual TCollection FindAll(Predicate<TItem> match)
    {
        List<TItem> innerList = (List<TItem>)Items;
        List<TItem> found = innerList.FindAll(match);

        TCollection result = new TCollection();
        result.AddRange(found);
        return result;
    }

    public void SaveAll()
    {
        foreach (BusinessObjectBase item in this.Items)
        {
            item.Save();
        }
    }
}

public class OrderCollection : BusinessObjectCollection<Order, OrderCollection>
{
    public decimal GetCelkovaCena()
    {
        decimal result = 0;
        // implementace
        return result;
    }
}

Co jsme to udělali?

  • Definici třídy BusinessObjectCollection jsme rozšířili o další generický typ naBusinessObjectCollection<TItem, TCollection>, kde TItem je typ prvků kolekce aTCollection je typ kolekce samotné!
  • Pomocí generického typu TCollection můžeme nyní postavit metodu FindAll tak, že jejím návratovým typem bude TCollection, tedy požadovaný typ kolekce.
  • Získáváme plně typově konzistentní kolekci OrderCollection, jejíž všechny metody používají typ Order jako typ prvku a typ OrderCollection jako typ kolekce!
  • Dostáváme jednoduchý snadno použitelný interface:
OrderCollection orders = Order.GetAll();

OrderCollection zeroOrders = orders.FindAll(delegate(Order item)
{
    return (item.Cena == 0);
});

zeroOrders.SaveAll(); //HURÁ! Funguje!
decimal d = zeroOrders.GetCelkovaCena(); //HURÁ! Funguje!
// přetypovávat už ani nepotřebujeme
Způsob 4. – A jak je to u nás?

Generika snesou hodně, opravdu hodně. U nás je to takto:

  • V našich class-libraries (HAVIT Framework Extensions) máme abstraktní předkyBusinessObjectBase a BusinessObjectCollection<TItem, TCollection>. Tyto třídy definují operace společné pro všechny business-objekty, na všech našich projektech, prostě úplný základ, jádro.
  • Na konkrétních projektech pak používáme generování základního kódu business-tříd, tedy business-objektů i business-kolekcí, na základě vstupního schematu (např. DB diagramu). Generovaný kód umísťujeme do bázových tříd OrderBase aOrderCollectionBase, zatímco vlastní kód zapisujeme až do potomků těchto třídOrder a OrderCollection.
  • Při prvním spuštění generátor vygeneruje třídy všechny (třídy Order aOrderCollection vygeneruje prázdné), zatímco při dalších spuštěních generátor přepisuje třídy OrderBase a OrderCollectionBase (náš „dopsaný“ kód tak zůstává nedotčen).
  • Výsledný kód tříd vypadá nějak takto:

Nejprve business-objekt Order:

public abstract class OrderBase : BusinessObjectBase
{
    // toto je generovaná třída, generátor ji vždy přepíše, nic se sem nesmí ručně dopisovat
    public decimal Cena { get; set; }
    public override void Save()
    {
        // implementace
    }
}

public class Order : OrderBase
{
    // toto je používaná třída, sem mohu zapsat vlastní kód

    public decimal GetCenaPoSleve()
    {
        // implementace
    }
}

Dále business-kolekce OrderCollection:

public abstract class OrderCollectionBase : BusinessObjectCollection<Order, OrderCollection>
{
    // toto je generovaná třída, generátor ji vždy přepíše, nic se sem nesmí ručně dopisovat
}

public class OrderCollection : OrderCollectionBase
{
    // toto je používaná třída, sem mohu zapsat vlastní kód
    public decimal GetCelkovaCena()
    {
        decimal result = 0;
        // implementace
        return result;
    }
}

Ano! Generika umožňují dokonce v předkovi OrderCollectionBase používat jako TCollection odkaz na vlastního potomka OrderCollection.

Pokračování

IDisposable – ResourceWrapper design pattern

IDisposable je interface, který předepisuje implementovat jednu jedinou metodu:

public interface IDisposable
{
    void Dispose ();
}

Metoda Dispose() slouží k uvolnění unmanaged resources (file handles, window handles, database connections, atp.) a celé rozhraní IDisposable je tak určeno k implementaci tzv. Explicit Resource Managementu, tedy chceme dát programátorům používajícím naší třídu možnost explicitního úklidu unmanaged resources v okamžiku, kdy instanci naší třídy přestanou dále potřebovat:

Resource r = new Resource();
try
{
  r.DoSomething();
}
finally
{
  if (r != null)
  {
    ((IDisposable)r).Dispose();
  }
}

Tento zápis lze v C# ekvivalentně zkrátit pomocí statementu using:

 

using (Resource r = new Resource())
{
    r.DoSomething();
}

Statement using je určen pro přehlednou práci s IDisposable třídami a ve skutečnosti to není nic jiného, než výše uvedený try-finally blok.

ResourceWrapper design pattern

Microsoft v .NET Frameworku sám většinou používá a pro explicitní management resources doporučuje tzv. ResourceWrapper design pattern. Jeho funkčnost vychází z následujích požadavků:

  1. Primárně by měli programátoři používající naší třídu uvolňovat unamanaged resources právě prostřednictvím volání metody IDisposable.Dispose(), popř. pomocí další metody očekávanějšího jména stejné funkčnosti – např. file.Close().
  2. Pokud nedejbože nepozorný programátor zapomene explicitně zavolat úklid metodou Dispose(), postaráme se o úklid a uvolnění resources alespoň v destruktoru třídy (C# destruktory jsou ve skutečnosti překládány do metody Finalize()).

Důležité je však zdůraznit, že zatímco metodu Dispose() můžeme volat ihned, jakmile přestaneme příslušnou instanci potřebovat, a tedy dojde k uvolnění zdrojů hned, destruktor je volaný až po garbage collection, která může proběhnout taky až za velmi dlouhou dobu a do té doby jsou všechny unmanaged resources neuklizeny, např. tedy blokujeme přístup k nějakému souboru, s nímž zbytečně dlouho nemůže nikdo jiný pracovat.

Dále je potřeba si uvědomit, jak Garbage Collector pracuje s instancemi tříd, které mají destructor (metodu Finalize):

  1. Runtime si udržuje seznam všech existujících objektů, které budou při svém odstraňování potřebovat finalizaci (mají destruktor) – tzv. Finalization queue.
  2. Garbage Collector při úklid prochází graf dostupných objektů od objektů kořenových (statické globální proměnné, lokální proměnné v zásobníku, registry procesoru) a objekty, které jsou nedostupné, ty uklízí – uvolňuje je z paměti.
  3. Pokud však Garbage Collector má uklidit objekt, který potřebuje finalizaci (je ve Finalization Queue, má destruktor), pak je neuvolní hned, nýbrž je vyjme z Finalization Queue a vytvoří jim záznam v tzv. Freachable queue (finalization-reachable). Freachable queue je součástí kořenových objektových struktur a objekty v ní umístěné jsou tak znovu dostupné (reachable) – Garbage Collector je neuklidí.
  4. Freachable queue existuje proto, že jednotlivé destuktory mohou trvat i velmi dlouho a není tak vhodné, aby volání destruktorů zdržovalo samotný běh Garbage Collectoru. Proto GC hází objekty, které potřebují finalizaci do Freachable queue a sám běží dál/skončí. Frontu Freachable obsluhuje samostatný finalizační thread, který z ní vybírá jednotlivé objekty a provádí jejich finalizaci (volá metodu Finalize, tj. destruktory).
  5. Provedením metody Finalize (destruktoru) finalizační thread vyjme objekt z Freachable queue a ten už tak není ani dostupný přes tuto frontu, ani není v hlavním seznamu objektů pro finalizaci, a proto může být Garbage Collectorem jako nedostupný konečně uklizen.
  6. Protože se objekty s destruktory stávají po dobu umístění v Freachable queue opět dostupnými, stávají se tak dostupnými i všechny objekty, které ten samotný objekt refrencuje, tj. celý podstrom objektů, přestože ony samy třeba destruktory nemají. Řádné uvolnění paměti je tak odsouváno, oproti běžnému průběhu GC nad objekty bez finalizace (bez destruktorů).
  7. Poslední záludností Freachable queue je, že objekty v ní nejsou umístěny v pořadí, v jakém byly založeny, ale v pořadí, v jakém se k jejich zpracování dostal Garbage Collector, tedy prakticky v pořadí nijak nedefinovaném. V našem destruktoru tedy nevíme, jestli destruktory objektů, které sami referencujeme, už proběhly (objekty byly ve frontě před námi), nebo jestli teprve proběhnou (objekty jsou ve frontě za námi).

Co z toho vyplývá pro implementaci:

  1. Pokud uživatel uvolní prostředky explicitně voláním metody Dispose(), pak chceme potlačit volání pojistné finalizace (destruktoru), protože finalizace odsouvá uvolnění objektu i objektů jím referencovaných. To budeme dělat použitím metody GC.SuppressFinalize(), která vyjímá objekt z Finalization Queue a objekt tak není při garbage collection už přesouván do Freachable queue, nýbrž je uvolněn ihned.
  2. Zatímco v metodě Dispose() můžeme uklízet i další námi vlastněné IDisposable objekty, protože víme, že jejich úklid ještě neproběhl (pokud je máme private/protected jen pro sebe), tak v destruktoru to možné není. Destruktory jsou totiž volány v nedefinovaném pořadí a úklid námi vlastněných podřízených IDisposable objektů už mohl proběhnout před námi.

Jak to tedy všechno uděláme:

  1. Abychom byla naše třída imunní vůči vícenásobnému volání Dispose(), a abychom sami pro další kontroly věděli, jestli už disposování proběhlo, zavedeme si proměnnou bool disposed:
    private bool disposed = false;
    

  2. Protože chceme mít implementaci uvolňování prostředků na jednom místě, a protože chceme umožnit svým případným potomkům rozšířit Dispose(), zavedeme si metodu protected virtual void Dispose(bool disposing), kde parametr disposing identifikuje, jestli samotné volání této implementační metody proběhlo z explicitního Dispose() a můžeme tak uvolňovat i další vlastněné IDisposable členy (disposing == true), nebo jestli volání proběhlo z destruktoru a můžeme uklízet jen sami sebe (disposing == false).
protected virtual void Dispose(bool disposing)
{
  // Dispose() má být imunní vůči vícenásobnému volání
  if(!this.disposed)
  {
    // Pokud jsme volání z metody Dispose(),
    // můžeme uvolnit i vlastněné IDisposable prvky. Z destruktoru ne.
    if(disposing)
    {
      // Uvolňujeme tedy tzv. managed resources.
      component.Dispose();
    }
         
    // ...v každém případě však uvolňujeme unmanaged resources 
    CloseHandle(handle);
    handle = IntPtr.Zero;            
    disposed = true; 
  }
}

3. Do samotné metody IDisposable.Dispose() pak dáme volání této implementace (s parametrem disposing = true) a dále vyřadíme objekt z Finalization Queue, tedy potlačíme jeho další finalizaci. volání destruktoru (a umožníme tak rychlý úklid objektu Garbage Collectorem):

public void Dispose()   // popř. explicitně IDisposable.Dispose()
{
  Dispose(true);
  GC.SuppressFinalize(this);
}

4. Protože chceme zajistit uvolnění unmanaged prostředků i v případě, že programátor zapomene zavolat Dispose(), zavoláme metodu Dispose() i zdestruktoru:

~ResourceWrapper()
{
   Dispose(false);
}

5. Samotnou funkčnost třídy, která má být dostupná jen před disposováním objektu, pak podmíníme (!disposed):

public void DoSomething()
{
  if (disposed)
  {
    throw new ObjectDisposedException("ResourceWrapper");
  }

  // následuje vlastní funkčnost metody

}

6. Nakonec můžeme přidat ještě metodu s domain-specific názvem pro dispose:

public void Close()
{
  Dispose();
}
Související

SqlTransaction jednoduše s využitím anonymních metod

Základní .NET pattern pro volání transakcí je poměrně jednoduchý a známý, ostatně je uveden i jako example v MSDN/SDK dokumentaci:

 
private static void ExecuteSqlTransaction(string connectionString)
{
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlCommand command = connection.CreateCommand();
        SqlTransaction transaction;
  
        // Start a local transaction.
        transaction = connection.BeginTransaction("SampleTransaction");
  
        // Must assign both transaction object and connection
        // to Command object for a pending local transaction
        command.Connection = connection;
        command.Transaction = transaction;
  
        try
        {
            command.CommandText =
                "Insert into Region (RegionID, RegionDescription) VALUES (100, 'Description')";
            command.ExecuteNonQuery();
            command.CommandText =
                "Insert into Region (RegionID, RegionDescription) VALUES (101, 'Description')";
            command.ExecuteNonQuery();
  
            // Attempt to commit the transaction.
            transaction.Commit();
            Console.WriteLine("Both records are written to database.");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Commit Exception Type: {0}", ex.GetType());
            Console.WriteLine("  Message: {0}", ex.Message);
  
            // Attempt to roll back the transaction.
            try
            {
                transaction.Rollback();
            }
            catch (Exception ex2)
            {
                // This catch block will handle any errors that may have occurred
                // on the server that would cause the rollback to fail, such as
                // a closed connection.
                Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType());
                Console.WriteLine("  Message: {0}", ex2.Message);
            }
        }
    }
}

Po chvilce práce s transakcemi nás však začne trápit, že se poměrně značná část zdrojového kódu neustále opakuje a vlastní výkonné jádro ve změti řádek zaniká.

Jak by se Vám líbil následující způsob volání transakcí?

int myID = 5;
object result;
  
SqlDataAccess.ExecuteTransaction(
   delegate(SqlTransaction transaction)
   {
      // uvnitř lze používat i lokální proměnné (samozřejmě i parametry, statické fieldy atp.)
  
      SqlCommand cmd1 = new SqlCommand("command string");
      cmd1.Transaction = transaction;
      cmd1.Connection = transaction.Connection;
      cmd1.Parameters.AddWithValue("@MyID", myID);
      cmd1.ExecuteNonQuery();
  
      SqlCommand cmd2 = new SqlCommand("another command");
      cmd2.Transaction = transaction;
      cmd2.Connection = transaction.Connection;
      result = cmd2.ExecuteScalar();
   });

Líbí? A přitom to není nic složitého, stačí využít delegátů a anonymních metod…

/// <summary>
/// Reprezentuje metodu, která vykonává jednotlivé kroky transakce.
/// </summary>
/// <param name="transaction">transakce, v rámci které mají být jednotlivé kroky vykonány</param>
public delegate void SqlTransactionDelegate(SqlTransaction transaction);

/// <summary>
/// Třída SqlDataAccess nám pomocí statických metod usnadňuje práci s SQL serverem.
/// </summary>
public static class SqlDataAccess
{
  /// <summary>
  /// Vykoná požadované kroky v rámci transakce.
  /// Je spuštěna a commitována nová samostatná transakce.
  /// </summary>
  public static void ExecuteTransaction(SqlTransactionDelegate transactionWork)
  {
   ExecuteTransaction(transactionWork, null);
  }
  
  /// <summary>
  /// Vykoná požadované kroky v rámci transakce.
  /// Pokud je zadaná transakce <c>null</c>, je vytvořena, spuštěna a commitována nová.
  /// Pokud zadaná transakce není <c>null</c>, jsou zadané kroky pouze v rámci transakce vykonány.
  /// </summary>
  /// <param name="transaction">transakce (vnější)</param>
  public static void ExecuteTransaction(SqlTransactionDelegate transactionWork, SqlTransaction transaction)
  {
   SqlTransaction currentTransaction = transaction;
   SqlConnection connection;
   if (transaction == null)
   {
    // otevření spojení, pokud jsme iniciátory transakce
    connection = SqlDataAccess.GetConnection(); // ponechávám na Vaší implementaci
    connection.Open();
    currentTransaction = connection.BeginTransaction();
   }
   else
   {
    connection = currentTransaction.Connection;
   }
  
   try
   {
    transactionWork(currentTransaction);
 
    if (transaction == null)
    {
     // commit chceme jen v případě, že nejsme uvnitř vnější transakce
     currentTransaction.Commit();
    }
   }
   catch
   {
    try
    {
     currentTransaction.Rollback();
    }
    catch
    {
     // chceme vyhodit vnější výjimku, ne problém s rollbackem
    }
    throw;
   }
   finally
   {
    // uzavření spojení, pokud jsme iniciátory transakce
    if (transaction == null)
    {
     connection.Close();
    }
   }
  }
}

Jenom dodávám, že druhý overload umožňuje mimo vytvoření transakce nové (pokud je parametr transaction = null) i spuštění celé operace v rámci rozlehlejší transakce vnější, což může v reálu vypada nějak takto:

public class Order
{
   ...
  
   public void Save(SqlTransaction transaction)
   {
      SqlDataAccess.ExecuteTransaction(
         delegate(SqlTransaction currentTransaction)
         {
  
             this.DoSave(currentTransaction);
             OrderItems.SaveAll(currentTransaction);
             Customer.Save(currentTransaction);
                     
  
         }, transaction);
   }
}
Implementace v HAVIT .NET Framework Extensions:

Havit.Data.SqlClient.SqlTransactionDelegate(…)
Havit.Data.SqlClient.SqlDataAccess.ExecuteTransaction(…)

Equals, operátory == a !=

 

Metoda Equals() slouží v .NET Frameworku k ověření rovnosti. Její výchozí implementace Object.Equals() ověřuje referenční rovnost, tedy zda obě reference odkazují na stejnou instanci. U typů, které nesou nějaké smysluplné hodnoty, může být však vhodnější implementovat Equals jako ověření hodnotové rovnosti, byť by se jednalo o dvě různé instance.

.NET má metodu Object.Equals ve dvou podobách

  • public virtual bool Equals(object obj) – k té chceme implementovat override
  • public static bool Equals(object objA, object objB)
public static bool Equals(object objA, object objB)
{
   if (objA == objB)
   {
      return true;
   }
   if ((objA != null) && (objB != null))
   {
      return objA.Equals(objB);
   }
   return false;
}

Statická metoda tedy nejprve ověří rovnost referencí, pokud jde o jednu instanci, vrátí true. Potom ověří, zda-li není jeden z objektů null. Pokud ano, vrátí false, pokud ne zavolá instanční podobu, kterou právě overridujeme.

Kontrakt

Nejprve základní pravidla, které musí Equals splňovat:

  • reflexe – A.Equals(A) = true,
  • symetrie – když A.Equals(B), pak B.Equals(A),
  • tranzitivita – když A.Equals(B) a B.Equals(C), pak i A.Equals(C),
  • konzistence – pokud nedojde ke změně objektů, pak A.Equals(B) musí dávat stále stejnou hodnotu,
  • A.Equals(null) = false,
  • metoda nesmí vyhazovat žádnou výjimku,

Doporučení:

  • potomek by měl volat base.Equals(), pokud je to možné,
  • měly bychom overridovat též GetHashCode(),
  • pokud implementujeme IComparable, měli bychom overridovat Equals(),
  • měly bychom vytvořit i strong-typed overload Equals().
Operátory ==, !=

Zároveň s override metody Equals() je vhodné přetížit i operátory == a !=, jejichž základní implementace ověřuje opět referenční shodu. Implementace je jednoduchá, prostě se odkážeme opět na statické Object.Equals()

Implementace

Ukažme si tedy běžnou implementaci pro business-object, u kterého nám stačí shoda ID.

public virtual bool Equals(BusinessObjectBase obj)
{
   if ((obj == null) || (this.GetType() != obj.GetType()))
   {
      return false;
   }
   if (!Object.Equals(this.ID, obj.ID))
   {
      return false;
   }
   return true;
}
public override bool Equals(object obj)
{
   if (obj is BusinessObjectBase)
   {
      BusinessObjectBase bob = obj as BusinessObjectBase;
      return this.Equals(bob);
   }
   return false;
}
public static bool operator ==(BusinessObjectBase objA, BusinessObjectBase objB)
{
   return Object.Equals(objA, objB);
}
public static bool operator !=(BusinessObjectBase objA, BusinessObjectBase objB)
{
   return !Object.Equals(objA, objB);
}
public override int GetHashCode()
{
   return this.ID;
}

Samotné porovnávání členských hodnot, které determinují shodu, je opět vhodné nechat na statické metodě Object.Equals().

Ještě si ukažme, jak by měla vypadat implementace potomka, který by chtěl mimo ID hlídat ještě shodu na nějakou další hodnotu (doporučené volání base.Equals())

public override Equals(Order order)
{
   return base.Equals((BusinessObjectBase)order)
            && Object.Equals(this.Cena, order.Cena);
}
public override bool Equals(object obj)
{
   if (obj is Order)
   {
      Order order = obj as Order;
      return this.Equals(order);
   }
   return false;
}
Viz též