Trochę teoretycznego wstępu
Środowisko .NET charakteryzuje się tym, że samo zarządza cyklem życia obiektów. Garbage collector potrafi wykryć, które obiekty nie są już potrzebne i usunąć je z pamięci. Oczywiście jest to bardzo duże uproszczenie całego mechanizmu – szczegóły jego działania są bardzo dobrze wyjaśnione w opisywanej przeze mnie wcześniej książce "CLR via C#, 3rd edition" w rozdziale 21 – Automatic Memory Management (Garbage Collection).
Oprócz automatycznego zarządzania pamięcią CLR umożliwia także ręczne monitorowanie oraz kontrolę cyklu życia obiektów za pomocą GC handle table. Dodawanie oraz usuwanie wpisów do tej tabeli umożliwiają metody struktury GCHandle. Mechanizm ten jest szczególnie przydatny, kiedy komunikujemy się lub współpracujemy w jakiś sposób z kodem niezarządzalnym, ale część właściwości można także wykorzystać, gdy pracujemy tylko z kodem zarządzalnym – konkretnie chodzi mi tutaj o słabe referencje.
W dużej ogólności obiekt A musi posiadać referencję do obiektu B, aby mógł na nim wołać metody:

Diagram 1. Mocna referencja z obiektu A do obiektu B.
Sytuacja taka uniemożliwia jednak automatyczne usunięcie obiektu B z pamięci, kiedy nie jest on już potrzebny, a jedyną referencją, jaka go jeszcze trzyma przy życiu, jest referencja posiadana przez obiekt A. Czasami takie zachowanie nie jest pożądane i aby temu zaradzić, możemy posłużyć się klasą
WeakReference, która opakowuje strukturę GCHandle umożliwiając łatwiejsze jej wykorzystanie w kodzie.
W praktyce wygląda to następująco. Zamiast takiego kodu:
class ClassB {
public void Foo() { }
}
class ClassA
{
private ClassB _objectB;
public ClassA( ClassB objectB ) {
// mocna referencja z A do B
_objectB = objectB;
}
public void CallB() {
_objectB.Foo();
}
}
kod klasy A możemy napisać następująco z wykorzystaniem klasy WeakReference i jej właściwości Target:
class ClassA {
private WeakReference _weakObjectB;
public ClassA( ClassB objectB ) {
// słaba referencja z A do B
_weakObjectB = new WeakReference( objectB );
}
public void CallB() {
// pobierz referencję do obiektu B
var objectB = _weakObjectB.Target as ClassB;
if( objectB != null ) { // jeżeli obiekt B jest jeszcze w pamięci
objectB.Foo(); // to możemy zawołać na nim metodę Foo
}
}
}
I praktyczne zastosowanie
Opisany powyżej mechanizm można w praktyczny sposób wykorzystać podczas pracy z eventami. Spójrzmy na uproszczony model:
class Producer {
public event EventHandler<EventArgs> Event;
public void Produce() {
Console.WriteLine( "Producer.Produce - raising event" );
if( this.Event != null ) {
this.Event( this, EventArgs.Empty );
}
}
}
class Consumer {
public Consumer( Producer producer ) {
producer.Event += new EventHandler<EventArgs>( this.Consume );
}
private void Consume( object sender, EventArgs e ) {
Console.WriteLine( "Consumer.Consume - consuming event" );
}
}
static class SimpleWeakReferenceExample {
public static void Run() {
var producer = new Producer();
var consumer = new Consumer( producer );
producer.Produce();
consumer = null; // konsument nie jest już nam potrzebny
GC.Collect();
// consumer i tak przeżyje,
// ponieważ pośrednio referencję do niego trzyma consumer
producer.Produce();
}
}
Obiekt klasy Producer jest producentem zdarzeń, na które zapisuje się obiekt klasy Consumer. Tworzy się nam taka oto struktura:

Diagram 2. Diagram obiektów dla przykładu Producent-Konsument.
Pomimo tego, że konsument nie jest już potrzebny (linikja 28, czerwona strzałka na diagramie 2), to pośrednio poprzez
EventHandler jest powiązany z producentem i będzie żył tak długo, jak i on. Aby temu zaradzić, można np. zaimplementować w klasie Consumer interfejs IDisposable i wołać metodę Dispose, w której konsument wypisuje się ze zdarzenia Event producenta. Niestety czasami w bardziej skomplikowanych przypadkach, nie jest to takie łatwe – kilkukrotnie spotkałem się z takimi sytuacjami. Pomóc może nam poprzednio opisany mechanizm słabych referencji.
Błędny WeakEventHandler z książki "CLR via C#, 3rd edition"
W książce "CLR via C#, 3rd edition" znajduje się przykładowa implementacja klasy WeakEventHandler bazująca na bardziej ogólnej klasie WeakDelegate<TDelegate>, niestety posiada ona bardzo poważny błąd logiczny, którego – o dziwo – nikt do tej pory nie zgłosił. Problem opisałem w poprzednim wpisie i zgłosiłem go do autora.
Podstawowe założenia do implementacji WeakEventHandler<TEventHandler>
Klasa ma umożliwiać tworzenie słabych event handlerów dla trzech typów delegatów:
- podstawowego, generycznego
EventHandler<TEventArgs> - niegenerycznego
EventHandler - dowolnego delegata, którego sygnatura jest zgodna z jednym z dwóch powyższych (ten scenariusz dodałem po dyskusji z Jeffrey’iem Richterem – ma zapewnić wsparcie dla starszego typu eventów obecnych w dużych ilościach zwłaszcza w WinForms)
Nasza struktura obiektów będzie wyglądać następująco:

Diagram 3. Wymagane połączenia obiektów.
Przerywaną linią jest zaznaczona słaba referencja. Musimy także zająć się mocną referencją od producenta do naszego niebieskiego delegata – po usunięciu konsumenta, kiedy delegat już nie jest potrzeby, powinien sam wyrejestrować się ze zdarzenia.
Implementacja WeakEventHandler<TEventHandler>
Sporo eksperymentowałem z różnymi implementacjami, aż w końcu doszedłem do czegoś takiego:
public sealed class WeakEventHandler<TEventHandler>
where TEventHandler : class
{
private delegate void OpenEventHandler<TTarget, TEventArgs>
( TTarget target, object sender, TEventArgs eventArgs )
where TTarget : class
where TEventArgs : EventArgs;
private readonly WeakReference _weakTarget;
private readonly Delegate _openEventHandler;
private readonly Action<TEventHandler> _cleanUp;
private readonly TEventHandler _proxyHandler;
public WeakEventHandler(
TEventHandler eventHandler, Action<TEventHandler> cleanUp ) {
var d = eventHandler as Delegate;
if( d == null || d.Target == null )
throw new ArgumentException(
"Can't make weak event handler to static method.", "d" );
if( cleanUp == null )
throw new ArgumentNullException( "cleanUp" );
// store event handler target as a WeakReference
_weakTarget = new WeakReference( d.Target );
_cleanUp = cleanUp;
// extract the types from the event handler
var eventHandlerType = typeof( TEventHandler );
var targetType = d.Target.GetType();
var eventArgsType = eventHandlerType.IsGenericType
? eventHandlerType.GetGenericArguments()[ 0 ]
: eventHandlerType.GetMethod( "Invoke" )
.GetParameters()[ 1 ].ParameterType;
// create open event handler for fast calling of the real event handler
Type openEventHandlerType = typeof( OpenEventHandler<,> )
.MakeGenericType( eventHandlerType, targetType, eventArgsType );
_openEventHandler = Delegate.CreateDelegate(
openEventHandlerType, null, d.Method );
// create proxy handler that points to our proxy method
var proxyHandlerMethod =
typeof( WeakEventHandler<TEventHandler> )
.GetMethod(
"ProxyHandler",
BindingFlags.Instance | BindingFlags.NonPublic )
.MakeGenericMethod( targetType, eventArgsType );
_proxyHandler =
Delegate.CreateDelegate( eventHandlerType, this, proxyHandlerMethod )
as TEventHandler;
}
private void ProxyHandler<TTarget, TEventArgs>(
object sender, TEventArgs e )
where TTarget : class
where TEventArgs : EventArgs
{
var target = _weakTarget.Target as TTarget;
var openEventHandler = this._openEventHandler
as OpenEventHandler<TTarget, TEventArgs>;
// if target is still alive
if( target != null || openEventHandler == null )
openEventHandler( target, sender, e ); // call it
else
_cleanUp( _proxyHandler ); // else clean up
}
public static implicit operator TEventHandler(
WeakEventHandler<TEventHandler> weakEventHandler ) {
return weakEventHandler != null
? weakEventHandler._proxyHandler
: null;
}
}
Implementacja spełnia postawione wcześniej założenia, co ważne – jest relatywnie szybka – około 4-6 razy wolniejsza, niż bezpośrednie wywołanie delegata. Musimy także podać delegat cleanUp, który umożliwi wyrejestrowanie się ze zdarzenia i posprzątanie, kiedy _target zostanie już usunięty. Wszystkie niezbędne dane są wydobywane z oryginalnego eventHandlera, następnie tworzone są: tzw. open event handler – delegat, który umożliwia późniejsze szybkie wołanie metody na konsumencie oraz proxy event handler – delegat, który wskazuje na generyczną metodę ProxyHandler.
Rozszerzenia
Aby ułatwić używanie ww. implementacji, można skorzystać z dodatkowych extension methods:
public static class WeakEventHandlerExtensions {
// usage:
// p.SomeEvent += new SomeEventHandler( c.HandleEvent )
// .AsWeak( eh => p.SomeEvent -= c.HandleEvent );
public static TEventHandler AsWeak<TEventHandler>(
this TEventHandler eventHandler, Action<TEventHandler> cleanUp )
where TEventHandler : class {
return new WeakEventHandler<TEventHandler>( eventHandler, cleanUp );
}
// usage:
// p.SomeEvent += new EventHandler( c.HandleEvent )
// .AsWeak( eh => p.SomeEvent -= c.HandleEvent );
public static EventHandler AsWeak(
this EventHandler eventHandler, Action<EventHandler> cleanUp ) {
return new WeakEventHandler<EventHandler>( eventHandler, cleanUp );
}
// usage:
// p.SomeEvent += new EventHandler<SomeEventArgs>( c.HandleEvent )
// .AsWeak( eh => p.SomeEvent -= c.HandleEvent );
public static EventHandler<TEventArgs> AsWeak<TEventArgs>(
this EventHandler<TEventArgs> eventHandler,
Action<EventHandler<TEventArgs>> cleanUp )
where TEventArgs : EventArgs {
return new WeakEventHandler<EventHandler<TEventArgs>>(
eventHandler, cleanUp );
}
}
Użycie
// decision on event consumer side:
producer.Event += new EventHandler( this.HandleEvent )
.AsWeak( eh => producer.Event -= eh );
// decision on event producer side
// caution, with this construction it's not possible
// to deregister from the event
public class EventProvider {
private EventHandler<EventArgs> _event;
public event EventHandler<EventArgs> Event {
add {
this._event += value.AsWeak( eh => this._event -= eh );
}
remove {
this._event -= value;
}
}
public void Raise() {
this._event( this, EventArgs.Empty );
}
}
Jak widać z załączonych przykładów, użycie klasy WeakEventHandler poprzez extension methods AsWeak jest bardzo proste. Ciekawostką jest możliwość wykorzystania rozszerzeń także po stronie producenta i stworzenie słabego eventa – Jeffrey Richter dostrzegł w tym kodzie błąd – a mianowicie nie jest możliwe wyrejestrowanie konsument z eventa. Aby tego dokonać, należałoby zmienić lekko implementację części remove oraz dodać metody pozwalające na porównanie obiektów WeakEventHandler<TEventHandler< z obiektami TEventHandler.
Implementację klasy wraz z rozszerzeniami oraz opisywanymi przykładami można pobrać tutaj.


















