Tải bản đầy đủ (.pdf) (85 trang)

Programming C# 4.0 phần 3 ppt

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (10.34 MB, 85 trang )

Summary
So far, we’ve seen how to create classes; to model relationships between instances of
those classes through association, composition, and aggregation; and to create rela-
tionships between classes by derivation. We also saw how virtual functions enable
derived classes to replace selected aspects of a base class.
We saw how to use protected and protected internal to control the visibility of mem-
bers to derived classes. Then, we saw how we can use either abstract classes and
methods or interfaces to define public contracts for a class.
Finally, we looked at a means of examining the inheritance hierarchy by hand, and
verifying whether an object we are referencing through a base class is, in fact, an instance
of a more derived class.
In the next chapter, we are going to look at some other techniques for code reuse and
extensibility that don’t rely on inheritance.
142 | Chapter 4: Extensibility and Polymorphism
CHAPTER 5
Composability and Extensibility with
Delegates
In the preceding two chapters, we saw how to encapsulate behavior and information
with classes. Using the concepts of association, composition, aggregation, and deriva-
tion, we modeled relationships between those classes and looked at some of the benefits
of polymorphism along with the use and abuse of virtual functions and their implied
contracts with derived classes.
In this chapter, we’ll look at a functional (rather than class-based) approach to com-
position and extensibility, and see how we can use this to implement some of the pat-
terns that have previously required us to burn our one and only base class and override
virtual functions; and all with the added benefit of a looser coupling between classes.
Let’s start with another example. This time, we want to build a system that processes
incoming (electronic) documents prior to publication. We might want to do an auto-
mated spellcheck, repaginate, perform a machine translation for a foreign-language
website, or perform one of any other number of operations that our editors will devise
during the development process and beyond.


After some business analysis, our platform team has given us a class called Document,
which is shown in Example 5-1. This is their baby, and we’re not allowed to mess with
it.
Example 5-1. The Document class
public sealed class Document
{
// Get/set document text
public string Text
{
get;
set;
}
// Date of the document
143
public DateTime DocumentDate
{
get;
set;
}
public string Author
{
get;
set;
}
}
It
has
simple properties for its Text, the DocumentDate, and the Author, and no other
methods.
What Is Coupling?

Two classes
are said to be coupled if a change to one requires a change to another. We
saw examples of that in the previous chapter. When we created our NamedPerson class,
it required changes to the FirefighterBase and the Administrator classes. We therefore
say that FirefighterBase and Administrator are coupled to NamedPerson.
Of course, any class or function that refers to another class or function is coupled to
that class—that’s unavoidable (indeed, desirable). But to make testing simpler and
systems more reliable, we try to ensure that we minimize the number of other types to
which any class or function is coupled, and that we minimize the number of couplings
between any two types. That way, any given change to a class will have a minimal
number of knock-on effects elsewhere in the system.
We also try to ensure that we organize classes into conceptual groupings called layers
so that more tightly coupled classes live together in one layer, and that there are a
minimal number of well-controlled couplings between layers. As part of that layered
approach, it is usual to try to ensure that most couplings go one-way; classes of a
“lower” layer should not depend on classes in a layer above.
That way, we can further limit (and understand) the way changes propagate through
the system. The layers act like firewalls, blocking the further impact of a change.
As usual with software design, these disciplines are not hard-and-fast rules, and they
are not imposed by the platform or language; but they are common practices that the
platform and language are designed to support.
Now we want to be able to process the document. At the very least, we want to be able
to Spellcheck, Repaginate, or Translate it
(into French, say). Because we can’t change
the Document class, we’ll implement these methods in a static utility class of common
processes, as we learned in Chapter 3. Example 5-2 shows this class, although the
implementations are obviously just placeholders—we’re illustrating how to structure
the code here, and trying to write a real spellchecker would be a rather large distraction.
144 | Chapter 5: Composability and Extensibility with Delegates
Example 5-2. Some document processing methods

static class DocumentProcesses
{
public static void Spellcheck( Document doc )
{
Console.WriteLine("Spellchecked document.");
}
public static void Repaginate( Document doc)
{
Console.WriteLine("Repaginated document.");
}
public static void TranslateIntoFrench( Document doc )
{
Console.WriteLine("Document traduit.");
}
//
}
Now
we can build a simple example of a document processor that translates, spell-
checks, and then repaginates the document (see Example 5-3).
Example 5-3. Processing a document
static class DocumentProcessor
{
public static void Process(Document doc)
{
DocumentProcesses.TranslateIntoFrench(doc);
DocumentProcesses.Spellcheck(doc);
DocumentProcesses.Repaginate(doc);
}
}
And we can call on it from our main function, to process a couple of documents, as

shown in Example 5-4.
Example 5-4. A program to test the document processing classes
class Program
{
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",
Composability and Extensibility with Delegates | 145
DocumentDate = new DateTime(2001, 01, 01),
Text = "This is the new millennium, I promise you."
};
Console.WriteLine("Processing document 1");
DocumentProcessor.Process(doc1);
Console.WriteLine();
Console.WriteLine("Processing document 2");
DocumentProcessor.Process(doc2);
Console.ReadKey();
}
}
Compile and run that, and you’ll see the following output:
Processing document 1
Document traduit.

Spellchecked document.
Repaginated document.
Processing document 2
Document traduit.
Spellchecked document.
Repaginated document.
We
encapsulated
a particular set of processing instructions, executed in a particular
order, in this (static) DocumentProcessor class so that we can easily reuse it with dif-
ferent client applications that want a standard, reliable means of performing our “trans-
late into French” process. So far, this should all be pretty familiar.
But what about a different set of processing operations, one that leaves the document
in its native language and just spellchecks and repaginates?
We could just create a second DocumentProcessor-like class, and encapsulate the rele-
vant method calls in a process function:
static class DocumentProcessorStandard
{
public static void Process(Document doc)
{
DocumentProcesses.Spellcheck(doc);
DocumentProcesses.Repaginate(doc);
}
}
And then we could add some calls to that processor in our Main method:
Console.WriteLine();
Console.WriteLine("Processing document 1 (standard)");
DocumentProcessorStandard.Process(doc1);
Console.WriteLine();
Console.WriteLine("Processing document 2 (standard)");

DocumentProcessorStandard.Process(doc2);
146 | Chapter 5: Composability and Extensibility with Delegates
Nothing is intrinsically wrong with any of this; it clearly works, and we have a nice
enough design that neatly encapsulates our processing.
We note that each DocumentProcessor is coupled to the Document class, and also to each
method that it calls on the DocumentProcesses class. Our client is coupled to the
Document and each DocumentProcessor class that it uses.
If we go back to the specification we showed earlier, we see that we are likely to be
creating a lot of different functions to modify the document as part of the production
process; they’ll slip in and out of use depending on the type of document, other systems
we might have to work with, and the business process of the day.
Rather than hardcoding this process in an ever-increasing number of processor classes
(and coupling those to an ever-increasing number of DocumentProcesses), it would ob-
viously be better if we could devolve this to the developers on our production team.
They could provide an ordered set of processes (of some kind) to the one and only
DocumentProcessor class that actually runs those processes.
We can then focus on making the process-execution engine as efficient and reliable as
possible, and the production team will be able to create sequences of processes (built
by either us, them, contractors, or whoever), without having to come back to us for
updates all the time.
Figure 5-1 represents that requirement as a diagram.
Figure 5-1. Document processor architecture
The document
is submitted to the document processor, which runs it through an or-
dered sequence of processes. The same document comes out at the other end.
Composability and Extensibility with Delegates | 147
OK, let’s build a DocumentProcessor class that implements that (see Example 5-5).
Example 5-5. An adaptable document processor
class DocumentProcessor
{

private readonly List<DocumentProcess> processes =
new List<DocumentProcess>();
public List<DocumentProcess> Processes
{
get
{
return processes;
}
}
public void Process(Document doc)
{
foreach(DocumentProcess process in Processes)
{
process.Process(doc);
}
}
}
Our
document processor has a List of DocumentProcess objects (a hypothetical type
we’ve not written yet). A List<T> is an ordered collection—that is to say that the item
you Add at index 0 stays at index 0, and is first out of the block when you iterate the
list, and so on. That means our Process method can just iterate over the collection of
DocumentProcess objects, and call some equally hypothetical Process method on each
to do the processing.
But what type of thing is a DocumentProcess? Well, we already saw a solution we can
use—we could create a DocumentProcess abstract base, with a Process abstract method:
abstract class DocumentProcess
{
public abstract void Process(Document doc);
}

We then need to create a derived class for every processing operation, as shown in
Example 5-6.
Example 5-6. Implementations of the abstract DocumentProcess
class SpellcheckProcess : DocumentProcess
{
public override void Process(Document doc)
{
DocumentProcesses.Spellcheck(doc);
}
}
148 | Chapter 5: Composability and Extensibility with Delegates
class RepaginateProcess : DocumentProcess
{
public override void Process(Document doc)
{
DocumentProcesses.Repaginate(doc);
}
}
class TranslateIntoFrenchProcess : DocumentProcess
{
public override void Process(Document doc)
{
DocumentProcesses.TranslateIntoFrench(doc);
}
}
Now
we
can configure a processor in our client by adding some process objects to the
list (see Example 5-7).
Example 5-7. Configuring a document processor with processes

static DocumentProcessor Configure()
{
DocumentProcessor rc = new DocumentProcessor();
rc.Processes.Add(new TranslateIntoFrenchProcess());
rc.Processes.Add(new SpellcheckProcess());
rc.Processes.Add(new RepaginateProcess());
return rc;
}
See how we are adding the processes to the processor in the same order we had in our
function calls previously? Our process objects are logically similar to function calls, and
the order in which they appear is logically similar to a program, except that they are
composed at runtime rather than compile time.
We can then use this configuration method in our client, and call on the processor to
process our documents, as shown in Example 5-8.
Example 5-8. Using the dynamically configured processor
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."
};
Composability and Extensibility with Delegates | 149

DocumentProcessor processor = Configure();
Console.WriteLine("Processing document 1");
processor.Process(doc1);
Console.WriteLine();
Console.WriteLine("Processing document 2");
processor.Process(doc2);
Console.ReadKey();
}
If you compile and run, you’ll see the same output as before:
Processing document 1
Document traduit.
Spellchecked document.
Repaginated document.
Processing document 2
Document traduit.
Spellchecked document.
Repaginated document.
This
is
a very common pattern in object-oriented design—encapsulating a method in
an object and/or a process in a sequence of objects.
What’s nice about it is that our DocumentProcessor is now coupled only to the
Document class, plus the abstract base it uses as a contract for the individual processes.
It is no longer coupled to each and every one of those processes; they can vary without
requiring any changes to the processor itself, because they implement the contract
demanded by the abstract base class.
Finally, the processing sequence (the “program” for the DocumentProcessor) is now the
responsibility of the client app, not the processor library; so our different production
teams can develop their own particular sequences (and, indeed, new processes) without
having to refer back to the core team and change the document processor in any way.

In fact, the only thing that is a bit of a pain about this whole approach is that we have
to declare a new class every time we want to wrap up a simple method call. Wouldn’t
it be easier just to be able to refer to the method call directly?
C# provides us with a tool to do just that: the delegate.
Functional Composition with delegate
We just wrote some code that wraps up a method call inside an object. The call itself
is wrapped up in another method with a well-known signature.
You can think of a delegate as solving that same sort of problem: it is an object that lets
us wrap up a method call on another object (or class).
150 | Chapter 5: Composability and Extensibility with Delegates
But while our DocumentProcess classes have their methods hardcoded into virtual func-
tion overrides, a delegate allows us to reference a specific function (from a given class
or object instance) at runtime, then use the delegate to execute that function.
So, in the same way that a variable can be considered to contain a reference to an object,
a delegate can be thought to contain a reference to a function (see Figure 5-2).
Figure 5-2. Delegates and variables
Before we get into the specific C# syntax, I just want to show you that there isn’t
anything mystical about a delegate; in fact, there is a class in the .NET Framework
called Delegate which encapsulates the behavior for us.
As you might expect, it uses properties to store the reference to the function. There are
two, in fact: Method (which indicates which member function to use) and Target (which
tells us the object on which the method should be executed, if any).
As you can see, the whole thing is not totally dissimilar in concept from our previous
DocumentProcess base class, but we don’t need to derive from Delegate to supply the
function to call. That ability has moved into a property instead.
That’s all there is to a delegate, really.
Functional Composition with delegate | 151
However, it is such a powerful and useful tool that the C# language designers have
provided us with special language syntax to declare new Delegate types, assign the
appropriate function, and then call it in a much more compact and expressive fashion.

It also allows the compiler to check that all the parameter and return types match up
along the way, rather than producing errors at runtime if you get it wrong.
It is so compact, expressive, and powerful that you can probably get through your entire
C# programming career without ever worrying about the classes the C# compiler emits
which derive from that Delegate class and implement it all.
So, why have we just spent a page or so discussing these implementation
details, if we’re never going to see them again?
While you don’t usually need to use the Delegate class directly, it is easy
to get confused by language-specific voodoo and lose track of what a
delegate really is: it is just an object, which in turn calls whichever func-
tion we like, all specified through a couple of properties.
Let’s start by defining a new delegate type to reference our document processing
functions.
As I mentioned earlier, rather than using that Delegate class, C# lets us define a delegate
type using syntax which looks pretty much like a function declaration, prefixed with
the keyword delegate:
delegate void DocumentProcess(Document doc);
That defines a delegate type for a method which returns void, and takes a single
Document parameter. The delegate’s type name is DocumentProcess.
Delegates Under the Hood
Anyone who
has sensibly decided not to go any further into the implementation details
can skip this sidebar. For those still reading
When you declare a delegate like this, under the covers C# emits a class called
DocumentProcess, derived from MulticastDelegate (which is a subclass of Delegate).
Among other things, that emitted class has a function called Invoke(int param) which
matches the signature we declared on the delegate.
So how is Invoke implemented? Surprisingly, it doesn’t have any method body at all!
Instead, all of the members of the emitted class are marked as special by the compiler,
and the runtime actually provides the implementations so that it can (more or less)

optimally dispatch the delegated function.
Having added the delegate, we have two types called DocumentProcess, which is not
going to work. Let’s get rid of our old DocumentProcess abstract base class, and the three
152 | Chapter 5: Composability and Extensibility with Delegates
classes we derived from it. Isn’t it satisfying, getting rid of code? There is less to test
and you are statistically likely to have fewer bugs.
So how are we going to adapt our DocumentProcessor to use our new definition for the
DocumentProcess type? Take a look at Example 5-9.
Example 5-9. Modifying DocumentProcess to use delegates
class DocumentProcessor
{
private readonly List<DocumentProcess> processes =
new List<DocumentProcess>();
public List<DocumentProcess> Processes
{
get
{
return processes;
}
}
public void Process(Document doc)
{
foreach(DocumentProcess process in Processes)
{
// Hmmm this doesn't work anymore
process.Process(doc);
}
}
}
We’re still storing a set of DocumentProcess objects, but those objects are now delegates

to member functions that conform to the signature specified by the DocumentProcess
delegate.
We can still iterate over the process collection, but we no longer have a Process method
on the object. The equivalent function on the delegate type is a method called Invoke
which matches the signature of our delegated function:
process.Invoke(doc);
While this works just fine, it is such a common thing to need to do with a delegate that
C# lets us dispense with .Invoke entirely and treat the delegate as though it really was
the function to which it delegates:
process(doc);
Here’s the final version of our Process method:
public void Process(Document doc)
{
foreach(DocumentProcess process in Processes)
{
process(doc);
}
}
Functional Composition with delegate | 153
This can take a bit of getting used to, because our variable names are
usually camelCased and our method names are usually PascalCased.
Using function call syntax against a camelCased object can cause severe
cognitive dissonance. I’ve still never really gotten used to it myself, and
I always feel like I need a sit-down and a cup of coffee when it happens.
Now we need to deal with the Configure method that sets up our processes. Rather
than creating all those process classes, we need to create the delegate instances instead.
You can construct a delegate instance just like any other object, using new, and passing
the name of the function to which you wish to delegate as a constructor parameter:
static DocumentProcessor Configure()
{

DocumentProcessor rc = new DocumentProcessor();
rc.Processes.Add(new DocumentProcess(DocumentProcesses.TranslateIntoFrench));
rc.Processes.Add(new DocumentProcess(DocumentProcesses.Spellcheck));
rc.Processes.Add(new DocumentProcess(DocumentProcesses.Repaginate));
return rc;
}
However, C# has more syntactic shorthand that can do away with a lot of that boil-
erplate code. It can work out which delegate type you mean from context, and you only
need to provide the method name itself:
static DocumentProcessor Configure()
{
DocumentProcessor rc = new DocumentProcessor();
rc.Processes.Add(DocumentProcesses.TranslateIntoFrench);
rc.Processes.Add(DocumentProcesses.Spellcheck);
rc.Processes.Add(DocumentProcesses.Repaginate);
return rc;
}
Not only have we achieved the same end in much less code, but we’ve actually reduced
coupling between our subsystems still further—our DocumentProcessor doesn’t depend
on any classes other than the Document itself; it will work with any class, static or oth-
erwise, that can provide a method that conforms to the appropriate signature, as de-
fined by our delegate.
So far, we’ve only provided delegates to static functions, but this works just as well for
an instance method on a class.
Let’s imagine we need to provide a trademark filter for our document, to ensure that
we pick out any trademarks in an appropriate typeface. Example 5-10 shows our
TrademarkFilter class.
Example 5-10. Another processing step
class TrademarkFilter
{

readonly List<string> trademarks = new List<string>();
154 | Chapter 5: Composability and Extensibility with Delegates
public List<string> Trademarks
{
get
{
return trademarks;
}
}
public void HighlightTrademarks(Document doc)
{
// Split the document up into individual words
string[] words = doc.Text.Split(' ', '.', ',');
foreach( string word in words )
{
if( Trademarks.Contains(word) )
{
Console.WriteLine("Highlighting '{0}'", word);
}
}
}
}
It
maintains
a list of Trademarks to pick out, and has a HighlightTrademarks method
that does the actual work. Notice that it is coupled only to the Document—it knows
nothing about our processor infrastructure. Neither have we burned our base; we didn’t
have to inherit from any particular class to fit in with the processor framework, leaving
it free for, say, our forthcoming “highlighter framework.”
Example 5-11 shows how we add it to our configuration code.

Example 5-11. Adding a processing step with a nonstatic method
static DocumentProcessor Configure()
{
DocumentProcessor rc = new DocumentProcessor();
rc.Processes.Add(DocumentProcesses.TranslateIntoFrench);
rc.Processes.Add(DocumentProcesses.Spellcheck);
rc.Processes.Add(DocumentProcesses.Repaginate);
TrademarkFilter trademarkFilter = new TrademarkFilter();
trademarkFilter.Trademarks.Add("O'Reilly");
trademarkFilter.Trademarks.Add("millennium");
rc.Processes.Add(trademarkFilter.HighlightTrademarks);
return rc;
}
We create our TrademarkFilter object and add a few “trademarks” to its list. To specify
a delegate to the method on that instance we use our reference to the instance and the
name of the function on that instance. Notice that the syntax is very similar to a method
call on an object, but without the parentheses.
Functional Composition with delegate | 155
If we compile and run, we get the expected output:
Processing document 1
Document traduit.
Spellchecked document.
Repaginated document.
Processing document 2
Document traduit.
Spellchecked document.
Repaginated document.
Highlighting 'millennium'
This
pattern is very common in object-oriented design: an overall process encapsulated

in a class is customized by allowing a client to specify some action or actions for it to
execute somewhere within that process. Our DocumentProcess delegate is typical for
this kind of action—the function takes a single parameter of some type (the object our
client wishes us to process), and returns void.
Because we so often need delegates with this kind of signature, the framework provides
us with a generic type that does away with the need to declare the delegate types ex-
plicitly, every time.
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>
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 de-
claring 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.
156 | Chapter 5: Composability and Extensibility with Delegates
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 pa-
rameters. (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 docu-
ment—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).
We could implement that by rewriting our DocumentProcessor as shown in Exam-
ple 5-13.
Generic Actions with Action<T> | 157
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))
Figure 5-3. Document processor with checking
158 | Chapter 5: Composability and Extensibility with Delegates
{
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.
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 dele-
gate (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,
Generic Actions with Action<T> | 159
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).
Generic Predicates with Predicate<T>
Unlike the many variants of Action<>, the framework provides us with a single
Predicate<T> type, which defines a delegate to a function that takes a single parameter
of type T and returns a Boolean.
Why only the one parameter? There are good computer-science-
philosophical reasons for it. In mathematical logic, a predicate is usually
defined as follows:
P : X → { true, false }
That can be read as “a Predicate of some entity X maps to ‘true’ or
‘false’”. The single parameter in the mathematical expression is an im-
portant limitation, allowing us to build more complex systems from the

simplest possible building blocks.
This formal notion gives rise to the single parameter in the .NET
Predicate<T> class, however pragmatically useful it may be to have more
than one parameter in your particular application.
We can delete our Check delegate (Hurrah! More code removed!), and replace it with
a Predicate<T> that takes a Document as its type parameter:
Predicate<Document>
And we can update the DocumentProcessor to make use of Predicate<T>, as shown in
Example 5-14.
Example 5-14. DocumentProcessor updated to use Predicate<T>
class DocumentProcessor
{
class ActionCheckPair
{
public Action<Document> Action { get; set; }
public Predicate<Document> 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,
160 | Chapter 5: Composability and Extensibility with Delegates
Predicate<Document> quickCheck)
{
processes.Add(
new ActionCheckPair { Action = action, QuickCheck = quickCheck });
}

//
}
We
can
now update our client code to use our new DocumentProcessor API, calling
AddProcess now that the list of processes is private (see Example 5-15).
Example 5-15. Updating Configure to use modified DocumentProcessor
static DocumentProcessor Configure()
{
DocumentProcessor rc = new DocumentProcessor();
rc.AddProcess(DocumentProcesses.TranslateIntoFrench);
rc.AddProcess(DocumentProcesses.Spellcheck);
rc.AddProcess(DocumentProcesses.Repaginate);
TrademarkFilter trademarkFilter = new TrademarkFilter();
trademarkFilter.Trademarks.Add("Ian");
trademarkFilter.Trademarks.Add("Griffiths");
trademarkFilter.Trademarks.Add("millennium");
rc.AddProcess(trademarkFilter.HighlightTrademarks);
return rc;
}
For the time being, we’re using the overload of AddProcess that doesn’t supply a
quickCheck, so if we compile and run, we get the same output as before:
Processing document 1
Document traduit.
Spellchecked document.
Repaginated document.
Processing document 2
Document traduit.
Spellchecked document.
Repaginated document.

Highlighting 'millennium'
OK, the idea here was to allow our production team to quickly configure a check to
see if the process was likely to fail, before embarking on a resource-intensive task. Let’s
say DocumentProcesses.TranslateIntoFrench is a very time-consuming function, and
they’ve discovered that any document whose text contains a question mark (?) will fail.
They’ve raised a bug with the machine translation team, but they don’t want to hold
up the entire production process until it is fixed—only 1 in 10 documents suffer from
this problem.
Generic Predicates with Predicate<T> | 161
They need to add a quick check to go with the TranslateIntoFrench process. It is only
one line of code:
return !doc.Contains("?");
They could create a static class, with a static utility function to use as their predicate,
but the boilerplate code would be about 10 times as long as the actual code itself. That’s
a barrier to readability, maintenance, and therefore the general well-being of the de-
veloper. C# comes to our rescue with a language feature called the anonymous method.
Using Anonymous Methods
An anonymous method is just like a regular function, except that it is inlined in the
code at the point of use.
Let’s update the code in our Configure function to include a delegate to an anonymous
method to perform the check:
rc.AddProcess(
DocumentProcesses.TranslateIntoFrench,
delegate(Document doc)
{
return !doc.Text.Contains("?");
});
The delegate to the anonymous method (i.e., the anonymous delegate) is passed as the
second parameter to our AddProcess method. Let’s pull it out so that we can see it a
little more clearly (there’s no need to make this change in your code; it is just for clarity):

Predicate<Document> predicate =
delegate(Document doc)
{
return !doc.Text.Contains("?");
}
Written like this, it looks recognizably like a function definition, except that we use the
delegate keyword to let the compiler know we are providing a delegate. There’s no
need to specify the return type—that is inferred from the context. (In this case, the
delegate type is Predicate<T>, so the compiler knows the return type is bool.) Any
parameters in our parameter list are accessible only inside the body of the anonymous
method itself.
Why do we call it an anonymous method? Because it doesn’t have a name that can be
referenced elsewhere! The variable that references the delegate to the anonymous
method has a name, but not the anonymous delegate type, or the anonymous method
itself.
If you compile and run the code you’ll see the new output:
Processing document 1
The processing will not succeed
162 | Chapter 5: Composability and Extensibility with Delegates
Processing document 2
Document traduit.
Spellchecked document.
Repaginated document.
The production team is happy; but is the job done?
Not
quite;
although this inline syntax for an anonymous method is a lot more compact
than a static class/function declaration, we can get more compact and expressive still,
using lambda expression syntax, which was added in C# 3.0 (anonymous methods
having been around since C# 2.0).

Creating Delegates with Lambda Expressions
In the 1930s (a fertile time for computing theory!) two mathematicians named Church
and Kleene devised a formal system for investigating the properties of functions. This
was called lambda calculus, and (as further developed by Curry and others) it is still a
staple part of computational theory for computer scientists.
Fast-forward 70 or so years, and we see just a hint of this theory peeking through in
C#’s lambda expressions—only a hint, though, so bear with it.
As we saw before, you can think of a function as an expression that maps a set of inputs
(the parameters) to an output (the return value).
Mathematicians sometimes use a notation similar to this to define a function:
(x,y,z) → x + y + z
You can read this as defining a function that operates on three parameters (x, y, and
z). The result of the function is just the sum of the three parameters, and, by definition,
it can have no side effects on the system. The parameters themselves aren’t modified by
the function; we just map from the input parameters to a result.
Lambda expressions in C# use syntax very similar to this to define functional expres-
sions. Here’s the C# equivalent of that mathematical expression we used earlier:
(x,y,z) => x + y + z;
Notice how it rather cutely uses => as
the programming language equiv-
alent of →. C++ users should not mistake this for the -> operator—it is
quite different!
This defines a lambda expression that takes three parameters and returns the sum of
those three parameters.
Creating Delegates with Lambda Expressions | 163
Some languages enforce the no side effects constraint; but in C# there is nothing to stop
you from writing a lambda expression such as this one:
(x,y,z) =>
{
SomeStaticClass.CrashAndBurnAndMessWithEverything();

x.ModifyInternalState();
return x + y + z;
}
(Incidentally, this form of lambda expression, using braces to help define its body, is
called a statement-form lambda.) In C#, a lambda is really just a concise way to write
an anonymous method. We’re just writing normal code, so we can include operations
that have side effects.
So, although C# brings along some functional techniques with lambda syntax, it is not
a “pure” functional language like ML or F#. Nor does it intend to be.
So, what use is a lambda, then?
We’ll see some very powerful techniques in Chapter 8 and Chapter 14, where lambdas
play an important role in LINQ. Some of the data access features of the .NET Frame-
work use the fact that we can convert lambdas into data structures called expression
trees, which can be composed to create complex query-like expressions over various
types of data.
For now, we’re merely going to take advantage of the fact that we can implicitly create
a delegate from a lambda, resulting in less cluttered code.
How do we write our anonymous delegate as a lambda? Here’s the original:
Predicate<Document> predicate =
delegate(Document doc )
{
return !doc.Text.Contains("?");
}
And here it is rewritten using a lambda expression:
Predicate<Document> predicate = doc => !doc.Text.Contains("?");
Compact, isn’t it!
For a lot of developers, this syntax takes some getting used to, because it is completely
unlike anything they’ve ever seen before. Where are the type declarations? Is this taking
advantage of some of these dynamic programming techniques we’ve heard so much
about?

The short answer is no (but we’ll get to dynamic typing in Chapter 18, don’t worry).
One of the nicer features of lambda expression syntax is that it takes care of working
out what types the various parameters need to be, based on the context. In this case,
the compiler knows that it needs to produce a Predicate<Document>, so it can infer that
164 | Chapter 5: Composability and Extensibility with Delegates
the parameter type for the lambda must be a Document. You even get full IntelliSense
on your lambda parameters in Visual Studio.
It is well worth getting used to reading and writing lambdas; you’ll find
them to be a very useful and expressive means of defining short func-
tions, especially when we look at various aspects of the LINQ technol-
ogies and expression composition in later chapters.
Most developers, once they get over the initial comprehension hurdles,
fall in love with lambdas—I promise!
Delegates in Properties
The delegates we’ve seen so far have taken one or more parameters, and returned either
void (an Action<>) or a bool (a Predicate<T>).
But we can define a delegate to any sort of function we like. What if we want to provide
a mechanism that allows the client to be notified when each processing step has been
completed, and provide the processor with some text to insert into a process log?
Our callback delegate might look like this:
delegate string LogTextProvider(Document doc);
We could add a property to our DocumentProcessor so that we can get and set the
callback function (see Example 5-16).
Example 5-16. A property that holds a delegate
class DocumentProcessor
{
public LogTextProvider LogTextProvider
{
get;
set;

}
//
}
And then we could make use of it in our Process method, as shown in Example 5-17.
Example 5-17. Using a delegate in a property
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.");
if (LogTextProvider != null)
Delegates in Properties | 165
{
Console.WriteLine(LogTextProvider(doc));
}
return;
}
}
// Then perform the action
foreach (ActionCheckPair process in processes)
{
process.Action(doc);
if (LogTextProvider != null)
{
Console.WriteLine(LogTextProvider(doc));
}
}

}
Notice
that
we’re checking that our property is not null, and then we use standard
delegate syntax to call the function that it references.
Let’s set a callback in our client (see Example 5-18).
Example 5-18. Setting a property with a lambda
static void Main(string[] args)
{
//
DocumentProcessor processor = Configure();
processor.LogTextProvider = (doc => "Some text for the log ");
//
}
Here we used a lambda expression to provide a delegate that takes a Document parameter
called doc, and returns a string. In this case, it is just a constant string. Later, we’ll do
some work to emit a more useful message.
Take a moment to notice again how compact the lambda syntax is, and how the com-
piler infers all those parameter types for us. Remember how much code we had to write
to do this sort of thing back in the world of abstract base classes?
Compile and run, and we see the following output:
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
166 | Chapter 5: Composability and Extensibility with Delegates

Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay
×