-
Notifications
You must be signed in to change notification settings - Fork 1
Weak Package
This package contains classes that can be used as replacement for various delegate types such as Action
, Func
, events, and multicast delegates. The difference between them and the "normal" delegates, is that they hold a weak reference to their target, so that they do not force that target to stay in memory, and allow it to be garbase collected.
Here is a typical demo of the WeakAction
wrapper. In this example we have two classes. A Listener
object and a 'Performer' object. The Listener
has a ListenTo
Method which takes a Performed
and registers a callback in it. The Performer
exposes a OnPerformCallback
method takes a delegate and keeps a reference to it, calling it whenever the Perform
method is called.
This is the Listener:
public class Listener
{
public void ListenTo(Performer performer)
{
performer.OnPerformCallback(this, () =>
{
Console.WriteLine("Listener reacting to performer object");
});
}
}
And this is a classic implementation of the performer:
public class Performer
{
Action _callback;
public void OnPerformCallback(Action callback)
{
_callback = callback;
}
public void Perform()
{
_callback.Invoke();
}
}
If we do not use weak delegates, as long as the performer instance is alive, the listener can not be garbage collected, even if it is not required anymore because the performer holds a delegate which has a strong reference to the listener.
Instead, we implement the performer using WeakAction
:
public class Performer
{
WeakAction weakCallback;
public void OnPerformCallback(object owner, Action callback)
{
weakCallback = callback.ToWeak(owner);
}
public void Perform()
{
Console.WriteLine($"IsAlive={weakCallback.IsAlive}");
weakCallback.Execute();
}
}
This way, the performer holds a WeakAction
which by itself only holds a weak reference of the listner. The listener may be garbage collected, and in that case, when the performer executes the weak delegate, it will not really call the delegate because the target was already collected.
Here is a program that demonstrates this:
public static class WeakDelegateDemo
{
public static void Run()
{
var listener = new Listener();
var performer = new Performer();
listener.ListenTo(performer);
Console.WriteLine("Calling delegate");
performer.Perform();
GC.Collect();
Console.WriteLine("Calling delegate after garbage collection while still referencing listener");
performer.Perform();
listener = null;
GC.Collect();
Console.WriteLine("Calling delegate after nulling listener and garbage collection");
performer.Perform();
Console.WriteLine("Demo Completed");
}
}
We run the Perform
method 3 times, forcing garbage collection between each call. The second garbage collection is performed after we remove all references to the listener, which causes it to be garbage collected. If you run this example, you will see that on the third Perform
the listener is not called.
All weak delegate classes share common logic and functionality. There are several functionality differences between them, but as far as being "weak" is concerened, they all function the same. There are several use cases for all of them:
The most common use case is for member delegates. That means that the delegate method runs in the context of a specific object known as the Target object. The purpose of the Weak Delegate is to avoid holding a strong reference to the target object, so that it may be garbage collected if there are no other references to it. That means that when the time comes to invoke the delegate, it's target may already have been garbase collected. If that is the case, the weak delegate is considered "dead", and of course, it wont run.
- Use the property
IsAlive
To check if the target was garbage collected - Use the property
Target
to get a reference to the target (if it was not garbage collected yet - otherwise you will get null) - Use the property
Method
to get a reflectionMethodInfo
object of the delegate method.
Weak Delegates may wrap a static delegate. Static delegates do not really need require the "weak delegate" feature since they do not have a target (their target is null), so they do not cause an object not to be garbage collected. However, when you develop a class that accepts an action (callback) from the user, and hold a reference to it, you do not know in advance if you will get a static delegate or not, so you need to cater for all possibilities. Therefore, the WeakDelgate
class recognizes static actions and ignores their target, functioning like a normal action in that case.
A static delegate never "dies" because it has no target that may be garbage collected.
- Use the property
IsStatic
To check if the delegate is static or not. - The
IsAlive
property will always return True for static delegates - The
Target
property will always return null for static delegates
One of the most complex cases, and also one of the most common cases, is Closures. To understand what a closure is, consider the next example:
public static void RunWithClosure()
{
string str = "Hello";
Action action = () =>
{
Console.WriteLine($"{str} Closure action running");
};
/// ... more code that passes the action to other objects
}
The Action
action uses a local variable - str. That means that the delegate has access to a variable that was defined inside the RunWithClosure
method. Of course, this variable ceases to exist as soon as the RunWithClosure function ends, but the delegate may still access it. In order to make it work, the C# compiler creates a new class that captures the str variable as a field member, and the delegate becomes a member method of that class. It creates an instance of that new class, and it becomes the target of the delegate.
The problem is that there are no other references to that instance. This entire class is automatically generated by the compiler, your code does not use it, so you obviously do not create a reference to it. With "normal" delegates, this is not a problem, since the delegate holds a strong reference to the target. But since we now create a Weak Delegate from this action, and there are no other references to it's target - the weak delegate target will be garbage collected instantly.
To prevent this behaviour, all weak delegates allow you (in fact, requier you) to specify a "lifespan controller", A.K.A owner. The owner object is an alternative object to be used to determine if the weak delegate is dead or alive. The weak delegate will prevent the method target from being garbage collected, as long as the owner is still alive. It will hold a weak reference to the owner so that it may be garbage collected, and as soon as the owner is garbage collected, the weak delegate dies as well.
First realize that the owner will only be used if the delegate is a closure, since we can not rely on the real target in that case. In all other cases, we simply use the method target and ignore the owner. So if the action you create a weak wrapper to is not a lambda expression or annonimous method, than you may pass null
as owner or any other object, it simply has no effect.
If the action is created using a lambda expression, you should simply pass this
as owner. This way, the lifespan of the delegate will be determined by the object containing the method, in which the delegate was defined, which is the expected behavior.
In that case we will make a strong reference to the action target, so it does not get garbage collected, and the weak delegate becomes a strong delegate and may cause small memory leak (note that the object containing the closure will still be garbage collected, since we only hold a strong reference to the tiny instance created automatically by the compiler, not the entire containing object). Of course, it is not recommended. 😃
- Use the
IsClosure
property to check if the delegate is a closure - Use the
Owner
property to access the owner object - Note that Closures are never static, even if they are created inside static methods.
Name | Description |
---|---|
IWeakActionWithParam1 |
Any weak action that has a single parameter |
IWeakActionWithParam2 |
Any weak action that has 2 parameters |
IWeakFunc |
Any weak function |
IWeakFunc<TRes> |
The generic variant of IWeakFunc , with specific return type |
Name | Description |
---|---|
WeakDelegate |
Abstract class that allows to combine a MethodInfo and a targer. To check if the target is alive and to execute it. |
WeakAction |
Weak version of Action
|
WeakAction<T> |
WeakAction with a single parameter |
WeakAction<T1, T2> |
WeakAction with two parameters |
WeakFunc<TRes> |
Weak version of Func<TRes>
|
WeakFunc<T, TRes> |
WeakFunc that accepts a single parameter |
WeakEvent |
Weak replacement to an event. You can add several listeners - each one is a WeakAction<object>
|
WeakEvent<T> |
Same as WeakEvent but with a specific type of event parameter, so listerens are WeakAction<object, T>
|
The Weaks
static class provides a set of extension methods, all called ToWeak
which create a Weak delegate wrapper for a specific delegate. For each delegate you need to provide an owner object, that controls the lifespan of the weak delegate in case it is a closure class (which otherwise will be garbase collected instantly).
From | To |
---|---|
Action |
WeakAction |
Action<T> |
WeakAction<T> |
Action<T1, T2> |
WeakAction<T1, T2> |
Func<TRes> |
WeakFunc<TRes> |
Func<T, TRes> |
WeakFunc<T, TRes> |