Sunday, September 15, 2013

Callbacks, Delegates, Action and Func in C#

C# delegates and generics

We can consider C# delegates as type safe pointers to methods.

Take a simple delegate SomeAction which takes a generic type and returns nothing.

delegate void SomeAction< T >( List< T > input);
We can use this delegate to refer to any method which has the same method signature.
In the code below an instance of SomeAction refers to PrintList

static void PrintList< T >( List< T > inList )
{
  inList.ForEach( ( i ) => { Console.Write( i + " " ); } );
  Console.WriteLine();
}

List< int > test = new List< int >(){1, 2, 3, 4, 5};
SomeAction< int > PrintIntList = PrintList;
PrintIntList( test );
//Output: 1 2 3 4 5

A map method in C# using delegate

Consider a map function, which takes as arguments a list and another function. It applies the function to all members of the list, to produce an output list. A naive implementation in Scheme could be:

(define (map f inlist)
  (if (null? inlist)
    '()
    (cons (f (car inlist)) (map f (cdr inlist)))))
So, if we want to apply a function which doubles a list:

(map (lambda (i) (* 2 i)) '(1 2 3 4 5))
;output => '(2 4 6 8 10)
(A note on implementing map in Scheme is here)

To implement a similar map method in C#, which operates on a List:

delegate T GenericDelegate< T >( T args );

static List< T > Map< T >( List< T > inList, GenericDelegate f )
{   
  List< T > outList = new List< T >();
  for ( int i = 0; i < inList.Count(); i++ ) {
    outList.Add( f( inList[i] ) );
  }
  return outList;
}

List< int > test = new List< int >(){1, 2, 3, 4, 5};
List< int > doubleTest = Map( test, ( i ) => { return 2 * i; } );
doubleTest.ForEach( ( i ) => { Console.Write( i + " " ); } );
//2 4 6 8 10 

Action and Func

These are just syntactic sugar in C# for generic delegates.
We could use this in place of the generic delegate we used in the previous section:

delegate TResult Func< in T, out TResult >(T arg);
So we could implement the map method using a Func overload as:

static List< T > MapWithFunc< T >( List< T > inList, Func< T, T > f )
{
  List< T > outList = new List< T >();
  for ( int i = 0; i < inList.Count(); i++ ) {
    outList.Add( f( inList[i] ) );
  }
  return outList;
}

List< int > test = new List< int >(){1, 2, 3, 4, 5};
List< int > doubleTest = MapWithFunc< int >( test, ( i ) => { return 2 * i; } );
doubleTest.ForEach( ( i ) => { Console.Write( i + " " ); } );
//2 4 6 8 10 
Action is a limited form of Func, it only represents a delegate which takes argument(s) and does not return anything. For example:

delegate void Action< in T > (T arg);
C# provides Func and Action for 1 to 16 arguments, which the language designers think should be enough for most cases.

C# Action, practical use

Consider a solution with a hierarchy of projects

Solution
|
|
---Common
|
|
---Web (References Common)

At times, we would like classes in Common to call methods in the Web project. However, by design, we do not want Common to reference the Web project. We can solve this problem by using callbacks, which in C# are Action or Func (or delegates).

Let us say there is a class in the Common, CommonUtil with a method UsefulMethod

namespace Common {
  public class CommonUtil {
    //...
    public void UsefulMethod(string someArg, Action< string > syslogOnError) {
      //...
      syslogOnError( "Some common error in the context of the caller" );
Now in the Web project, we call the UsefulMethod from a Controller class:

using Common;
namespace Web {
  public class TestController : BaseController {
    CommonUtil util = new CommonUtil();
    util.UsefulMethod("abc", new SyslogControllerDelegate( this ).Syslog());
    //...
Where the Syslog method would use the controller to add extra context to a syslog message:

namespace Web {
  public BaseController controller;
  //...
  public Action< string > Syslog() {
    Action< string > syslogAction = delegate( string mesg ) {
      //Writes controller context in addition to mesg to the syslog
      new SyslogBuilder( controller ).Info( mesg );
    }
  }
}
So when UsefulMethod calls its syslogOnError, in addition to the Common error we would also get whatever extra context SyslogBuilder wants to print about the calling TestController. Note that Common did not have to reference the Web project to do this, it just used a callback provided by the calling Web class.

Conclusion

We can consider Action and Function as syntactic sugar for generic delegates in C#. They are a simple and type safe way to implement callbacks (function pointer like functionality) in C#.

1 comment:

  1. Thanks for posting this. It was very informative.

    ReplyDelete

Boston, MA, United States