The .NET Framework exposes a generic class called Func<T, TResult>
,
which you can read as “Func-of T and TResult.”
As with Predicate<T>
and
Action<T>
the first type
parameter determines the type of the first parameter of the function
referenced by the delegate.
Unlike Predicate<T>
or
Action<T>
we also get to specify
the type of the return value, using the last type parameter: TResult
.
Just like Action<T>
,
there is a whole family of Func<>
types which take one, two, three,
and more parameters. Before .NET 4, Func<>
went up to four parameters, but
now goes all the way up to 16.
So we could replace our custom delegate type with a Func<>
. We can delete the delegate
declaration:
delegate string LogTextProvider(Document doc);
and update the property:
public Func<Document,string> LogTextProvider
{
get;
set;
}
We can build and run without any changes to our client code because the new property declaration still expects a delegate for a function with the same signature. And we still get a bit of log text:
Processing document 1 The processing will not succeed. Some text for the log... Processing document 2 Document traduit. Some text for the log... Spellchecked document. Some text for the log... Repaginated document. Some text for the log... Highlighting 'millennium' Some text for the log...
OK, let’s go back and have a look at that log function. As we noted earlier, it isn’t very useful right now. We can improve it by logging the name of the file we have processed after each output stage, to help the production team diagnose problems.
Example 5-19 shows an
update to the Main
function to do
that.
Example 5-19. Doing more in our logging callback
static void Main(string[] args) { Document doc1 = new Document { Author = "Matthew Adams", DocumentDate = new DateTime(2000, 01, 01), Text = "Am I a year early?" }; Document doc2 = new Document { Author = "Ian Griffiths", DocumentDate = new DateTime(2001, 01, 01), Text = "This is the new millennium, I promise you." }; Document doc3 = new Document { Author = "Matthew Adams", DocumentDate = new DateTime(2002, 01, 01), Text = "Another year, another document." }; string documentBeingProcessed = null; DocumentProcessor processor = Configure(); processor.LogTextProvider = (doc => documentBeingProcessed); documentBeingProcessed = "(Document 1)"; processor.Process(doc1); Console.WriteLine(); documentBeingProcessed = "(Document 2)"; processor.Process(doc2); Console.WriteLine(); documentBeingProcessed = "(Document 3)"; processor.Process(doc3); Console.ReadKey(); }
We added a third document to the set, just so that we can see more
get processed. Then we set up a local variable called documentBeingProcessed
. As we move through the
documents we update that variable to reflect our current status.
How do we get that information into the lambda expression? Simple: we just use it!
Compile and run that code, and you’ll see the following output:
The processing will not succeed. (Document 1) Document traduit. (Document 2) Spellchecked document. (Document 2) Repaginated document. (Document 2) Highlighting 'millennium' (Document 2) Document traduit. (Document 3) Spellchecked document. (Document 3) Repaginated document. (Document 3)
We took advantage of the fact that an anonymous method has access to variables declared in its parent scope, in addition to anything in its own scope. For more information about this, see the sidebar below.
We’ve seen how to read variables in our containing scope, but what about writing back to them? That works too. Let’s create a process counter that ticks up every time we execute a process, and add it to our logging function (see Example 5-20).
Example 5-20. Modifying surrounding variables from a nested method
static void Main(string[] args) { // ... (document setup) DocumentProcessor processor = Configure(); string documentBeingProcessed = "(No document set)"; int processCount = 0; processor.LogTextProvider = (doc => { processCount += 1; return documentBeingProcessed; }); documentBeingProcessed = "(Document 1)"; processor.Process(doc1); Console.WriteLine(); documentBeingProcessed = "(Document 2)"; processor.Process(doc2); Console.WriteLine(); documentBeingProcessed = "(Document 3)"; processor.Process(doc3); Console.WriteLine(); Console.WriteLine("Executed " + processCount + " processes."); Console.ReadKey(); }
We added a processCount
variable
at method scope, which we initialized to zero. We’ve switched our lambda
expression into the statement form with the braces so that
we can write multiple statements in the function body. In addition to
returning the name of the document being processed, we also increment our
processCount
.
Finally, at the end of processing, we write out a line that tells us how many processes we’ve executed. So our output looks like this:
The processing will not succeed. (Document 1) Document traduit. (Document 2) Spellchecked document. (Document 2) Repaginated document. (Document 2) Highlighting 'millennium' (Document 2) Document traduit. (Document 3) Spellchecked document. (Document 3) Repaginated document. (Document 3) (Document 3) Executed 9 processes.
OK, our production team is very happy with all of that, but they have another requirement. Apparently, they have one team working on some diagnostic components that are going to track the time taken to execute some of their processes, and another team developing some real-time display of all the processes as they run through the system. They want to know when a process is about to be executed and when it has completed so that these teams can execute some of their own code.
Our first thought might be to implement a couple of additional callbacks: one called as processing starts, and the other as it ends; but that won’t quite meet their needs—they have two separate teams who both want, independently, to hook into it.
We need a pattern for notifying several clients that something has occurred. The .NET Framework steps up with events.