Minule jsme si ukázali postup jak obejít nutnost implementace konkrétní třídy pro implementaci jednoduchého interface, což může být v některých případech docela užitečné.
Dnes si ukážeme ještě jeden příklad využití stejného principu. Jedná se o anonymní implementaci IDisposable. Přestože je typicky tento interface implementován nějakým konkrétním objektem, který vyžaduje deterministický úklid (uvolnění používaných zdrojů, odregistrace událostí apod.), je možné využití tohoto interface zobecnit. Tím mám namysli to, že jakákoliv svým charakterem “úklidová” akce může být např. u nějakého API předávána “zapouzdřena” právě do podoby typu IDisposable místo např. jen obyčejné Action. Výhoda je v tom, že je pak z kódu na první pohled patrný účel takové akce, klient může použít klíčové slovo using apod.
Tohoto principu právě poměrně masivně využívá knihovna Reactive Extensions for .NET (Rx) (kde se interface IDisposable konkrétně používá pro rušení registrací IObservable zdrojů). Součástí této knihovny jsou k tomu implementované pomocné třídy v namespace System.Disposables. Disposables konkrétně u verze Rx pro .NET Framework 4.0 nebo Silverlight sedí v assembly System.CoreEx.dll. A protože jsou tyto třídy public, dají se, kromě toho že jsou využívány interně v knihovně samotné, použít i zvenku.
Základem je třída Disposable, jejíž implementace je právě slíbený nám již známý pattern:
namespace System.Disposables
{
internal sealed class AnonymousDisposable : IDisposable
{
#region member varible and default property initialization
private readonly Action dispose;
private bool isDisposed;
#endregion
#region constructors and destructors
public AnonymousDisposable(Action dispose)
{
this.dispose = dispose;
}
#endregion
#region action methods
public void Dispose()
{
if (!this.isDisposed)
{
this.isDisposed = true;
this.dispose();
}
}
#endregion
}
internal sealed class DefaultDisposable : IDisposable
{
#region member varible and default property initialization
public static readonly DefaultDisposable Instance = new DefaultDisposable();
#endregion
#region constructors and destructors
private DefaultDisposable() { }
#endregion
#region action methods
public void Dispose() { }
#endregion
}
public static class Disposable
{
#region action methods
public static IDisposable Create(Action dispose)
{
if (dispose == null)
{
throw new ArgumentNullException("dispose");
}
return new AnonymousDisposable(dispose);
}
#endregion
#region property getters/setters
public static IDisposable Empty
{
get { return DefaultDisposable.Instance; }
}
#endregion
}
}
(Pozn.: Implementace je uvedená přesně tak jak je obsažena v knihovně Rx. Pokud by jsme jí chtěli dát do vlastní assembly pouze pro interní použití uvnitř dané assembly (tj. nereferencovali System.CoreEx.dll), vložíme obě třídy AnonymousDisposable a DefaultDisposable jako private do třídy Disposable a tu uděláme pouze internal.)
Kromě metody Disposable.Create(), která umožňuje převést libovolnou akci do metody Dispose() vraceného anonymního IDisposable, tu máme rovnou navíc zabudovanou “logiku” pro to, aby se dispose akce volala maximálně 1x (proměnná isDisposed ve třídě AnonymousDisposable).
Dále je tu vlastnost Disposable.Empty, která vrací sigleton instanci IDisposable s prázdnou implementací metody Dispose(). To se může hodit v případě, že API obecně vyžaduje vrátit nějaké IDisposable (vracení hodnoty null se od API neočekává) a žádný úklid v dané situaci nutný není.
Kromě třídy Disposable obsahuje namespace System.Disposables další třídy implementující IDisposable, nejdůležitější jsou tyto:
- BooleanDisposable nedělá při vlastním Dispose() nic, ale umožnuje později přes vlastnost IsDisposed zjistit, že k volání Dispose() už došlo.
public sealed class BooleanDisposable : IDisposable
{
#region member varible and default property initialization
public bool IsDisposed { get; private set; }
#endregion
#region action methods
public void Dispose()
{
this.IsDisposed = true;
}
#endregion
}
public sealed class CancellationDisposable : IDisposable
{
#region member varible and default property initialization
private CancellationTokenSource cts;
#endregion
#region constructors and destructors
public CancellationDisposable() : this(new CancellationTokenSource()) { }
public CancellationDisposable(CancellationTokenSource cts)
{
if (cts == null)
{
throw new ArgumentNullException("cts");
}
this.cts = cts;
}
#endregion
#region action methods
public void Dispose()
{
this.cts.Cancel();
}
#endregion
#region property getters/setters
public CancellationToken Token
{
get { return this.cts.Token; }
}
#endregion
}
(Pozn.: Dvojice typů CancellationTokenSource a CancellationToken tvoří implementaci obecného cooperative cancellation patternu, který je používán od .NET Frameworku 4.0)
- CompositeDisposable je vlastně kolekce jiných IDisposable, na které se pak zavolá Dispose() při Dispose() na celé kolekci. Prvky kolekce lze předat do konstruktoru nebo přidávat i kdykoliv později. Pokud je nějaký prvek přidáván až po volaní Dispose() na kolekci, je místo přidání na něj rovnou také jen zavoláno Dispose().
public sealed class CompositeDisposable : ICollection<IDisposable>,
IEnumerable<IDisposable>, System.Collections.IEnumerable, IDisposable
{
#region member varible and default property initialization
private List<IDisposable> disposables;
private bool disposed;
#endregion
#region constructors and destructors
public CompositeDisposable(params IDisposable[] disposables)
{
if (disposables == null)
{
throw new ArgumentNullException("disposables");
}
IDisposable[] disposableArray = disposables;
for (int i = 0; i < disposableArray.Length; i++)
{
if (disposableArray[i] == null)
{
throw new ArgumentOutOfRangeException("disposables");
}
}
this.disposables = new List<IDisposable>(disposables);
}
#endregion
#region action methods
public void Clear()
{
lock (this.disposables)
{
foreach (IDisposable disposable in this.disposables)
{
disposable.Dispose();
}
this.disposables.Clear();
}
}
public bool Contains(IDisposable disposable)
{
lock (this.disposables)
{
return this.disposables.Contains(disposable);
}
}
public void Add(IDisposable disposable)
{
if (disposable == null)
{
throw new ArgumentNullException("disposable");
}
bool disposed = false;
lock (this.disposables)
{
disposed = this.disposed;
if (!this.disposed)
{
this.disposables.Add(disposable);
}
}
if (disposed)
{
disposable.Dispose();
}
}
public bool Remove(IDisposable disposable)
{
if (disposable == null)
{
throw new ArgumentNullException("disposable");
}
bool removed = false;
lock (this.disposables)
{
if (!this.disposed)
{
removed = this.disposables.Remove(disposable);
}
}
if (removed)
{
disposable.Dispose();
}
return removed;
}
public void CopyTo(IDisposable[] array, int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (arrayIndex < 0 || arrayIndex >= array.Length)
{
throw new IndexOutOfRangeException();
}
lock (this.disposables)
{
Array.Copy(this.disposables.ToArray(), 0, array, arrayIndex,
array.Length - arrayIndex);
}
}
public void Dispose()
{
lock (this.disposables)
{
if (!this.disposed)
{
this.disposed = true;
this.Clear();
}
}
}
public IEnumerator<IDisposable> GetEnumerator()
{
lock (this.disposables)
{
return ((IEnumerable<IDisposable>)this.disposables.ToArray()).GetEnumerator();
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
#region private member functions
public int Count
{
get { return this.disposables.Count; }
}
public bool IsReadOnly
{
get { return false; }
}
#endregion
}
- MutableDisposable obdoba CompositeDisposable, ale umožňuje opakovaně nastavovat pouze jeden aktuální IDisposable prvek. Na tento prvek je Dispose() volán buď při výměně za jiný prvek nebo při volání Dispose() na celém objektu. Prvek se nastavuje kdykoliv ale až po založení objektu. Pokud je prvek měněn až po volaní Dispose() na objektu samotném, je na prvek rovnou voláno Dispose().
public sealed class MutableDisposable : IDisposable
{
#region member varible and default property initialization
private IDisposable current;
private bool disposed;
private object gate = new object();
#endregion
#region action methods
public void Dispose()
{
lock (this.gate)
{
if (!this.disposed)
{
this.disposed = true;
if (this.current != null)
{
this.current.Dispose();
this.current = null;
}
}
}
}
#endregion
#region property getters/setters
public IDisposable Disposable
{
get { return this.current; }
set
{
bool disposed = false;
lock (this.gate)
{
disposed = this.disposed;
if (!disposed)
{
if (this.current != null)
{
this.current.Dispose();
}
this.current = value;
}
}
if (disposed && (value != null))
{
value.Dispose();
}
}
}
#endregion
}
Ještě asi dlužím alespoň nějaký jednoduchý příklad použití, co třeba tohle:
Používáme nějakého klienta, který má velmi pomalé ukončování (já jsem se s tímto problémem setkal u jednoho COM objektu). Co můžeme udělat je volat jeho dispose akci asynchronně bez zbytečného čekání. Pomoci třídy Disposable to provedeme takto:
public class ClientWithExpensiveDispose : IDisposable
{
//
public void Dispose()
{
//Expensive dispose
}
}
void Main()
{
var client = new ClientWithExpensiveDispose();
using (Disposable.Create(() => Task.Factory.StartNew(client.Dispose)))
{
//Use client
}
//Other work
}