1. Trang chủ
  2. » Công Nghệ Thông Tin

Programming C# 4.0 phần 3 ppt

85 297 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 85
Dung lượng 10,34 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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,

Trang 1

So far, we’ve seen how to create classes; to model relationships between instances ofthose classes through association, composition, and aggregation; and to create rela-tionships between classes by derivation We also saw how virtual functions enablederived classes to replace selected aspects of a base class

We saw how to use protected and protected internal to control the visibility of bers to derived classes Then, we saw how we can use either abstract classes andmethods or interfaces to define public contracts for a class

mem-Finally, we looked at a means of examining the inheritance hierarchy by hand, andverifying 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 andextensibility that don’t rely on inheritance

Trang 2

CHAPTER 5 Composability and Extensibility with

Delegates

In the preceding two chapters, we saw how to encapsulate behavior and informationwith 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 impliedcontracts 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 terns that have previously required us to burn our one and only base class and overridevirtual 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 processesincoming (electronic) documents prior to publication We might want to do an auto-mated spellcheck, repaginate, perform a machine translation for a foreign-languagewebsite, or perform one of any other number of operations that our editors will deviseduring the development process and beyond

pat-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 withit

Example 5-1 The Document class

public sealed class Document

{

// Get/set document text

public string Text

Trang 3

public DateTime DocumentDate

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 aminimal number of well-controlled couplings between layers As part of that layeredapproach, 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 throughthe 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 theyare not imposed by the platform or language; but they are common practices that theplatform 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 changethe Document class, we’ll implement these methods in a static utility class of commonprocesses, as we learned in Chapter 3 Example 5-2 shows this class, although theimplementations are obviously just placeholders—we’re illustrating how to structurethe code here, and trying to write a real spellchecker would be a rather large distraction

Trang 4

Example 5-2 Some document processing methods

static class DocumentProcesses

spell-Example 5-3 Processing a document

static class DocumentProcessor

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

Trang 5

DocumentDate = new DateTime(2001, 01, 01),

Text = "This is the new millennium, I promise you."

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 vant method calls in a process function:

rele-static class DocumentProcessorStandard

Trang 6

Nothing is intrinsically wrong with any of this; it clearly works, and we have a niceenough design that neatly encapsulates our processing.

We note that each DocumentProcessor is coupled to the Document class, and also to eachmethod that it calls on the DocumentProcesses class Our client is coupled to theDocument and each DocumentProcessor class that it uses

If we go back to the specification we showed earlier, we see that we are likely to becreating a lot of different functions to modify the document as part of the productionprocess; 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 onlyDocumentProcessor class that actually runs those processes

We can then focus on making the process-execution engine as efficient and reliable aspossible, 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 forupdates 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 dered sequence of processes The same document comes out at the other end

or-Composability and Extensibility with Delegates | 147

Trang 7

OK, let’s build a DocumentProcessor class that implements that (see Example 5-5).

Example 5-5 An adaptable document processor

Example 5-6 Implementations of the abstract DocumentProcess

class SpellcheckProcess : DocumentProcess

Trang 8

class RepaginateProcess : DocumentProcess

Example 5-7 Configuring a document processor with processes

static DocumentProcessor Configure()

We can then use this configuration method in our client, and call on the processor toprocess 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

Trang 9

DocumentProcessor processor = Configure();

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 theDocument 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 withoutrequiring any changes to the processor itself, because they implement the contractdemanded by the abstract base class

Finally, the processing sequence (the “program” for the DocumentProcessor) is now theresponsibility of the client app, not the processor library; so our different productionteams can develop their own particular sequences (and, indeed, new processes) withouthaving 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)

Trang 10

But while our DocumentProcess classes have their methods hardcoded into virtual

func-tion overrides, a delegate allows us to reference a specific funcfunc-tion (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’tanything mystical about a delegate; in fact, there is a class in the NET Frameworkcalled Delegate which encapsulates the behavior for us

As you might expect, it uses properties to store the reference to the function There aretwo, in fact: Method (which indicates which member function to use) and Target (whichtells 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 previousDocumentProcess base class, but we don’t need to derive from Delegate to supply thefunction to call That ability has moved into a property instead

That’s all there is to a delegate, really

Functional Composition with delegate | 151

Trang 11

However, it is such a powerful and useful tool that the C# language designers haveprovided us with special language syntax to declare new Delegate types, assign theappropriate 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 upalong 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 entireC# programming career without ever worrying about the classes the C# compiler emitswhich 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 processingfunctions

As I mentioned earlier, rather than using that Delegate class, C# lets us define a delegatetype using syntax which looks pretty much like a function declaration, prefixed withthe keyword delegate:

delegate void DocumentProcess(Document doc);

That defines a delegate type for a method which returns void, and takes a singleDocument 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 detailscan skip this sidebar For those still reading

When you declare a delegate like this, under the covers C# emits a class calledDocumentProcess, derived from MulticastDelegate (which is a subclass of Delegate).Among other things, that emitted class has a function called Invoke(int param) whichmatches 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 notgoing to work Let’s get rid of our old DocumentProcess abstract base class, and the three

Trang 12

classes we derived from it Isn’t it satisfying, getting rid of code? There is less to testand you are statistically likely to have fewer bugs.

So how are we going to adapt our DocumentProcessor to use our new definition for theDocumentProcess type? Take a look at Example 5-9

Example 5-9 Modifying DocumentProcess to use delegates

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 DocumentProcessdelegate

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 Invokewhich 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 thatC# lets us dispense with .Invoke entirely and treat the delegate as though it really wasthe function to which it delegates:

process(doc);

Here’s the final version of our Process method:

public void Process(Document doc)

Trang 13

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 Ratherthan 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 passingthe 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));

boil-static DocumentProcessor Configure()

oth-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 ourTrademarkFilter class

Example 5-10 Another processing step

class TrademarkFilter

{

readonly List<string> trademarks = new List<string>();

Trang 14

public List<string> Trademarks

// Split the document up into individual words

string[] words = doc.Text.Split(' ', '.', ',');

foreach( string word in words )

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()

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 methodcall on an object, but without the parentheses

Functional Composition with delegate | 155

Trang 15

If we compile and run, we get the expected output:

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 toexecute somewhere within that process Our DocumentProcess delegate is typical forthis kind of action—the function takes a single parameter of some type (the object ourclient 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 plicitly, every time

ex-Generic Actions with Action<T>

Action<T> is a generic type for a delegate to a function that returns void, and takes asingle 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 touse an Action<Document> instead, as shown in Example 5-12

Trang 16

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

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 havenoticed 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 todefine delegates to methods which return void, but which take two, three, or moreparameters .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 integrationtest 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 complexprocesses They have some code which would let them do a quick check for some oftheir 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 returnvalues If any fail, we can give up (see Figure 5-3)

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

Exam-Generic Actions with Action<T> | 157

Trang 17

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)

// 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

Trang 18

// Then perform the action

foreach (ActionCheckPair process in processes)

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 canavoid polluting the namespace with implementation details Although you can makenested 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 thework, 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; onetakes 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 gate (if it is not null) to see if all is OK As soon as one of these checks returns false,

dele-we return from the method and do no further work Otherwise, dele-we iterate through theprocesses 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

Trang 19

given the way things have been going so far, this is a sufficiently useful idea for it toappear 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 singlePredicate<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:

public Action<Document> Action { get; set; }

public Predicate<Document> QuickCheck { get; set; }

Trang 20

Example 5-15 Updating Configure to use modified DocumentProcessor

static DocumentProcessor Configure()

up the entire production process until it is fixed—only 1 in 10 documents suffer fromthis problem

Generic Predicates with Predicate<T> | 161

Trang 21

They need to add a quick check to go with the TranslateIntoFrench process It is onlyone 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 anonymousmethod to perform the check:

Why do we call it an anonymous method? Because it doesn’t have a name that can bereferenced elsewhere! The variable that references the delegate to the anonymousmethod has a name, but not the anonymous delegate type, or the anonymous methoditself

If you compile and run the code you’ll see the new output:

Processing document 1

The processing will not succeed

Trang 22

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 compactthan 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 Churchand 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 inC#’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 sions Here’s the C# equivalent of that mathematical expression we used earlier:

expres-(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

Trang 23

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:

(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 operationsthat 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 lambdasplay 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:

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 completelyunlike anything they’ve ever seen before Where are the type declarations? Is this takingadvantage of some of these dynamic programming techniques we’ve heard so muchabout?

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 workingout 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

Trang 24

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 eithervoid (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 beencompleted, 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 thecallback function (see Example 5-16)

Example 5-16 A property that holds a delegate

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)

Trang 25

// Then perform the action

foreach (ActionCheckPair process in processes)

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 parametercalled doc, and returns a string In this case, it is just a constant string Later, we’ll dosome work to emit a more useful message

Take a moment to notice again how compact the lambda syntax is, and how the piler infers all those parameter types for us Remember how much code we had to write

com-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

Trang 26

Repaginated document.

Some text for the log

Highlighting 'millennium'

Some text for the log

That’s an example of a delegate for a function that returns something other than void

or a bool As you might have already guessed, the NET Framework provides us with

a generic type so that we don’t have to declare those delegates by hand, either

Generic Delegates for Functions

The NET Framework exposes a generic class called Func<T, TResult>, which you canread as “Func-of T and TResult.”

As with Predicate<T> and Action<T> the first type parameter determines the type of thefirst 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 delegatedeclaration:

delegate string LogTextProvider(Document doc);

and update the property:

public Func<Document,string> LogTextProvider

Processing document 1

The processing will not succeed.

Some text for the log

Trang 27

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 veryuseful right now We can improve it by logging the name of the file we have processedafter 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);

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 thedocuments we update that variable to reflect our current status

How do we get that information into the lambda expression? Simple: we just use it!

Trang 28

Compile and run that code, and you’ll see the following output:

The processing will not succeed.

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

Closures

In general, we call an instance of a function and the set of variables on which it operates

a closure.

In a pure functional language, a closure is typically implemented by taking a snapshot

of the values of the variables at the time at which the closure is created, along with areference to the function concerned, and those values are immutable

In C#, a similar technique is applied—but the language allows us to modify those

variables after the closure has been created.

As we see in this chapter, we can use this to our advantage, but we have to be careful

to understand and manage the scope of the variables in the closure to avoid peculiarside effects

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 weexecute 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();

Generic Delegates for Functions | 169

Trang 29

string documentBeingProcessed = "(No document set)";

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 ofthe document being processed, we also increment our processCount

Finally, at the end of processing, we write out a line that tells us how many processeswe’ve executed So our output looks like this:

The processing will not succeed.

Trang 30

require-are going to track the time taken to execute some of their processes, and another teamdeveloping 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 NETFramework steps up with events

Notifying Clients with Events

An event is raised (or sent) by a publisher (or sender) when something of interest occurs (such as an action taking place, or a property changing) Clients can subscribe to the

event by providing a suitable delegate, rather like the callbacks we used previously The

method wrapped by the delegate is called the event handler The neat thing is that more than one client can subscribe to the event.

Here’s an example of a couple of events that we can add to the DocumentProcessor tohelp our production team:

class DocumentProcessor

{

public event EventHandler Processing;

public event EventHandler Processed;

//

}

Notice that we use the keyword event to indicate that what follows is an event ration We then specify the delegate type for the event (EventHandler) and the name ofthe event (using PascalCasing) So, this is just like a declaration for a public field of typeEventHandler, but annotated with the event keyword

decla-What does this EventHandler delegate look like? The framework defines it like this:

delegate void EventHandler(object sender, EventArgs e);

Notice that our delegate takes two parameters The first is a reference to the publisher

of the event so that subscribers can tell who raised it The second is some data associatedwith the event The EventArgs class is defined in the framework, and is a placeholderfor events that don’t need any extra information We’ll see how to customize this later

Almost all events follow this two-argument pattern Technically, they’re

not required to—you can use any delegate type for an event But in

practice, this pattern is almost universal.

Notifying Clients with Events | 171

Trang 31

So, how do we raise an event? Well, it really is just like a delegate, so we can use thedelegate calling syntax as shown in the OnProcessing and OnProcessed methods in Ex-ample 5-21.

Example 5-21 Raising events

public void Process(Document doc)

{

OnProcessing(EventArgs.Empty);

// First time, do the quick check

foreach (ActionCheckPair process in processes)

// Then perform the action

foreach (ActionCheckPair process in processes)

Trang 32

If we are designing our class as a base, we often mark this kind of method

as a protected virtual so that derived classes can override the

event-raising function instead of subscribing to the event.

This can be more efficient than going through the event, and it allows

us (optionally) to decline to raise the event by not calling on the base

implementation.

Be careful to document whether derived classes are allowed not to call

the base, though!

Now we need to subscribe to those events So let’s create a couple of classes to simulatewhat the production department would need to do (see Example 5-22)

Example 5-22 Subscribing to and unsubscribing from events

Trang 33

To subscribe to an event we use the += operator, with a suitable delegate You can see

in ProductionDeptTool1.Subscribe that we used the standard delegate syntax, and inProductionDeptTool2.Subscribe we used the lambda expression syntax

Of course, you don’t have to subscribe to events in methods called

Subscribe —you can do it anywhere you like!

When you’re done watching an event for any reason, you can unsubscribe usingthe -= operator and another delegate to the same method You can see that in theProductionDeptTool1.Unsubscribe method

When you subscribe to an event your subscriber implicitly holds a reference to thepublisher This means that the garbage collector won’t be able to collect the publisher

if there is still a rooted reference to the subscriber It is a good idea to provide a means

of unsubscribing from events you are no longer actively observing, to avoid growingyour working set unnecessarily

Let’s add some code to our Main method to make use of the two new tools, as shown

in Example 5-23

Example 5-23 Updated Main method

static void Main(string[] args)

If we compile and run, we now see the following output:

Tool1 has seen processing.

Tool2 has seen processing.

The processing will not succeed.

(Document 1)

Too11 has seen that processing is complete.

Tool2 has seen that processing is complete.

Tool1 has seen processing.

Tool2 has seen processing.

Trang 34

Too11 has seen that processing is complete.

Tool2 has seen that processing is complete.

Tool1 has seen processing.

Tool2 has seen processing.

Too11 has seen that processing is complete.

Tool2 has seen that processing is complete.

Executed 9 processes.

You might notice that the event handlers have been executed in the order

in which we added them This is not guaranteed to be the case, and you

cannot depend on this behavior.

If you need deterministic ordering (as we did for our processes, for

ex-ample) you should not use an event.

Earlier, I alluded to the fact that we can customize the data we send through with theevent We do this by deriving our own class from EventArgs, and adding extra properties

or methods to it Let’s say we want to send the current document through in the event;

we can create a class like the one shown in Example 5-24

Example 5-24 Custom event arguments class

class ProcessEventArgs : EventArgs

// The extra property

// We don't want subscribers to be able

// to update this property, so we make

// it private

// (Of course, this doesn't prevent them

// from changing the Document itself)

Notifying Clients with Events | 175

Trang 35

public Document Document

delegate void ProcessEventHandler(object sender, ProcessEventArgs e);

Once again, this is such a common thing to need that the framework provides us with

a generic type, EventHandler<T>, to save us the boilerplate code So we can replace theProcessEventHandler with an EventHandler<ProcessEventArgs>

Let’s update our event declarations (see Example 5-25)

Example 5-25 Updated event members

public event EventHandler<ProcessEventArgs> Processing;

public event EventHandler<ProcessEventArgs> Processed;

and then our helper methods which raise the event that will need to take a ProcessE ventArgs (see Example 5-26)

Example 5-26 Updated code for raising events

private void OnProcessing(ProcessEventArgs e)

Example 5-27 Creating the event arguments object

public void Process(Document doc)

{

ProcessEventArgs e = new ProcessEventArgs(doc);

OnProcessing(e);

Trang 36

// First time, do the quick check

foreach (ActionCheckPair process in processes)

// Then perform the action

foreach (ActionCheckPair process in processes)

Notice how we happen to reuse the same event data for each event we raise That’s safe

to do because our event argument instance cannot be modified—its only property has

a private setter If it were possible for event handlers to change the event argumentobject, it would be risky to use the same one for both events

We could offer our colleagues on the production team another facility using theseevents We already saw how they need to perform a quick check before each individualprocess to determine whether they should abort processing We can take advantage ofour Processing event to give them the option of canceling the whole process before iteven gets off the ground

The framework defines a class called CancelEventArgs which adds a Boolean propertycalled Cancel to the basic EventArgs Subscribers can set the property to True, and thepublisher is expected to abort the operation

Let’s add a new EventArgs class for that (see Example 5-28)

Example 5-28 A cancelable event argument class

class ProcessCancelEventArgs : CancelEventArgs

public Document Document

Notifying Clients with Events | 177

Trang 37

Example 5-29 A cancelable event

public event EventHandler<ProcessCancelEventArgs> Processing;

private void OnProcessing(ProcessCancelEventArgs e)

argu-Example 5-30 Supporting cancellation

public void Process(Document doc)

{

ProcessEventArgs e = new ProcessEventArgs(doc);

ProcessCancelEventArgs ce = new ProcessCancelEventArgs(doc);

Now we’ll make use of this in one of our production tools, as shown in Example 5-31

Example 5-31 Taking advantage of cancelability

Trang 38

public void Unsubscribe(DocumentProcessor processor)

Notice how we don’t have to update the event data parameter—we can take advantage

of polymorphism and just refer to it through its base type, unless we want to takeadvantage of its new features In the lambda expression syntax, of course, the new typeparameter is inferred and we don’t have to change anything; we can just update thehandler in ProductionDeptTool2 to cancel if it sees the text "document"

If we compile and run, we now see the following output:

The process will not succeed.

(Document 1)

Tool1 has seen that processing is complete.

Tool2 has seen that processing is complete.

Tool1 has seen processing, and not canceled.

Tool2 has seen processing, and not canceled.

Trang 39

(Document 2)

Highlighting 'millennium'

(Document 2)

Tool1 has seen that processing is complete.

Tool2 has seen that processing is complete.

Tool1 has seen processing, and not canceled.

Tool2 has seen processing and canceled.

events, you must ensure that it doesn’t matter if some or all of those handlers never get

called and that they behave correctly if the action they expect never actually occurs.Cancelable events need very careful documentation to indicate how they relate to theactions around them, and the exact semantics of cancellation It is therefore (in general)

a bad idea to do what we have just done, and convert a noncancelable event into acancelable one, if your code has already shipped; you stand a very good chance ofbreaking any clients that just recompile successfully against the new version

Exposing Large Numbers of Events

Some classes (particularly those related to user interactions) need to expose a very largenumber of events If you use the normal event syntax shown in the preceding examples,storage is allocated for every single event you declare, even if the events have no sub-scribers This means that objects of this type can get very large, very quickly

To avoid this situation, C# provides you with the ability to manage storage for theevents yourself, using syntax similar to a property getter and setter, with your ownbacking storage:

public event EventHandler MyEvent

Trang 40

Example 5-32 updates the DocumentProcessor we’re developing in this chapter to use adictionary for the backing storage for its events.

Example 5-32 Custom event storage

class DocumentProcessor

{

private Dictionary<string, Delegate> events;

public event EventHandler<ProcessCancelEventArgs> Processing

Ngày đăng: 06/08/2014, 09:20

TỪ KHÓA LIÊN QUAN