Weak Delegates in CSharp
Problem
This idea has always plagued me -- how do I create weak delegates in C#?
There is a problem with some of the code in my current engine where I want a short-lived entity to subscribe to a long-lived controller. When the entity loses ref, by default, the controller will store a reference to any methods on the entity that have subscribed to the controller via a strong reference in the closure. Below is some code that solves this.
Solution
The premise of the code below is pretty simple, and you've probably already seen it before. I
store a WeakReference
to the object, and a separate variable that tracks which method to call.
This eliminates any strong reference to the object directly, and thus, it can be garbage collected.
All the extra stuff is just syntactical fluff.
There are two tricks I do with this:
1) In order to make calling Invoke clean, I do it on the fly in a property. That allows me to write code like:
var ref = new WeakDelegate<Action>(MyMethod);
if (ref.IsAlive)
ref.Invoke(1,2,3);
2) In order to make it even cleaner, I provide a Wrap
method that transparently wraps code to the same
delegate type using lambdas. That allows me to do:
var ref = WeakDelegate.Wrap<Action>(MyMethod);
ref(1,2,3);
The above code throws ObjectDisposedException
s if the target object is no longer alive in both cases, so it's
best to either check for those, or check IsAlive
before calling.
Code
public class WeakDelegate<TDelegate> : WeakDelegate
where TDelegate : class
{
private readonly WeakReference _object;
private readonly MethodInfo _method;
static WeakDelegate()
{
if (!typeof(TDelegate).IsSubclassOf(typeof(Delegate)))
throw new InvalidOperationException(typeof(TDelegate).Name + " is not of type Delegate");
}
public WeakDelegate(TDelegate action)
{
var dl = action as Delegate;
if (dl.Method.GetCustomAttributes(typeof(CompilerGeneratedAttribute), true).Length > 0)
throw new InvalidOperationException("Delegate must not have its own closure, and must be a class-method");
_object = new WeakReference(dl.Target, false);
_method = dl.Method;
}
public static TDelegate Wrap(TDelegate action)
{
return WeakDelegate.Wrap<TDelegate>(action);
}
public TDelegate Invoke
{
get
{
if (!this.IsAlive)
throw new ObjectDisposedException("Weak Target");
return Delegate.CreateDelegate(typeof(TDelegate), _object.Target, _method) as TDelegate;
}
}
public bool IsAlive
{
get
{
return _object.IsAlive;
}
}
public bool IsSameAs(TDelegate other)
{
if (this.IsAlive)
{
var otherDlg = other as Delegate;
return object.ReferenceEquals(_object.Target, otherDlg.Target) && _method == otherDlg.Method;
}
return false;
}
}
public class WeakDelegate
{
protected WeakDelegate(){}
public static TDelegate Wrap<TDelegate>(TDelegate weakMethod)
where TDelegate : class
{
var dlgt = weakMethod as Delegate;
var inst = new WeakDelegate<TDelegate>(weakMethod);
//Collect params
var expParams = new List<ParameterExpression>();
foreach(var param in dlgt.Method.GetParameters())
{
expParams.Add(Expression.Parameter(param.ParameterType, param.Name));
}
//Write wrapper expression
var expInst = Expression.Constant(inst);
var invoker = Expression.Property(expInst, "Invoke");
var call = Expression.Invoke(invoker, expParams);
return Expression.Lambda(typeof(TDelegate), call, expParams).Compile() as TDelegate;
}
}