Events, delegates and lambdas in .NET C# part 5: lambda, Func, Action
February 27, 2014 Leave a comment
Introduction
In this post we’ll continue our discussion by diving into the following topics:
- Lambdas
- Func of T,TResult
- Action of T
Lambdas
Lambdas fit very well into the topic of events and delegates. We touched upon anonymous methods at the end of the previous post:
superman.WorldSavingCompleted += delegate(object sender, WorldSavingCompletedEventArgs e) { StringBuilder superHeroMessageBuilder = new StringBuilder(); superHeroMessageBuilder.Append("Superhero has saved the world! Name: ").Append(e.Saviour) .Append(", time spent saving the world: ").Append(e.HoursItTookToSaveWorld).Append(", message from the superhero: ") .Append(e.MessageFromSaviour); Console.WriteLine(superHeroMessageBuilder.ToString()); };
Anonymous methods are a good way to get going with lambdas as they have a similar syntax. However, lambdas are more concise. The following is the lambda equivalent of the anonymous method:
superman.WorldSavingCompleted += (s, e) => { StringBuilder superHeroMessageBuilder = new StringBuilder(); superHeroMessageBuilder.Append("Superhero has saved the world! Name: ").Append(e.Saviour) .Append(", time spent saving the world: ").Append(e.HoursItTookToSaveWorld).Append(", message from the superhero: ") .Append(e.MessageFromSaviour); Console.WriteLine(superHeroMessageBuilder.ToString()); };
In case the method body is a single statement then you can leave out the curly braces:
superman.WorldSavingCompleted += (s, e) => Console.WriteLine("World saved by " + e.Saviour);
Note how you can access the properties of the event args without having to declare the exact type in the parameter list.
The structure of a lambda expression is the following:
- Method parameters, in this case (s, e). If there are no input params then this simply becomes (). The compiler will infer the parameter types from the delegate signature. That’s how you could access the Saviour property of the WorldSavingCompletedEventArgs parameter
- The parameters are followed by the lambda operator ‘=>’. It separates the input parameters from the method body
- The method body: the parameters defined within the parenthesis will be passed into this inline method
Lambdas with custom delegates
Open the console application we’ve been working on in this series. Add a new class called ProcessIntegers:
public class ProcessIntegers { public void DoAction(int firstInteger, int secondInteger) { } }
At first this doesn’t look very clever: we don’t know how the integers will be processed and the method doesn’t return anything. However, by now we know that with delegates and events we can return multiple values from a void method. The goal here is to generalise the way two integers are handled: multiply them, add them etc. Instead of guessing what a client may want to do with the integers and writing 10 methods we’ll let the client define the operation.
In Program.cs insert the following custom delegate:
public delegate int IntegerOperationDelegate(int first, int second);
So it takes two integers and returns an integer. Let’s extend the DoAction method so that it accepts a delegate of this type:
public void DoAction(int firstInteger, int secondInteger, IntegerOperationDelegate del) { int result = del(firstInteger, secondInteger); Console.WriteLine(result); }
So we invoke the delegate in the method body and print out the result. The method allows the caller to pass in a custom delegate so that the DoAction method doesn’t have to be hard coded as far as processing the two integers are concerned. The DoAction method will have no knowledge about the exact delegate passed in which allows for the construction of loosely coupled classes. You can call DoAction from Main using lambdas. Here’s the first integer processor:
IntegerOperationDelegate addIntegersDelegate = (f, s) => f + s;
Looks weird if you see this for the first time, right? You’ll now recognise the input parameters and the lambda operator. Recall that the signature must match the delegate signature hence the (f, s) bit which stand for the first and second integers. The ‘f + s’ bit looks strange as there’s no return statement. It is added by the compiler for you in case there are no curly braces. It is inferred from the delegate signature that you’d like to add two numbers and return the result. A more obvious way of declaring this delegate is the following:
IntegerOperationDelegate addIntegersDelegate = (f, s) => { return f + s; };
Let’s add a couple more delegates:
IntegerOperationDelegate addSquaresOfIntegersDelegate = (f, s) => (f * f) + (s * s); IntegerOperationDelegate doSomethingRandomDelegate = (f, s) => (f + 3) * (s + 4);
We can then call DoAction as follows:
ProcessIntegers processIntegers = new ProcessIntegers(); processIntegers.DoAction(4, 5, addSquaresOfIntegersDelegate); processIntegers.DoAction(4, 5, doSomethingRandomDelegate);
So it’s up to the caller to inject the correct rule on how to process the integers. The DoAction method will happily invoke the delegate without having prior knowledge on what’s going to happen to them.
These were a couple of simple examples on how custom delegates work with lambdas.
In the next sections we’ll see how to work with the delegates built into the .NET framework.
Action of T
Action of T represents a delegate that accepts a single parameter and returns no value. The Action object is of type ‘delegate’. You can pass in any object of type T. With Action of T you can skip declaring your delegate as ‘delegate void NameOfDelegate(params)’. Action of T provides a shorter solution to that as it already encapsulates a delegate. You can declare an Action as follows:
Action<string> actionOfString = MatchingActionOfT; actionOfString("Test");
…where MatchingActionOfT is a method that takes a string to make sure it matches the delegate signature:
private static void MatchingActionOfT(string inputs) { Console.WriteLine(inputs); }
What if you need to pass in more parameters? That’s no problem as Action of T has 16 versions ranging from…
Action<in T>
…to
Action<in T1, in T2, ... , in T16>
If 16 separate input parameters are not enough then you need to seriously review your code.
You can even assign built-in methods to the Action if the method signature is correct:
Action<string> consoleString = Console.WriteLine; consoleString("Test from console.");
Extend the ProcessIntegers class with the following method:
public void DoAction(int firstInteger, int secondInteger, Action<int, int> action) { action(firstInteger, secondInteger); Console.WriteLine("Paramaters passed: {0}, {1}", firstInteger, secondInteger); }
In Main we can declare our action:
Action<int, int> voidAction = (f, s) => Console.WriteLine(f + s);
…and then invoke it indirectly:
ProcessIntegers processIntegers = new ProcessIntegers(); processIntegers.DoAction(2, 2, voidAction);
The overloaded DoAction method will invoke the voidAction Action for us which will result in a printout on the console window.
Function of T, TResult
This is similar to Action of T. Its type is still ‘delegate’. The key difference is that it has a return value of type TResult. Just like with Actions you can pass in 16 parameters. The return type is always the last element in the type declaration. A function that takes two integers and returns a double will look like this:
Func<int, int, double> func = MatchingFuncOfT;
…and a matching method will take the following form:
private double MatchingFuncOfT(int first, int second) { return first / second; }
The Func delegate can be invoked as usual:
double res = func(3, 2);
In case the Func doesn’t accept any parameters then the single type declaration will be the return type:
Func<out T>
Just like with Action of T this method saves you the time and code declaring the custom delegate.
Previously in this post we added a custom delegate and created instances of that delegate using lambdas:
IntegerOperationDelegate addIntegersDelegate = (f, s) => f + s; IntegerOperationDelegate addSquaresOfIntegersDelegate = (f, s) => (f * f) + (s * s); IntegerOperationDelegate doSomethingRandomDelegate = (f, s) => (f + 3) * (s + 4);
We can achieve the same goal as follows:
Func<int, int, int> addIntegersFunction = (f, s) => f + s; Func<int, int, int> addSquaresOfIntegersFunction = (f, s) => (f * f) + (s * s); Func<int, int, int> doSomethingRandomFunction = (f, s) => (f + 3) * (s + 4);
Insert the following method into ProcessIntegers:
public void DoAction(int firstInteger, int secondInteger, Func<int, int, int> action) { int res = action(firstInteger, secondInteger); Console.WriteLine("Func result: {0}", res); }
We invoke the action in the usual way of invoking methods. We get the result and print it on the Console. Just like before, the DoAction method is not aware of what’s going on within the action.
We can call the overloaded DoAction method from Main as follows:
processIntegers.DoAction(2, 2, addIntegersFunction); processIntegers.DoAction(4, 5, addSquaresOfIntegersFunction); processIntegers.DoAction(4, 5, doSomethingRandomFunction);
Lambdas and Funcs in LINQ
Lambdas and Funcs are often used in LINQ when querying objects. Add a Product object to the solution:
public class Product { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int OnStock { get; set; } }
Add a ProductService with a list of all products and a method stub:
public class ProductService { private List<Product> _allProducts = new List<Product>() { new Product(){Description = "FirstProduct", Id = 1, Name = "FP", OnStock = 456} , new Product(){Description = "SecondProduct", Id = 2, Name = "SP", OnStock = 123} , new Product(){Description = "ThirdProduct", Id = 3, Name = "TP", OnStock = 987} , new Product(){Description = "FourthProduct", Id = 4, Name = "FoP", OnStock = 432} , new Product(){Description = "FifthProduct", Id = 5, Name = "FiP", OnStock = 745} , new Product(){Description = "SixthProduct", Id = 6, Name = "SiP", OnStock = 456} }; public void PlayWithLinq() { } }
Start typing the following statement in PlayWithLinq:
IEnumerable<Product> filteredProducts = _allProducts.Where(
As you type ‘(‘ IntelliSense will show that Where accepts a Function of Product that returns a boolean. This means that the function will look at the Product objects in _allProducts and if there’s a match, i.e. the Func returns true, then the Product will be added to the List of products. Now that we know lambdas we can have something like this:
IEnumerable<Product> filteredProducts = _allProducts.Where(p => p.OnStock > 300);
‘p’ is the input parameter to the function, which will be a Product object from _allProducts. Then we have the lambda operator followed by the method body which returns a boolean. Just like above we can explicitly show the return statement if we want to:
IEnumerable<Product> filteredProducts = _allProducts.Where(p => { return p.OnStock > 300; });
You can call the input parameter as you wish: ‘p’, ‘prod’, ‘mickeymouse’ etc. The function takes a single parameter so we can omit the parenthesis, but you can also put ‘(p)’ if you want to. If the function accepts more parameters then we would need to add them within parenthesis. You can then simply iterate through the filtered products:
foreach (Product prod in filteredProducts) { Console.WriteLine(prod.Id); }
Call the method like this:
ProductService productService = new ProductService(); productService.PlayWithLinq();
…and you’ll see the filtered ids appear.
You can have multiple filtering criteria. As long as the method body returns a bool and accepts a Product it will be fine:
IEnumerable<Product> filteredProducts = _allProducts.Where(p => p.OnStock > 300 && p.Id < 4).OrderBy(p => p.Name);
We can extend our filter by an OrderBy clause which accepts a Function of T and returns a key of type TKey. The key is the property we want to order by:
IEnumerable<Product> filteredProducts = _allProducts.Where(p => p.OnStock > 300).OrderBy(p => p.Name);
Read the finishing post on this topic here.