Serializing delegates in Unity

Delegates are one of the nicer features of C#. They’re essentially high-level and type-safe references to methods. But, like pretty much every fancy C# feature, delegate values are not serializable by default. If you hold a reference to a delegate, it will be lost during code hot-reload. I will show you a way to serialize them and explain how they work.

Note: This solution is inspired by the uFAction tool in Unity, which I am not affiliated with. Go check it out here.

Delegate introduction

As always, feel free to skip the introductory section if you wish.

Declaring a delegate looks like this:

You can then use this delegate as such (C# non-Unity code):

A delegate can also point to an anonymous function or a lambda function:

Note that lambda functions are not interchangeable with delegates. Lambda functions are declared by:

The last type is always the return type, the rest are the parameter types. This is equivalent to the following:

But they’re not exactly the same. An interesting discussion about it can be found here.

For all intents and purposes, you can be rest assured that you can assign delegate references to lambda functions or vice-versa, and you can look the other way and pretend they’re the same.

A simpler distinction is that if you use the lambda syntax you don’t have to define a new type whenever you expect a function as a parameter. For example, one possible implementation of the Where extension method could look like this:

(see also: Having fun with yield).This allows us to achieve the same thing in a number of ways:

All four calls to Where will produce exactly the same result. Notice how in the last example we use IsAdult without brackets or any parameters – this is because we pass the method itself and not its result.

You can now see how delegates are very, very useful and powerful. In your face, Java.

Okay, I love delegates… now what?

The solution to our problem is actually the hybrid result of two solutions. I haven’t found one solution that achieves both.

Note that for the sake of simplicity, the following source code uses  System.Action. This is a delegate defined by the standard C# library, and it is a simple void delegate without any parameters.  The final solution solves the problem for all delegates.

Solution A: The standard C# way

The code to serialize a delegate is actually pretty straight-forward, but not intuitively easy. We can use the default serialization of C# to do it:

This will first serialize the delegate into an array of bytes and then recreate the delegate again using that array.

So, there we go – we serialized a delegate! If we need serialization, can inherit from ISerializationCallbackReceiver and recreate all delegates using the byte array! So, are we done?

Unfortunately, no.

The Target of a delegate

A delegate is more than just the name of a method. It also contains information about the method, such as its return type. More importantly, it contains the target of the delegate. And, for C# to be able to serialize a delegate, its target must also be serializable by C# standards – i.e. it needs to be marked as [Serializable]. Even though UnityEngine.Object classes are serializable in Unity, they’re not marked with the [Serializable] attribute and, consequently, we cannot serialize delegates that belong to any Unity built-in type using Solution A!

To further explain what a target is, consider this example:

The above program will perform as expected. The purpose is to demonstrate that the delegate reference to PrintInfo needs to remember which DelegatingClass object it is called on. This makes sense, since PrintInfo accesses members that belong to a very specific DelegatingClass object. This object is the Target of the delegate.

However, there is no such requirement for PrintStaticInfo. In fact, static methods aren’t called on any object, and thus delegates to static methods don’t have a Target.

This is the output on the console:

No target is printed the second time, because the target is null.

Solution B: Using reflection

Fortunately, C# provides a way to get delegates that point to methods by their method name, using Reflection. Reflection is a special toolset that allows us to perform all sorts of magic stuff that isn’t normally accessible via standard language features. For example, we could use Reflection to access private fields of other classes.

The use of Reflection is generally discouraged, unless there is absolutely no way to get what we want via other means. It is also very slow. Neither of these issues should concern us, however. Serialization and deserialization have always been valid excuses for Reflection.

The same output of the target demonstration can be achieved with this code:

Even though the result appears to be the same, what goes on in the background is completely different.

The problem with Solution B is that Unity does not keep the original references after serialization for any type that does not derive from UnityEngine.Object. This will not work for non-Unity objects, for two reasons:

  •  The original target will be “forgotten” after Unity does its serialization magic.
  • Polymorphic serialization in Unity is still impossible. We cannot use System.Object to store all kinds of references, because after serialization the method will be searched for on System.Object rather than the actual type of the target.
Final solution: A + B combined

The final solution is to combine both solutions into one. If the target is derived from UnityEngine.Object, we use Solution B. If not, we use Solution A. Pretty simple!

Note how in the event that the input Action is null, we don’t just set _methodName and _serialData to null, but we assign empty objects to them. This is because, as we have seen numerous times before, Unity does not serialize null references for any object that does not derive from UnityEngine.Object… unless the class or the reference itself is not serializable, in which case it will always be null.  If I had set their values to null, the result would have been the same; _serialData would have become an array of size 0, and checking it for null after deserialization would always return false. Confusing? Probably so, but the important thing is that the above code works. And that’s all that matters.

Now, using this solution requires implementing ISerializationCallbackReceiver. Refer to this article for more information about it.

If the target is derived from UnityEngine.Object, we will save the method name instead of its binary data and then recreate the delegate using that and the reference to the target. If the delegate is to a static method or to a method that belongs to a class that does not derive from UnityEngine.Object, we convert the body of the method into an array of bytes and then recreate the delegate by deserializing it.

Working with non-simple delegates.

The solution is very similar, we just use a generic class with some extra added complexity.

The following solution also includes the previous working solution by deriving SerializableAction from SerializableDelegate<Action> (lines 67-68).

Unfortunately, C# doesn’t provide a legitimate way to enforce a generic constraint to only take delegates as a type parameter. The easiest way to do it was stolen from here (lines 16-20). We just check inside the static constructor (called the first time that class is used) and the CreateDelegate() method for the type and throw an exception if the parameter is not a delegate. The class will keep throwing exceptions if the user dares to use this with any type that’s not a delegate.

The existing constraint (line 7) is required so that CreateDelegate() can return null if it has to.

Finally, like we have seen in this article, there is an extra complication when we want to serialize generic classes. We need to create our own non-generic class that inherits from it, and remember to mark it as serializable (I always forget that).

Here’s another usage example that uses a complex delegate.

The usage is still simple enough, but the step in lines 6-7 must be followed in addition to the rest of the highlighted lines. Otherwise, your delegate will still fail to serialize.

Things to keep in mind:
  • You still need to remember to mark your own classes as [Serializable] if you plan to use them as delegate targets.
  • Anonymous delegates and lambda functions are not exactly anonymous. They are still named by the compiler using a cryptic name that is hidden to us unless we try to print it. As such, Solution B still works with anonymous delegates and lambda functions.
  • Invoking delegates might be slower than invoking methods, although the difference would be really tiny. They won’t ever pose a problem. The difference might be more apparent on mobile platforms, but I haven’t been able to run any tests to verify that. However, the chance of them being a bottleneck is still small.

Leave a Reply