Skip to content

Weak Package

kobi2294 edited this page Apr 17, 2019 · 1 revision

The Weak Delegates package

Overview

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.

Example

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.


Use Cases

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:

Member Delegates

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 reflection MethodInfo object of the delegate method.

Static Delegates

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

Closures

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.

Which object should be passed as owner

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.

What if I pass null as owner

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.

Interfaces

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

Classes

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>

Shortcuts

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>