Monday, 31 July 2017

Delegate Types Conversions

The (non) equivalence between different delegates with the same signature can be a source of confusion. I'm not going to talk in this post about another related source of confusion with delegates, Covariance and Contravariance (pointing delegates to methods with "similar" signatures), probably I'll dedicate another short post to that topic.

Since the advent of the Func and Action generic kind of delegates, it's quite unusual (and advised against) to create your own delegate types, but you will still find them, mainly coming from old code. So let's say that you want to assign an instance of a custom delegate type to one of the generic ones, like this:

delegate string FormatterDelegate(string st);
...
class Formatter
 {
  private string formatItem;
  
  public Formatter(string formatItem)
  {
   this.formatItem = formatItem;
  }
  public string Wrap(string st)
  {
   return this.formatItem + st + this.formatItem;
  }
}

var formatter = new Formatter("--");

FormatterDelegate formatterDeleg = formatter.Wrap;

//this line won't compile
Func<string, string> formatterDeleg2 = formatterDeleg;

The last line will not compile. Though their signatures are the same (receive a string an return a string), Func<string, string> and FormatterDelegate are completely different types and you can not assign one to another. So the obvious solution is to create a Func<string, string> delegate that calls to FormatterDelegate. I used to do this:

Option 1:

Func<string, string> formatterDeleg2 = (st) => formatterDeleg(st);

Somehow the other day I came across some discussions ([1] and [2]) on this topic and there are other similar ways to address this issue.

Option 2

Func<string, string> formatterDeleg2 = new Func<string, string>(formatterDeleg);

Option 3

Func<string, string> formatterDeleg2 = formatterDeleg.Invoke;

Option 3 made quite sense to me, but I have to admit that option 2 pretty surprised me. I didn't know that the compiler allowed to pass a delegate to a delegate constructor. In the end the compiler generates the same IL code for option 2 and option 3.

Both cases are similar in performance terms to Option 1. In all of them the call to the code in the original delegate goes through an additional level of indirection. This is clear for Option 1, where you are invoking an anonymous method that in turn calls to the original delegate. In Options 2 and 3 the MethodInfo of the Func delegate will point to the Invoke method of the original delegate, so you also have a "double hop". Option 1 creates a closure to hold the reference to the first delegate, so in the end will have some extra (but minimum) cost.

Option 4

. In the discussion they came up with a different technique that avoids the extra level of indirection:

formatterDeleg2 = (Func<string, string>) Delegate.CreateDelegate(typeof(Func<string, string>), formatterDeleg.Target, formatterDeleg.Method);

With Option 4 you directly assign the method referenced from the original delegate to the new delegate, so the indirection is avoided. Notice that they mention that this will cause problems with multicast delegates.

I was going to put up some test code as a gist in github, but it is down.

No comments:

Post a Comment