Generic Actions with Action<T>

Action<T> is a generic type for a delegate to a function that returns void, and takes a single parameter of some type T. We used a generic type before: the List<T> (List-of-T) where T represents the type of the objects that can be added to the list. In this case, we have an Action-of-T where T represents the type of the parameter for the function.

So, instead of declaring our own delegate:

delegate void DocumentProcess( Document doc );

we could just use an Action<> like this:

Action<Document>

Warning

A quick warning: although these are functionally equivalent, you cannot use an Action<Document> polymorphically as a DocumentProcess—they are, of course, different classes under the covers.

We’re choosing between an implementation that uses a type we’re declaring ourselves, or one supplied by the framework. Although there are sometimes good reasons for going your own way, it is usually best to take advantage of library code if it is an exact match for your requirement.

So, we can delete our own delegate definition, and update our DocumentProcessor to use an Action<Document> instead, as shown in Example 5-12.

Example 5-12. Modifying the processor to use the built-in Action<T> delegate type

class DocumentProcessor
{
    private readonly List<Action<Document>> processes =
        new List<Action<Document>>();

    public List<Action<Document>> Processes
    {
        get
        {
            return processes;
        }
    }

    public void Process(Document doc)
    {
        foreach (Action<Document> process in Processes)
        {
            process(doc);
        }
    }
}

Compile and run, and you’ll see that we still get our expected output.

If you were watching the IntelliSense as you were typing in that code, you will have noticed that there are several Action<> types in the framework: Action<T>, Action<T1,T2>, Action<T1,T2,T3>, and so on. As you might expect, these allow you to define delegates to methods which return void, but which take two, three, or more parameters. .NET 4 provides Action<> delegate types going all the way up to 16 parameters. (Previous versions stopped at four.)

OK, let’s suppose that everything we’ve built so far has been deployed to the integration test environment, and the production folks have come back with a new requirement. Sometimes they configure a processing sequence that fails against a particular document—and it invariably seems to happen three hours into one of their more complex processes. They have some code which would let them do a quick check for some of their more compute-intensive processes and establish whether they are likely to fail. They want to know if we can implement this for them somehow.

One way we might be able to do this is to provide a means of supplying an optional “check” function corresponding to each “action” function. We could then iterate all of the check functions first (they are supposed to be quick), and look at their return values. If any fail, we can give up (see Figure 5-3).

Document processor with checking

Figure 5-3. Document processor with checking

We could implement that by rewriting our DocumentProcessor as shown in Example 5-13.

Example 5-13. Adding quick checking to the document processor

class DocumentProcessor
{
    class ActionCheckPair
    {
        public Action<Document> Action { get; set; }
        public Check QuickCheck { get; set; }
    }

    private readonly List<ActionCheckPair> processes = new List<ActionCheckPair>();


    public void AddProcess(Action<Document> action)
    {
        AddProcess(action, null);
    }

    public void AddProcess(Action<Document> action, Check quickCheck)
    {
        processes.Add(
            new ActionCheckPair { Action = action, QuickCheck = quickCheck });
    }

    public void Process(Document doc)
    {
        // First time, do the quick check
        foreach( ActionCheckPair process in processes)
        {
            if (process.QuickCheck != null && !process.QuickCheck(doc))
            {
                Console.WriteLine("The process will not succeed.");
              return;
            }
        }

        // Then perform the action
        foreach (ActionCheckPair process in processes)
        {
            process.Action(doc);
        }
    }
}

There are quite a few new things to look at here.

First, we declared a new class inside our DocumentProcessor definition, rather than in the namespace scope. We call this a nested class.

We chose to nest the class because it is private to the DocumentProcessor, and we can avoid polluting the namespace with implementation details. Although you can make nested classes publicly accessible, it is unusual to do so and is considered a bad practice.

This nested class just associates a pair of delegates: the Action<Document> that does the work, and the corresponding Check that performs the quick check.

We removed the public property for our list of processes, and replaced it with a pair of AddProcess method overloads. These allow us to add processes to the sequence; one takes both the action and the check, and the other is a convenience overload that allows us to pass the action only.

Note

Notice how we had to change the public contract for our class because we initially exposed the list of processes directly. If we’d made the list an implementation detail and provided the single-parameter AddProcess method in the first place, we wouldn’t now need to change our clients as we’d only be extending the class.

Our new Process function first iterates the processes and calls on the QuickCheck delegate (if it is not null) to see if all is OK. As soon as one of these checks returns false, we return from the method and do no further work. Otherwise, we iterate through the processes again and call the Action delegate.

What type is a Check? We need a delegate to a method that returns a Boolean and takes a Document:

delegate bool Check(Document doc);

We call this type of “check” method a predicate: a function that operates on a set of parameters and returns either true or false for a given input. As you might expect, given the way things have been going so far, this is a sufficiently useful idea for it to appear in the framework (again, as of .NET 3.5).