swirl
Home Software Blog Wallpapers Webtools
Demystifying callbacks in .NET
Thursday 10, May 2018   |   Post link
Callback

I always say the most important that ever happened for programmers is the 'function'! Bundling few lines of code together to do something meaningful and be able to repeat the process at will is one of the corner-stones of programming.

What makes functions (also known as methods) so useful is the ability to keep the logic of the operation intact inspite of the ability to pass different values and still get the correct result. For instance the method int Add(int a, int b) { return a + b; } will work correctly for (3, 4) or (4, 3) both resulting in the correct result 7. This is all well and good, adding two numbers is really fast and you get the result immediately. Now, what if you don't want the answer immediately? or what if the calculation takes a really long time and you don't want to sit around waiting for the result?

This is when you need callbacks. A callback is way for methods to send you the result by calling another method which you provide. This is specially useful when used asynchronously. Let's convert the simple Add method to use a callback.

Good ol' delegates

using System;
namespace Callbacks
{
	class Program
	{
		delegate void IntResultDelegate(int result);

		static void Add(int a, int b, IntResultDelegate resultHandler)
		{            
			if (resultHandler != null) resultHandler(a + b);
		}

		static void ResultHander(int r)
		{
			Console.WriteLine("Yahoo...I got the result {0}", r);
		}

		static void Main(string[] args)
		{
			IntResultDelegate d = new IntResultDelegate(ResultHander);
			Add(2, 3, d);
		}
	}
}	

So what's happening here is that you don't get the answer directly as a return value of calling the Add method, rather you provide another method named 'ResultHandler' which is called by the Add method to provide the answer. The way .NET allows this is using delegates. A delegate defines how a method should look so that the another part of code can invoke it when needed. In this case the Add method has decided that it will provide the answer by calling a method which has a single integer parameter into which it will pass the result. To set this rule in stone, a delegate named 'IntResultDelegate' has been declared. Anyone who wishes to use the Add method has to provide a method which has the same signature as the 'IntResultDelegate'.

The program can be written slightly differently to reduce creating the variable 'd' in the main method and keeping everything else same:

static void Main(string[] args)
{		
	Add(2, 3, new IntResultDelegate(ResultHander));
}

If we take a look at the IL (intermediate language) generated, it looks like this:

.method private hidebysig static void  Main(string[] args) cil managed
{
	.entrypoint
	// Code size       22 (0x16)
	.maxstack  8
	IL_0000:  nop
	IL_0001:  ldc.i4.2
	IL_0002:  ldc.i4.3
	IL_0003:  ldnull
	IL_0004:  ldftn      void Callbacks.Program::ResultHander(int32)
	IL_000a:  newobj     instance void Callbacks.Program/IntResultDelegate::.ctor(object,
																				native int)
	IL_000f:  call       void Callbacks.Program::Add(int32,
													int32,
													class Callbacks.Program/IntResultDelegate)
	IL_0014:  nop
	IL_0015:  ret
} // end of method Program::Main

We can further reduce the code by changing the code in the main method to:

static void Main(string[] args)
{            
	Add(2, 3, ResultHander);
}

If you check the IL generated it will be the same as the previous case. This is because C# allows you a shorthand syntax to simply provide the method name instead of creating an instance of the delegate.

Anonymous methods

The method of declaring delegates and creating methods to match the delegate signature is a bit dated - no longer fashionable. .NET 2.0 introduced the concept of anonymous methods. Seeing the example will give you an idea of what it is:

using System;
namespace Callbacks
{
	class Program
	{
		delegate void IntResultDelegate(int result);

		static void Add(int a, int b, IntResultDelegate resultHandler)
		{            
			if (resultHandler != null) resultHandler(a + b);
		}
		
		static void Main(string[] args)
		{            
			Add(2, 
				3, 
				delegate(int r) 
				{
					Console.WriteLine("Yahoo...I got the result {0}", r);
				}
			);
		}
	}
}		

Notice how we don't have the method 'ResultHander' at all. We defined the same logic while calling the Add method inline. Since this logic is not stored in a named method, this way of using callbacks is called using anonymous methods.

Actions

.NET 2.0 introduced generics and along with it the 'Action<T>' delegate. The family of 'Action<T>' delegates provide nothing but shortcuts to define delegates. For e.g. instead of defining the 'IntResultDelegate' delegate we can directly use Action<int> which stands for a delegate with one int parameter. So the following code is equivalent to what we did earlier:

using System;
namespace Callbacks
{
	class Program
	{
		static void Add(int a, int b, Action resultHandler)
		{            
			if (resultHandler != null) resultHandler(a + b);
		}
		
		static void Main(string[] args)
		{            
			Add(2, 
				3, 
				delegate(int r) 
				{
					Console.WriteLine("Yahoo...I got the result {0}", r);
				}
			);
		}
	}
}

Notice how we have not declared a specific delegate and how we are using the Action<int> instead in the Add method's signature.

Now why did I mention the word family when talking about the Action<T> delegate? This is because there are a bunch of them like Action<T1, T2>. This will allow you to define a delegate which takes two parameters whose types depend on what you specify for T1 and T2. E.g.: Action<int, bool> MyDelegate is the same as defining delegate void MyDelegate(int, bool).

What the Func?

Just like Action is used in place of an explicit delegate definition, Func<T> is used to define delegates. Funcs were introduced with .NET 3.5. The only difference is that Func<T> represents a delegate that returns a value of the type T which is why the documentation mentions Func<TResult> to tell you that the type specified for TResult is what will be returned. You must have guessed by now using Func<string> MyDelegate is the same as using delegate string MyDelegate(). Obviously Funcs can have parameters as well e.g. Func<bool, string, int> is a delegate which expects two parameters one bool and the second a string and returns an int. We can change the code written so far to use Func:

using System;
namespace Callbacks
{
	class Program
	{
		static void Add(int a, int b, Func resultHandler)
		{            
			if (resultHandler != null) resultHandler(a + b);
		}
		
		static void Main(string[] args)
		{            
			Add(2, 
				3, 
				delegate(int r) 
				{
					Console.WriteLine("Yahoo...I got the result {0}", r);
					return true;
				}
			);
		}
	}
}

Notice we are returning something even though there is no use of it because Funcs are used for delegates which return a value.

Lambdas

Lambda expressions were introduced with C# 3.0 which is a C# language feature and is not part of the .NET runtime as such. It's just a convenient way to create anonymous methods. If we use the lambda syntax in our example the code would look like this:

using System;
namespace Callbacks
{
	class Program
	{
		static void Add(int a, int b, Action resultHandler)
		{            
			if (resultHandler != null) resultHandler(a + b);
		}
		
		static void Main(string[] args)
		{            
			Add(2, 
				3, 
				(int r)  =>
				{
					Console.WriteLine("Yahoo...I got the result {0}", r);                    
				}
			);
		}
	}
}

If you check the IL code, the call to Add compiles to:

IL_0022:  call       void Callbacks.Program::Add(int32,
int32,
class [mscorlib]System.Action`1)

The above IL is exactly the same as using delegate(int r) instead of (int r) => when invoking the Add method.

Interfaces anyone?

What if .NET never had something called as delegates? In fact most general design pattern books talk only about interfaces most of the time. Well, .NET has excellent support for interfaces. Let's convert the code to use interfaces instead.

using System;
namespace Callbacks
{
	interface IIntResult
	{
		void OnResult(int r);
	}
	
	class Program
	{
		static void Add(int a, int b, IIntResult resultHandler)
		{            
			if (resultHandler != null) resultHandler.OnResult(a + b);
		}

		class ResultHandler : IIntResult
		{
			public void OnResult(int r)
			{
				Console.WriteLine("Yahoo...I got the result {0}", r);
			}
		};
		
		static void Main(string[] args)
		{            
			Add(2, 3, new ResultHandler());
		}
	}
}

Using interfaces to replace delegates is definitely more verbose but you can get the same functionality. Do note though we have replaced the use of delegates with interfaces delegates offer much much more. For instance you can choose to run the delegate method asynchronously. Here is the same program but using an asynchronous callback:

using System;
using System.Threading;
namespace Callbacks
{
	class Program
	{
		delegate void IntResultDelegate(int result);

		static void Add(int a, int b, IntResultDelegate resultHandler)
		{
			if (resultHandler != null)
				resultHandler.BeginInvoke(a + b, null, null);
		}

		static void ResultHander(int r)
		{
			Thread.Sleep(2000); // I am going to waste two seconds!
			Console.WriteLine("Yahoo...I got the result {0}", r);
		}

		static void Main(string[] args)
		{
			IntResultDelegate d = new IntResultDelegate(ResultHander);
			Add(2, 3, d);
			Console.WriteLine("I did try to add but when will i get the result?");
			Console.ReadLine();
		}
	}
}

In the above example the callback method ResultHandler has a sleep of 2 seconds. However when the Add method is called we immediately see the line 'I did try to add but when will i get the result?' and after two seconds we see the line 'Yahoo...I got the ..'. If we had not used BeginInvoke to call ResultHandler we would have first waited for two seconds then seen the 'Yahoo...' message and only after that seen the 'I did try..' message. All this asynchronous invocation would not work when using just interfaces without lots of additional threading code.

I am late for my event!

Microsoft documentations explains events as Events enable a class or object to notify other classes or objects when something of interest occurs. The class that sends (or raises) the event is called the publisher and the classes that receive (or handle) the event are called subscribers. . In order to understand events let's restructure the code keeping the functionality the same.

using System;
namespace Callbacks
{
	delegate void IntResultDelegate(int result);

	class Calculator
	{
		public IntResultDelegate OnResult;
		public void Add(int a, int b)
		{
			if (OnResult != null) OnResult(a + b);
		}
	}
	
	class Program
	{
		static void ResultHander(int r)
		{   
			Console.WriteLine("Yahoo...I got the result {0}", r);
		}

		static void Main(string[] args)
		{
			IntResultDelegate d = new IntResultDelegate(ResultHander);
			Calculator calc = new Calculator();
			calc.OnResult = ResultHander;
			calc.Add(5, 5);            
		}
	}
}

What we have done is moved out Add method into another class. The class has a member which is of the IntResultDelegate type. In order to have our handler method which accepts the result of the addition we set this member to the ResultHandler method. If you run the program the result is the same as all the other synchronous versions of the program we have seen so far.

Now what if you want to invoke not just the method 'ResultHandler' but others methods which want to take a peek at the result? You could have a list of IntResultDelegates, this would work or you can use events. Events perfectly fit this requirement. Let's see this in action:

using System;
namespace Callbacks
{
	delegate void IntResultDelegate(int result);

	class Calculator
	{
		public event IntResultDelegate OnResult;
		public void Add(int a, int b)
		{
			if (OnResult != null) OnResult(a + b);
		}
	}
	
	class Program
	{
		static void ResultHander(int r)
		{   
			Console.WriteLine("Yahoo...I got the result {0}", r);
		}

		static void AnotherResultHander(int r)
		{
			Console.WriteLine("I too got the result {0}", r);
		}

		static void Main(string[] args)
		{
			IntResultDelegate d = new IntResultDelegate(ResultHander);
			Calculator calc = new Calculator();
			calc.OnResult += ResultHander;
			calc.OnResult += AnotherResultHander;
			calc.Add(5, 5);            
		}
	}
} 

The output of the above programs is:

Yahoo...I got the result 10
I too got the result 10

Note how we keep adding (appending) our handler methods to the OnResult event. You can even remove handler methods using the -= operator. E.g. calc.OnResult -= AnotherResultHander. Events and delegates work together to provide a convenient framework for developing an publisher / subscriber apporach.

Hope this post gave your some insights into callbacks and how they work in .NET. Happy coding! :)



Categories: C# (3) DotNet (2)

Comments

Posts By Year

2018 (4)
2017 (11)
2016 (6)
2015 (18)
2014 (2)
2013 (4)
2012 (2)

Posts By Category

.NET (1)
.NET Core (2)
ASP.NET MVC (4)
AWS API Gateway (1)
Android (1)
Book review (3)
Business (1)
C# (3)
Containers (3)
Corporate culture (1)
Database (1)
Database migration (1)
Desktop (1)
DotNet (2)
Entity Framework (3)
Git (2)
IIS (1)
Java (2)
Life (6)
Lucene (1)
OData (1)
Office (1)
PHP (1)
PowerShell (1)
Programming (17)
Rants (5)
Software Engineering (1)
Software development (1)
Solr (1)
Sql Server (1)
T-SQL (1)
TDD (1)
TSQL (5)
Tablet (1)
Technology (1)
Test Driven (1)
Unit Testing (1)
Utilities (1)
VC++ (1)
Visual Studio (1)
Wallpapers (1)
Windows (5)
XML (1)

Posts By Tags

.NET(3) API Gateway(1) ASP.NET(4) AWS(1) Adults(1) Advertising(1) Android(1) Anti-forgery(1) Backup(1) Beliefs(1) Book review(2) Books(1) Busy(1) C#(3) C++(2) CLR(1) CORS(1) CSRF(1) CTE(1) Callbacks(1) Checkbox(1) Cmdlet(1) Commons(1) Company culture(1) Consumerism(1) Containers(3) Core(2) Data-time(1) Database(1) Debugging(1) Delegates(1) Developer(2) Dockers(2) EF 1.0(1) Entity framework(1) Events(1) File copy(1) File history(1) Git(1) GradleApache(1) HierarchyID(1) IIS(1) Installing(1) Intelli J(1) JSON(1) JavaScript(1) Localization(1) Log4J(1) Lucene(1) MVC(4) Management(2) Migration history(1) Mobile Apps(1) Modern Life(1) Money(1) OData(1) Objects(1) Office(1) Organization(1) PHP(1) Paths(1) PowerShell(1) Programming(1) Python(1) Quality(1) Runtimes(1) SD card(1) SQL(1) SQL Code-first Migration(1) SSH(1) Sattelite assemblies(1) School(1) Self reliance(1) Solr(1) Sony VAIO(1) Spirituality(1) Sql Express(1) System Image(1) TDD(1) TSQL(3) Tablet(1) Url rewrite(1) Validation(2) Wallpapers(1) Web Development(4) Windows(1) Windows 10(1) Windows 2016(2) Windows 8.1(1) Work culture(1) XML(1) Yii(1)