Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain. The index of all my past little wonders posts can be found here.
Back in one of my three original “Little Wonders” Trilogy of posts, I had listed generic delegates as one of the Little Wonders of .NET. Later, someone posted a comment saying said that they would love more detail on the generic delegates and their uses, since my original entry just scratched the surface on them.
So over the next few weeks, I’ll be looking at some of the handy generic delegates built into .NET. For this week, I’ll give a quick overview of delegates and their usefulness. Then I’ll launch into a look at the Action generic delegates and how they can be used to support more generic, reusable algorithms and classes.
Delegates in a nutshell
Delegates are similar to function pointers in C++ in that they allow you to store a reference to a method. They can store references to either static or instance methods, and can actually be used to chain several methods together in one delegate.
Delegates are very type-safe and can be satisfied with any standard method, anonymous method, or a lambda expression. They can also be null as well, so care should be taken to make sure that the delegate is not null before you call it.
So let’s go back to the early days of .NET, before generic delegates, when you typically had to define delegates explicitly. For example, if we wanted to define a delegate for a logging method, we could define a delegate that takes a string as an argument and returns void:
1: // This delegate matches any method that takes string, returns nothing
2: public delegate void Log(string message);
This delegate defines a type named Log that can be used to store references to any method(s) that satisfies its signature (whether instance, static, lambda expression, etc.).
Let’s look at some example code using this delegate type:
1: // creates a delegate instance named currentLogger defaulted to Console.WriteLine (static method)
2: Log currentLogger = Console.WriteLine;
3: currentLogger("Hi Console!");
4:
5: using(var file = File.CreateText("c:\\sourcecode\\out.txt"))
6: {
7: // changes delegate to now refer to the file instance's WriteLine method
8: currentLogger = file.WriteLine;
9: currentLogger("Hello file!");
10:
11: // now append Console.WriteLine to delegate (instead of replace, adds)
12: currentLogger += Console.WriteLine;
13: currentLogger("Hi to both!");
14: }
Let’s examine what’s going on in the code above:
- At line 2, we create an instance of a delegate of type Log and have it refer to a static method that takes a string and returns void by assigning it the method name, Console.WriteLine() has an overload that fits this signature.
- At line 3, we invoke the delegate just like we would any other method, by using the delegate name and passing the arguments in the parenthesis. If our delegate returned a value, we could assign it like a normal method as well.
- At line 8, we change the delegate to refer to an instance method, in this case the WriteLine() method of the file variable.
- At line 9, we invoke the delegate again, this time it will write to file.
- At line 12, instead of replacing the delegate, we are appending a method to the delegate. This means that both Console.WriteLine (from the previous assignment) and file.WriteLine() appended with the += will be called.
So, both = and += can be used to assign method references to a delegate, albeit in different ways. The = releases any previous method(s) referenced, and assigns a new method reference (or you can assign to null to remove all and leave unassigned). And the += is used to chain a new method reference to any existing method referenced (if any). Likewise, you can use –= to remove a method from a delegate chain.
Always remember that on the call to invoke the delegate, you should make sure the delegate reference isn’t currently null. Depending on the structure of your code, you may or may not have to check at the point you call it, but you should at least be logically sure it has a value (by comparing to null) before you invoke it.
Also remember that delegates can be used to refer to anonymous methods or lambda expressions as well:
1: // assign to an anonymous method
2: currentLogger = delegate(string message) { Console.WriteLine(message.ToUpper()); };
3: currentLogger("This will be upper case.");
4:
5: // assign to a lambda expression
6: currentLogger = msg => Console.WriteLine(msg.Length);
7: currentLogger("This will output 19");
Even though you can accomplish the same thing in either anonymous method syntax or lambda expressions, the lambda expression syntax is much more concise and tends to be used the most often.
Lights, Camera, Action delegates!
So, to the real reason we’re here, the Action delegate family! Delegates give us a lot of power to hold references to methods to execute, but it can be cumbersome to define a new delegate for every situation in which you need one.
As such, .NET introduced the Action family of generic delegates. This started with Action<T> in .NET 2.0, and then expanded through each version to eventually support actions that take anywhere from 0 to 16 arguments in .NET 4.0.
Currently, the Action family can be used to refer to methods that take 0-16 arguments and return no result.
For example:
- Action – matches a method that takes no arguments and returns no value.
- Action<T> – matches a method that takes an argument of type T and returns no value.
- Action<T1, T2> – matches a method that takes an argument of type T1, a second argument of type T2, and returns no value.
- Action<T1, T2, …> – and so on up to 16 arguments and returns no value.
These are very handy because they quickly allow you to be able to specify that a method or class you design will perform an action as long as the method you specify meets the signature.
For example, in our previous code, instead of creating a new delegate type for Log, we can just use Action<string>:
1: // creates a delegate instance named currentLogger defaulted to Console.WriteLine (static method)
2: Action<string> currentLogger = Console.WriteLine;
3: currentLogger("Hi Console!");
This makes it easy to store delegates of nearly any method signature as long as it returns void, and you don’t have to define a new delegate type every time you need one for a class or method.
If there is one downside of using the generic delegates, it’s that you loose a little bit of the implied usage from the name. For example, Log gives you a pretty good indication that the method you are to specify should log the string, but Action<string> really only tells you that the method you give it must take a string. In addition, Action<> delegates with the larger number of arguments tend to become less readable in practice.
These things aside, however, really don’t detract from the usefulness of Action delegates when used judiciously. Their name already implies they are to perform an action on the types they are given, and you can name the variable name in such a way to impart intention of it’s use, such as:
1: // in this case, the delegate reference name indicates it wants a
2: // method to perform in case the work fails on the item.
3: public void PerformWork<T>(T item, Action<T> failureCallback)
4: {
5: ...
6: }
Thus, in the method above, it will attempt to PerformWork() on the item, and if that work fails for whatever reason, it will call the failureCallback action you have specified to give you a chance to handle the item for errors.
Action delegates and contra-variance in .NET 4.0
As of .NET 4.0, the Action family of delegates is contra-variant. To support this, in .NET 4.0 the signatures of the Action<> delegates with generic type parameters changed to:
- Action<in T> – matches a method that takes an argument of type T (or narrower) and returns no value.
- Action<in T1, in T2> – matches a method that takes an argument of type T1 (or narrower), a second argument of type T2 (or narrower), and returns no value.
- Action<in T1, in T2, …> – and so on up to 16 arguments and returns no value.
Notice the addition of the in keyword before each of the generic type placeholders. This is the new keyword to specify that a generic type can be contra-variant. What that means is that the method being assigned to this delegate can specify type arguments that are the same type or a less derived type.
If you get confused as to the meaning of in, think of a typical class hierarchy from a few root types up high and many different child types below, and think of in as going higher up the hierarchy to super-classes and interfaces.
Why allow this? If you think about it, if you are saying you need an action that will accept a string, you can just as easily give it an action that refers to that item as an object. In other words, if you say “give me an action that will wash dogs”, I could pass you a method that will wash any animal, because all dogs are animals.
For example, in the code below, even though our method takes an Action<string> we can satisfy it by passing an Action<object> instead. This is because object is less derived than string, thus you can handle any string as an object:
1: // method which takes a sequence of string and an action to perform on each string
2: public static void VisitCollection(IEnumerable<string> collection, Action<string> action)
3: {
4: foreach (var item in collection)
5: {
6: action(item);
7: }
8: }
9:
10: public static void Main()
11: {
12: // instead, we have an action that applies to any object
13: Action<object> printObject = o => Console.WriteLine(o.ToString());
14: List<string> items = new List<string> { "A", "B", "C" };
15:
16: // Because a string can be passed to an Action<object> due to the fact
17: // that object is less derived than string, the contra-variant action
18: // can be passed in .NET 4.0, in previous versions of .NET this didn't compile.
19: VisitCollection(items, printObject);
20: }
Previous versions of .NET implemented some forms of co-variance and contra-variance before, but .NET 4.0 goes one step further and allows you to pass or assign an Action<X> to an Action<Y> as long as X is the same (or a less derived) super-type of Y.
Sidebar: Action vs. Inheritance
The nice thing about the Action family (and really all delegates in general) is that it makes it easy to define reusable logic without having to override a class every time to specify user actions. For example, if we wanted to create a Consumer class that will read items from a queue until stopped, but want to allow the user of the class to specify how each item is consumed, we could do this by making the class abstract and making the user override an abstract method:
1: public abstract class Consumer<T>
2: {
3: private BlockingCollection<T> _collection = new BlockingCollection<T>();
4:
5: // the method to override to consume a single item
6: protected abstract void Consume(T item);
7:
8: // Assume this is launched by a task or thread somewhere else in this class...
9: // (rest omitted for brevity)
10: private void ConsumptionLoop()
11: {
12: while (!_collection.IsCompleted)
13: {
14: T item;
15: if (_collection.TryTake(out item, TimeSpan.FromSeconds(1.0)))
16: {
17: // call the override to consume the item
18: Consume(item);
19: }
20: }
21: }
22: }
The problem with this concept is that every time we want to define a new Consumer, we would need to inherit from the Consumer and override the Consume() method, this can quickly lead to a lot of clutter classes that really only are specifying an action to take and don’t really alter the behavior of the class itself.
1: // This is a lot of work just to specify that we want each line sent to Console.WriteLine()...
2: public class ConsoleConsumer : Consumer<string>
3: {
4: protected override void Consume(string item)
5: {
6: Console.WriteLine(item);
7: }
8: }
When you have a method or class that is generic in such a way that the mechanism that the action a user wants to perform is different from what the class or method itself does, this is a prime case for using a delegate. For example, the Consumer<T> class defines how to process a collection and hand off elements to be consumed, but it let’s the user define what action to take on Consume().
So, we could make this class much easier to use by just allowing the user to give it an Action<T> to perform on the item to consume:
1: public sealed class Consumer<T>
2: {
3: private BlockingCollection<T> _collection = new BlockingCollection<T>();
4: private Action<T> _consumeItem;
5:
6: // pass in the delegate on construction
7: public Consumer(Action<T> consumeItem)
8: {
9: if (consumeItem == null) throw new ArgumentNullException("consumeItem");
10:
11: _consumeItem = consumeItem;
12: }
13:
14: // Assume this is launched by a task or thread somewhere else in this class...
15: // (rest omitted for brevity)
16: private void ConsumptionLoop()
17: {
18: while (!_collection.IsCompleted)
19: {
20: T item;
21: if (_collection.TryTake(out item, TimeSpan.FromSeconds(1.0)))
22: {
23: // call the override to consume the item
24: _consumeItem(item);
25: }
26: }
27: }
28: }
Now, there’s nothing to override! This leads to safer and somewhat more efficient code because I can seal the class and keep anyone from overriding its core behavior. Plus, it’s now very flexible in that I can easily create many different types of Consumer just by constructing them with different Action<T> arguments!
1: // consumer that writes each item to console
2: var consoleConsumer = new Consumer<string>(Console.WriteLine);
3:
4: // consumer that writes length of each item to console
5: var lengthConsumer = new Consumer<string>(item => Console.WriteLine(item.Length));
6:
7: // consume that writes each item in upper case to console.
8: var upperCaseConsumer = new Consumer<string>(item => Console.WriteLine(item.ToUpper()));
These are just simplistic examples, but you get the point. This gives us the flexibility as users to define consumption any way we want as long as we provide an Action<string>. No classes to override!
Summary
Generic delegates give us a lot of power to make truly generic algorithms and classes. The Action family of delegates is a great way to be able to specify actions to be performed that can take from 0-16 arguments and return no value. Stay tuned in the weeks that follow for other generic delegates in the .NET Framework!