Magic without delegates
Action type delegates are one of the little wonders brought to .NET from functional programming. They can be defined as a method that has only one parameter (in its simplest overload) and returns no value. They’re usually used to store references to methods or to pass a method as a parameter without having to explicitly declare a delegate. Just define the parameter with the same signature you expect to receive and the magic starts to work.
An important detail we can see when observing the Action
Learn more about covariance and contravariance in Generics here.
Let’s see some of this magic. Assuming we have a method that accepts a parameter of type Action
void test()
{
string msg = "This is the value...";
doSomethingWithStringValue(enqueueMessage, msg);
doSomethingWithStringValue(saveToDatabase, msg);
doSomethingWithStringValue(writeMessageToConsole, msg);
}
private void doSomethingWithStringValue(Action<string> actionToDo, string value)
{
//do several things with this value
validateMessage(value);
compressMessage(value);
//when finishing...
actionToDo(value);
}
private void enqueueMessage(string value)
{
//do something & enqueue this value
Queue<string> messages = new Queue<string>();
messages.Enqueue(value);
}
private void saveToDatabase(string value)
{
//do something & save to db this value
addLineToUserLog(value);
}
private void writeMessageToConsole(string value)
{
//do something & output this value
Console.WriteLine(value);
}
On one hand, we have three methods that do different things but have the same signature (they all expect a string type parameter). And on the other hand, we have a method that has a parameter of type Action
Pretty cool, right? It’s the same as using delegates but, uhm… wait! Yes, without using them :smile:
Actions everywhere
Well, more and more framework classes are making use of this type of delegate and its sibling Func, which is basically the same but returns a value. Without going too far, LINQ extension methods (Select, Where, OrderBy) use Func and almost all of TPL is based on using Action, from the For and ForEach loops of the static Parallel class, to the explicit creation of tasks through the Task class.
For example, when we want to execute a task asynchronously, we can use the StartNew method of the Task.Factory class. This method has an overload that accepts a parameter of type Action or Func, and best of all, it can be created inline, that is, at the same moment the call is made. Let’s see some examples:
Starting with a simple method:
private void doSomething()
{
//Pause for 0 to 10 seconds (random)
Random r = new Random(Guid.NewGuid().GetHashCode());
Thread.Sleep(r.Next(10000));
}
Since it’s a method that neither receives parameters nor returns anything, we can use its simplest overload:
Task.Factory.StartNew(doSomething);
Another option, if the method had an int parameter to specify the number of seconds (instead of being random) could be this:
private void doSomething(int seconds)
{
int mseconds = seconds * 1000
Thread.Sleep(mseconds);
}
Task.Factory.StartNew(() => doSomething(5));
Here we see something more interesting. Something we’ve probably observed many times and used before: A lambda expression. This expression is also something taken from functional programming, and can be read as: “goes to”. On the left side of the expression, input parameters or variables are specified (if they exist, in this case they don’t), and on the right side the expression itself. The previous case is so simple that it has no parameters and we only use the right side of the expression to send the value 5 to the method.
When using a lambda expression, the instructions contained in that expression can span multiple lines, so we can also do something like this:
Task.Factory.StartNew(() =>
{
int x = 5;
doSomething(x);
Console.WriteLine("finished!");
});
Or directly this:
Task.Factory.StartNew(() =>
{
int x = 5;
int mseconds = seconds * 1000
Thread.Sleep(mseconds);
Console.WriteLine("finished!");
});
In this case, we can even omit the doSomething method and use the code inline directly in the StartNew call. However, a piece of advice: It’s not advisable to abuse inline expressions, so if we have more than 5 or 6 lines, it might be more convenient to refactor this code to not make it too complex and respect good design principles.
Now with parameters
So far when making the call we’ve always used an Action type delegate without parameters, hence the empty parentheses on the left side of the lambda expression. However, we’ll find many cases where we need to pass parameters. Without going too far, the Parallel.For method has an Action type parameter that needs to be passed an int value, which is logical since within a loop it’s very necessary to know the iteration value at all times:
Parallel.For(1, 40, (i) =>
{
serie.Add(i.Fibonacci());
});
Notice that it’s not necessary to define the data type of variable i because the compiler itself is capable of inferring it, but obviously we can also declare the type before the variable name, as always (int i).
We can pass as many parameters as the Action needs. The same method has another overload that accepts a ParallelLoopState object to be able to cancel the loop:
Parallel.For(1, 40, (i, loopState) =>
{
serie.Add(i.Fibonacci());
if (i > 35) loopState.Break();
});
And of course we can create our own actions with as many parameters as necessary. Although like before, if we need to pass more than 3 or 4 parameters to an Action, maybe we should ask ourselves if we’re doing things right:
private void saveToDatabase(string value, bool useDetails)
{
addLineToUserLog(value);
if (useDetails) addLineToUserLogDetails();
}
void test()
{
//Define an action that points to the saveToDatabase method
Action<string, bool> myAction = (v, s) =>
{
saveToDatabase(v, s);
};
string value = "This is the value...";
bool usedetails = true;
myAction(value, usedetails); //Here the action is called and the method it points to
}
Wrapping up
Action type delegates are very useful for simplifying work with delegates (now that I think about it, it’s been quite a while since I’ve used them, not even for declaring events). They allow us to specify the actions to perform and can have up to 16 parameters - too many in my opinion - and like void methods, they don’t return any value. If we want the same thing but being able to return a result, we should use its sibling Func<T, TResult> which is exactly the same, but in all its overloads (and it has as many as Action) the last argument represents the return value.

Resistance is futile.