Tag Archives: .NET Interfaces

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í

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éž

IComparable

Rozhraní IComparable, popř. strong-typed IComparable<T> slouží jako podpora pro porovnávání prvků, zejména pro jejich řazení (metody Sort). K implementaci je jediná instanční metoda CompareTo(), jejíž výsledek má být:

  • nula, pokud jsou instance stejné (z hlediska porovnání rovnocenné),
  • záporný, pokud je je instance (this) menší než parametr metody (při vzestupném řazení bude this dříve než parametr),
  • kladný, pokud je instance (this) větší než parametr metody (při vzestupném řazení bude parametr dříve než this).

Tato konvence vyplynula z metodiky používané pro porovnávání čísel prostým odečtením this-param.

Dále musíme dodržet následující definiční pravidla, na která se často zapomíná:

  • null < cokoliv jiného,
  • null = null (neimplementuje se)

Samozřejmě taky všechna klasická pravidla:

  • když A = B a B = C, tak A = C,
  • když A < B a B < C, tak A < C,
  • když A < B, tak B > A,

Vzorová implementace obou rozhraní by pak mohla vypadat takto:

 public class MyComparableClass : IComparable, IComparable<MyComparableClass>
{
   // Strong-typed implementace IComparable<T>
   public int CompareTo(MyComparableClass other)
   {
      if (other == null)
      {
         return 1;
      }
      // naše implementace srovnání, například
      return this.Value.CompareTo(other.Value);
   }
   // Implementace IComparable
   public int CompareTo(object obj)
   {
      if (obj == null)
      {
         return 1;
      }
      if (obj is MyComparableClass)
      {
         return CompareTo(obj as MyComparableClass);
      }
      throw new ArgumentException("obj musí být typu MyComparableClass", "obj");
   }
}

Doporučuje se, aby třída, která implementuje IComparable, overridovala i metodu Equals.

ICloneable

ICloneable je rozhraní (interface) .NET Frameworku pro podporu klonování objektů, tedy pro vytváření nových instancí třídy se stejnými hodnotami, jako má instance existující.

Základním kamenem pro implementaci ICloneable je protected metoda Object.MemberwiseClone(), která vytvoří shallow-copy (mělkou kopii) objektu. Mělkost kopie spočívá v tom, že jsou zkopírovány pouze přímé hodnoty objektu, což u referenčních typů má za následek pouze vytvoření nových referencí na stejnou instanci.

Korektní implementace rozhraní ICloneable by měla vypadat asi takto:

class MyCloneableClass : System.ICloneable
{
   // Explicitní implementace metody rozhraní.
   // Potřebná pro klienty ICloneable, ale neviditelná 
   // běžné klienty MyCloneableClass
   object ICloneable.Clone()
   {
      // prostě vrátíme hodnotu naší implementace
      return this.Clone(); 
   }
   // Silně-typovaná metoda Clone
   public virtual MyCloneableClass Clone()
   {
      // běžná memberwise-kopie vlastních hodnot
      MyCloneableClass x = this.MemberwiseClone() as MyCloneableClass;
      // následuje deep-copy všeho, co je potřeba
      x.somethingDeep = this.somethingDeep.Clone();
      return x;
   }
}

Pokud ICloneable korektně implementujeme u všech našich tříd, bude vše velmi primitivní. Vlastní klonování bude probíhat v kaskádě, resp. procházením stromu do hloubky. Problém samozřejmě nastane, když budeme mít člen referenčního typu, který neimplementuje ICloneable.