Article Image
read

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 ObjectDisposedExceptions 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;
        }
    }
Blog Logo

Christopher LaPointe


Published

Interested in Related Posts from this Site?

Persistent Terminal in VSCode Remote Session

September 19, 2020: I've been moving around a bit lately (Nowhere far... since COVID-19 and all), and have...

Simple systemd service

July 13, 2018: # Simple systemd service There aren't very many simple examples on the internet of simple...

Weak Delegates 2

February 08, 2016: # Weak Delegates 2 After toying with weak delegates a bit more, I found that...

Events and Expressions

March 13, 2015: Events and Expressions I ran into a scenario recently where I had a C# Action...

Image

Chris LaPointe

Another site of Code

Back to Overview