Delegations that Neter should know thoroughly

Posted by quiettech on Wed, 11 Dec 2019 01:01:13 +0100

This article will lead to several questions, and through examples to analyze the delegation and usage in C#as a starting point

For delegation I find that most people have the following questions, or may have encountered such questions in an interview:

  • Is a delegate equivalent to a C/C++ function pointer?
  • What exactly is a delegation?
  • What exactly is a delegation for?
  • What is the difference between a delegate and an anonymous function?
  • The relationship between delegation and events?


Let's start by declaring and using C++ function pointers:
The code is as follows:

#include <iostream>
using namespace std;

typedef int (*Foohandle)(int a,int b);

int fooMenthod(int a, int (*foohandle1)(int a,int b)) //callback
{
  return a + (*foohandle1)(2,3);//You can also write as foohandle1(2,3)
}

int add(int a,int b) {
  return a + b;
}

int multiply(int a, int b) {
  return a * b;
}

int main()
{
 Foohandle foohandle = add;
 int (*foohandle1)(int a, int b) = &add;
 cout << foohandle(2,3)<<endl;
 cout << foohandle1(2,3) << endl;
 cout << typeid(Foohandle).name() << endl;
 cout << typeid(foohandle).name()<<endl;
 cout << typeid(foohandle1).name() << endl;
 cout << fooMenthod(2, add)<<endl;
 cout << fooMenthod(2, multiply);
}

The output is as follows:

     

 

 

 

 

In the code, I declare two functions, add and multiply, and then declare the function pointer as typedef. Then I assign add to the foohandle variable of the function pointer type Foohandle, respectively. Then I assign a function pointer foohandle1 with two parameters and a return value of int in the form of &add, where (*foohandle1) is the function nameFinally, my output finds that they are of the same type as the output. Later, we define a fooMenthod function whose return value is int and one of the parameters is a function pointer. Then I call the add and multiply functions twice last, assigning them to each other. At this time, add and multiply are callback functions of the fooMenthod function, and the output will be twoThe impact of different implementations within a function
Then we can make a summary:

  • First, the function pointer is a memory address, pointing to the function's entry memory address
  • When the function pointer is a parameter of a function, it does decouple
  • Function pointers are obviously type insecure

Let's declare and use delegation again:

public delegate int Foohandle(int a, int b);
class Program
{
static void Main(string[] args)
{
 Foohandle foohandle = new Foohandle(add);
 Console.WriteLine(foohandle(2, 3));
 Console.WriteLine(foohandle.GetType().Name);
 Console.WriteLine(fooMenthod(2, add));
 Console.WriteLine(fooMenthod(2, multiply));
 Console.WriteLine($"foohandle Called function function name:{foohandle.Method.Name}");
 Console.WriteLine($"foohandle Return value type of the called function{foohandle.Method.ReturnType.ToString()}");
 Console.WriteLine("foohandle The called function parameter type and parameter name are:");
 Console.WriteLine($"Type:{foohandle.Method.GetParameters()[0].ParameterType},Name:{foohandle.Method.GetParameters()[0].Name}");
 Console.WriteLine($"Type:{foohandle.Method.GetParameters()[1].ParameterType},Name:{foohandle.Method.GetParameters()[1].Name}");
 Console.Read();
}

static int fooMenthod(int a, Foohandle foohandle) //The parameter function is passed to the callback function
{
 return a + foohandle(2, 3);
}

static int add(int a, int b)
{
 return a + b;
}

static int multiply(int a, int b)
{
 return a * b;
}
}

Output results:

   

 

 

 

 

     

Obviously, both declared and used in the same way as in c++, even the output is similar, but interestingly, foohandle is of type Foohandle, and I can actually output all the information about the function I'm calling from foohandle, including the function name, return value, parameter type and parameter name, and unlike c++, we have no direct operationSave the address as if it were safe?So what is the Foohandle type?

What is delegation?

First example:

namespace DelegateSample
{

  public delegate void FooHandle(int value);
class Program { static void Main(string[] args) { FooHandle fooHandle = new FooHandle(multiply); fooHandle(3); Console.WriteLine($"fooHandle.Target:{fooHandle.Target},fooHandle.Method:{fooHandle.Method},fooHandle.InvocationListCount:{fooHandle.GetInvocationList().Count()}"); Console.WriteLine("-----------------------------------------------------------------------------------------------------------------------------------"); FooHandle fooHandle1 = new FooHandle(new Foo().Add); fooHandle1.Invoke(3); Console.WriteLine($"fooHandle1.Target:{fooHandle1.Target},fooHandle1.Method:{fooHandle1.Method},fooHandle1.InvocationListCount:{fooHandle1.GetInvocationList().Count()}"); Console.Read(); } static void multiply(int a) { Console.WriteLine(a*2); } } public class Foo { public void Add(int value) { Console.WriteLine(value + 2); } } }

Let's look at the output:

Clearly, here is the simplest delegate declaration, instantiating the simplest scenario that initializes a delegate object and then invokes it
We don't care about the first line of the output. Obviously, once an object is instantiated, you can access three of the open public function members.
Target(object type), Method(MethodInfo type), and GetInvocationList function are parameterized functions that return a Delegate[]
In the above code, in fact, I specifically declare the delegate FooHandle outside the Program class. In fact, here we already know what the delegate is, instantiate the object, and be able to declare it outside the class. In fact, it is a class in nature. We verify it by decompiling:

 

 

 

 

 

 

Probably so, the pseudocode is as follows:

public class FooHandle: MulticastDelegate
{
  public FooHandle(object @object,IntPtr menthod);//Construction method

  void Invoke(int value)//Call the delegate, which is compiled and given by the Common Language Runtime delegate Special methods provided

  void EndInvoke(System.IAsyncResult asyncResult)// Compiled Common Language Runtime to MulticastDelegate Special methods provided

  // Compiled Common Language Runtime to MulticastDelegate Special methods provided
  void BeginInvoke(int value,System.AsyncCallback callback, object obj) 
}

 

We can see that a compiled FooHandle is a class that inherits MulticastDelegate, and the inheritance chain relationship is like this in msdn:



   

   

And we found that the three function members exposed above are all from the Delegate class and compiled to generate several special methods provided by the common runtime. The Invoke method is very clear to us that we are calling delegates. Let's first look at the delegate initialization. By looking at the source code of Delegate, we see that Delegate has two constructors:

1. The delegate object initialization constructor is an instance function:

[SecuritySafeCritical]
protected Delegate(object target, string method)
{
  if (target == null)
  {
    throw new ArgumentNullException("target");
  }
  if (method == null)
  {
    throw new ArgumentNullException("method");
  }
  if (!BindToMethodName(target, (RuntimeType)target.GetType(), method, (DelegateBindingFlags)10))
  {
    throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTargMeth"));
  }
} 

 

2. The delegate object initialization constructor is a static function:

[SecuritySafeCritical]
protected Delegate(Type target, string method)
{
  if (target == null)
  {
    throw new ArgumentNullException("target");
  }
  if (target.IsGenericType && target.ContainsGenericParameters)
  {
    throw new ArgumentException(Environment.GetResourceString("Arg_UnboundGenParam"), "target");
  }
  if (method == null)
  {
    throw new ArgumentNullException("method");
  }
  RuntimeType runtimeType = target as RuntimeType;
  if (runtimeType == null)
  {
    throw new ArgumentException(Environment.GetResourceString("Argument_MustBeRuntimeType"), "target");
  }
  BindToMethodName(null, runtimeType, method, (DelegateBindingFlags)37);
}

Last method called together:

//call CLR Internal Code
[MethodImpl(MethodImplOptions.InternalCall)]
[SecurityCritical]
private extern bool BindToMethodName(object target, RuntimeType methodType, string method, DelegateBindingFlags flags);

Although we can't see the implementation of the BindToMethodName method, it's already obvious that the delegate object initialization constructor is a static function that passes in the target parameter of the first object of BindToMethodName to be null, so we've probably implemented the previous pseudocode constructor like this:

Pseudocode section:

internal object _target//Target object;
internal IntPtr _methodPtr//Target Method;
internal IntPtr _methodPtrAux//Used for judgment Target Is it empty;

//foolHandle Construction method implementation:
public FooHandle(object @object,IntPtr menthod)
{
  _methodPtr=menthod;//multiply
  _methodPtrAux=1;//As long as it is not equal to nul

}

//foolHandle1 Construction method implementation:
public FooHandle(object @object,IntPtr menthod)
{
  _methodPtr=menthod//Add
  _methodPtrAux=0//by null
  _target=foo;

}

 

Delegate Target attribute source code section:

[__DynamicallyInvokable]
public object Target
{
  [__DynamicallyInvokable]
  get
  {
    return GetTarget();
  }
}

[SecuritySafeCritical]
internal virtual object GetTarget()
{
  if (!_methodPtrAux.IsNull())
  {
    return null;
  }
  return _target;
}

The method of getting a Method does not expand, that is, by reflection. We already know what the Target and Method attributes are, and we find that we haven't talked about the GetInvocationList method yet?We know that delegates support multicast delegates, which is roughly the same. Modify the above code to:

namespace DelegateSample
{
   public delegate void FooHandle(int value);
class Program { static void Main(string[] args) { FooHandle fooHandle = new FooHandle(multiply); fooHandle(3); Console.WriteLine($"fooHandle.Target:{fooHandle.Target},fooHandle.Method:{fooHandle.Method},fooHandle.InvocationListCount:{fooHandle.GetInvocationList().Count()}"); Console.WriteLine("----------------------------------------------------------------------------------------------------------------"); FooHandle fooHandle1 = new FooHandle(new Foo().Add); fooHandle1.Invoke(3); Console.WriteLine($"fooHandle1.Target:{fooHandle1.Target},fooHandle1.Method:{fooHandle1.Method},fooHandle1.InvocationListCount:{fooHandle1.GetInvocationList().Count()}"); Console.WriteLine(); Console.WriteLine("--------------------------------------------------Add Code------------------------------------------------------"); FooHandle fooHandle2 = new FooHandle(new Program().Minus); Console.WriteLine($"fooHandle2.Target:{fooHandle2.Target},fooHandle1.Method:{fooHandle2.Method},fooHandle1.InvocationListCount:{fooHandle2.GetInvocationList().Count()}"); fooHandle2(2); Console.WriteLine("----------------------------------------------------------------------------------------------------------------"); FooHandle fooHandle3 = null; fooHandle3 += fooHandle; fooHandle3 =(FooHandle)Delegate.Combine(fooHandle3,fooHandle1);//Equivalent to fooHandle3+=fooHandle1; fooHandle3 += new Program().Minus; Console.WriteLine($"fooHandle3.Target:{fooHandle3.Target},fooHandle3.Method:{fooHandle3.Method},fooHandle3.InvocationListCount:{fooHandle3.GetInvocationList().Count()}"); fooHandle3(2); foreach (var result in fooHandle3.GetInvocationList()) { Console.WriteLine($"result.Target:{result.Target},result.Method:{result.Method},result.InvocationListCount:{result.GetInvocationList().Count()}"); } Console.Read(); } private void Minus(int a) { Console.WriteLine(a-1); } static void multiply(int a) { Console.WriteLine(a * 2); } } public class Foo { public void Add(int value) { Console.WriteLine(value + 2); } } }

The output is:

The new code added above declares a new delegate variable, fooHandle3, initialized to null, then adds delegates or functions to fooHandle in three different ways, and then outputs the equivalent of calling out three methods in sequence, while we iterate through the fooHandle3.GetInvocationList() delegate array and the output does have three methods, but noteNo, I did not declare a delegate variable in the fooHandle3 += new Program().Minus paragraph. We can notice the sentence Delegate.Combine (fooHandle3, fooHandle1). Combine obviously needs two delegate variables. Looking at the compiled code, we can see what exactly happened.
The Il key code is as follows:

//fooHandle3 += fooHandle
IL_00f7: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_00fc: castclass DelegateSample.FooHandle
IL_0101: stloc.3
IL_0102: ldloc.3
IL_0103: ldloc.1
//fooHandle3 =(FooHandle)Delegate.Combine(fooHandle3,fooHandle1)
IL_0104: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_0109: castclass DelegateSample.FooHandle
IL_010e: stloc.3
IL_010f: ldloc.3
//new Program()
IL_0110: newobj instance void DelegateSample.Program::.ctor()
IL_0115: ldftn instance void DelegateSample.Program::Minus(int32)
//new FooHandle()A new one has been added FooHandle Delegate variable
IL_011b: newobj instance void DelegateSample.FooHandle::.ctor(object,
native int)
//fooHandle3 += new Program().Minus 
IL_0120: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)

In other words, all three different methods are translated as Combine methods. If this is a direct <b>+=function </b>, there will also be a new delegate variable in the background, assigning the method to the variable and adding fooHandle3, then we know that the most critical core code should be Delegate.combine, which is a static method. Let's see what the source code is like:
Delegate class:

[__DynamicallyInvokable]
public static Delegate Combine(Delegate a, Delegate b)
{
   if ((object)a == null)
   {
     return b;
   }
   return a.CombineImpl(b);
}

protected virtual Delegate CombineImpl(Delegate d)
{
   throw new MulticastNotSupportedException(Environment.GetResourceString("Multicast_Combine"));
}

 

MulticastDelegate class:

[SecurityCritical]
private object _invocationList;//Delegation Chain List

[SecurityCritical]
private IntPtr _invocationCount;

[SecuritySafeCritical]
protected sealed override Delegate CombineImpl(Delegate follow)
{
   if ((object)follow == null)
   {
      return this;
   }
   if (!Delegate.InternalEqualTypes(this, follow))
   {
     throw new ArgumentException(Environment.GetResourceString("Arg_DlgtTypeMis"));
   }
   MulticastDelegate multicastDelegate = (MulticastDelegate)follow;
   int num = 1;
   object[] array = multicastDelegate._invocationList as object[];
   if (array != null)
   {
       num = (int)multicastDelegate._invocationCount;
   }
   object[] array2 = _invocationList as object[];
   int num2;
   object[] array3;
   if (array2 == null)
   {
     num2 = 1 + num;
     array3 = new object[num2];
     array3[0] = this;
     if (array == null)
     {
        array3[1] = multicastDelegate;
     }
     else
     {
       for (int i = 0; i < num; i++)
       {
         array3[1 + i] = array[i];
       }
     }
     return NewMulticastDelegate(array3, num2);
    }
    int num3 = (int)_invocationCount;
    num2 = num3 + num;
    array3 = null;
    if (num2 <= array2.Length)
    {
       array3 = array2;
       if (array == null)
       {
           if (!TrySetSlot(array3, num3, multicastDelegate))
           {
             array3 = null;
           }
       }
       else
       {
         for (int j = 0; j < num; j++)
         {
            if (!TrySetSlot(array3, num3 + j, array[j]))
            {
               array3 = null;
               break;
            }
         }
       }
    }
    if (array3 == null)
    {
       int num4;
       for (num4 = array2.Length; num4 < num2; num4 *= 2)
       {
       }
       array3 = new object[num4];
       for (int k = 0; k < num3; k++)
       {
          array3[k] = array2[k];
       }
       if (array == null)
       {
          array3[num3] = multicastDelegate;
       }
       else
       {
          for (int l = 0; l < num; l++)
          {
             array3[num3 + l] = array[l];
          }
       }
     }
     return NewMulticastDelegate(array3, num2, thisIsMultiCastAlready: true);
   }

 

Implementation of the GetInvocationList method:

//Delgate Class
public virtual Delegate[] GetInvocationList()
{
   return new Delegate[1]
   {
     this
   };
}

//MulticastDelegate Class
public sealed override Delegate[] GetInvocationList()
{
   object[] array = _invocationList as object[];
   Delegate[] array2;
   if (array == null)
   {
     array2 = new Delegate[1]
     {
       this
     };
   }
   else
   {
     int num = (int)_invocationCount;
     array2 = new Delegate[num];
     for (int i = 0; i < num; i++)
     {
        array2[i] = (Delegate)array[i];
     }
   }
   return array2;
}

 

In fact, as we see here, we can see that one of the most important is the _invocationList variable, that is, when calling Combine, we will determine if the left delegate variable is empty, if it is empty, we will return the right delegate variable, if not, we will call the CombineImpl method. In the example above, fooHandle3_invocationList stores all the delegate variables attached toA delegate variable, which contains the object itself, that is, what traverses the fooHandle3.GetInvocationList, outputs three delegate variables attached to the fooHandle3 variable. In this example, fooHandle3 is initialized to null. It also means that the Targt and Menthod attributes of fooHandle3 are the Target and Menthod attributes of the last delegate variable added, and when the delegate is returned by a return value, it also returns the lastThe return value of these functions, then fooHandle3 is roughly structured as follows:

 

We've only used += so far, but in fact - = is to call its Delegate.Remove method, which is the opposite of the Combine method and is not detailed enough.
Seeing this, we can finally answer a few questions thrown at the beginning?

  • Is a delegate equivalent to a C/C++ function pointer?

Clearly, no, from the data structure, the c++ function pointer represents a memory address pointing to a function, which is actually no different from writing the function name directly, because the function name when we call the function is also the function entry address, while the delegate is a class, a managed memory, which is released by clr after using Invoke, and its function members canStores all the information about the function being called, which the function pointer does not do, but in some special cases, the function pointer in C++ is just like a delegate. Interested friends can see p/invoke

  • What is a delegation?

Delegates are classes in nature, and the essence of multicast delegates is to maintain a private_invocationList delegate chain object, +=and-=are both calls to its static methods Combine and Remove

  • What is a delegation for?

Delegates, like c++ function pointers, can act as function transponders, decouple callers from callees and callees, and act as function parameters when calling back functions

  • What is the difference between a delegate and an anonymous function?

Let's first declare and use anonymous functions:

public delegate int Foohandle(int a, int b);

Foohandle foohandle = delegate (int a, int b) { return a + b; };//Anonymous Method Method
Foohandle foohandle1= (a, b)=> a + b;//Lambda Expression mode

foohandle.Invoke(2,2);//Output 4
foohandle1.Invoke(2,2);//Output 4

 

Let's see how msdn defines anonymous functions:

 

Obviously, an anonymous function is just an expression that can be used to initialize a delegate, while a delegate is a class. By looking at IL, a new delegate object is instantiated in the background and assigned to it as a function.

  • The relationship between delegation and events?

In the same way, we declare and use events:

public class Foo
{
   public delegate void Foohandel(int a, int b);

   public event Foohandel foohandle;

   public Foo()
   {
      foohandle = new Foohandel(add);
      foohandle(2,2);//stay Foo Events can be called directly inside
      Console.WriteLine($"{foohandle.Target},{foohandle.Method}");
   }

   public void excute(int a,int b)//Functions exposed to external classes that call events
   {
      foohandle?.Invoke(a,b);
   }

   private void add(int a, int b)
   {
      Console.WriteLine(a + b); 
   }
}

class Program
{
   static void Main(string[] args)
   {
      Foo foo = new Foo();
      //foo.foohandle = new Foo.Foohandel(multiply);Compile but prompt foo.foohandle Can only appear again+=and-=left
      foo.foohandle +=new Foo.Foohandel(multiply);
      foo.excute(2, 2); 
      Console.Read();
   }

   static void multiply(int a,int b)
   {
      Console.WriteLine(a * b); 
   }
}

 

Output results:

4
EventSample.Foo,Void add(Int32, Int32)
4
4

 

In the Foo class, we find that the event foohandle is equivalent to a delegate, but outside, when we access it again by the main function of program, we find that foohandle can only do += or - = or access its function members Target and Menthod, and we can only call them by calling the excute function, when we know that Event is actually based on a delegateInternal classes are equivalent to delegates, while external classes can only have delegated multicast functions, and the rest can't be accessed. In fact, we want to know whether this property is the same.Interesting friends can learn about the principles of events, and it's also interesting


Finally, let's talk about one of the functions delegated:

Parameter inversion of delegate and covariance of return value

Since delegates also support generic delegations, we can see what Microsoft has defined.

public delegate void Action<in T>(T obj);//among in Represents inversion
public delegate TResult Func<out TResult>();//among out Represent covariance

class Program
{
    static Action<object> action;
    static Func<string> func;
    static void Main(string[] args)
    {
       action = (object a) => { Console.WriteLine(a.ToString()); };
       Action<string> action1 = action;//Parameter Inversion
       action("Hello!");


       func = () => { return "I am Func"; };
       Func<object> func1 = func;//Return value covariance
       Console.WriteLine(func1()); 
       Console.ReadLine();
    }

}

 

Output results:

Hello!
I am Func

If you want to know more about friends, you can learn about covariance and contravariance in generics without going into it here

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Topics: C# Attribute Lambda REST