1. Trang chủ >
  2. Công Nghệ Thông Tin >
  3. Kỹ thuật lập trình >

4 C# 2 and 3: new features on a solid base

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 (3.69 MB, 424 trang )


55



C# 2 and 3: new features on a solid base



fixes this with anonymous methods, and introduces a simpler syntax for the cases where

you still want to use a normal method to provide the action for the delegate. You can also

create delegate instances using methods with compatible signatures—the method signature no longer has to be exactly the same as the delegate’s declaration.

Listing 2.4 demonstrates all these improvements.

Listing 2.4 Improvements in delegate instantiation brought in by C# 2

static void HandleDemoEvent(object sender, EventArgs e)

{

Console.WriteLine ("Handled by HandleDemoEvent");

}

...

EventHandler handler;



B



handler = new EventHandler(HandleDemoEvent);

handler(null, EventArgs.Empty);

handler = HandleDemoEvent;



C



Specifies delegate

type and method



Implicitly converts

to delegate instance



handler(null, EventArgs.Empty);

handler = delegate(object sender, EventArgs e)

{ Console.WriteLine ("Handled anonymously"); };



D



handler(null, EventArgs.Empty);

handler = delegate

{ Console.WriteLine ("Handled anonymously again"); };

handler(null, EventArgs.Empty);

MouseEventHandler mouseHandler = HandleDemoEvent;

mouseHandler(null, new MouseEventArgs(MouseButtons.None,

0, 0, 0, 0));



Specifies action

with anonymous

method



E



Uses

anonymous

method

shortcut



F



Uses

delegate

contravariance



The first part of the main code B is just C# 1 code, kept for comparison. The remaining delegates all use new features of C# 2. The conversion involved C makes event

subscription code read a lot more pleasantly—lines such as saveButton.Click +=

SaveDocument; are very straightforward, with no extra fluff to distract the eye. The

anonymous method syntax D is a little cumbersome, but does allow the action to

be very clear at the point of creation, rather than being another method to look at

before you understand what’s going on. The shortcut used E is another example of

anonymous method syntax, but this form can only be used when you don’t need the

parameters. Anonymous methods have other powerful features as well, but we’ll see

those later.

The final delegate instance created F is an instance of MouseEventHandler rather

than just EventHandler—but the HandleDemoEvent method can still be used due to

contravariance, which specifies parameter compatibility. Covariance specifies return

type compatibility. We’ll be looking at both of these in more detail in chapter 5. Event

handlers are probably the biggest beneficiaries of this, as suddenly the Microsoft

guideline to make all delegate types used in events follow the same convention makes



56



CHAPTER 2



Core foundations: building on C# 1



a lot more sense. In C# 1, it didn’t matter whether or not two different event handlers

looked “quite similar”—you had to have a method with an exactly matching signature

in order to create a delegate instance. In C# 2, you may well find yourself able to use

the same method to handle many different kinds of events, particularly if the purpose

of the method is fairly event independent, such as logging.

C# 3 provides special syntax for instantiating delegate types, using lambda expressions. To demonstrate these, we’ll use a new delegate type. As part of the CLR gaining

generics in .NET 2.0, generic delegate types became available and were used in a number of API calls in generic collections. However, .NET 3.5 takes things a step further,

introducing a group of generic delegate types called Func that all take a number of

parameters of specified types and return a value of another specified type. Listing 2.5

gives an example of the use of a Func delegate type as well as lambda expressions.

Listing 2.5



Lambda expressions, which are like improved anonymous methods



Func func = (x,y) => (x*y).ToString();

Console.WriteLine (func(5, 20));



Func is a delegate type that takes two integers and returns a string.



The lambda expression in listing 2.5 specifies that the delegate instance (held in

func) should multiply the two integers together and call ToString(). The syntax is



much more straightforward than that of anonymous methods, and there are other

benefits in terms of the amount of type inference the compiler is prepared to perform

for you. Lambda expressions are absolutely crucial to LINQ, and you should get ready

to make them a core part of your language toolkit. They’re not restricted to working

with LINQ, however—almost any use of anonymous methods from C# 2 can use

lambda expressions in C# 3.

To summarize, the new features related to delegates are as follows:













Generics (generic delegate types)—C# 2

Delegate instance creation expressions—C# 2

Anonymous methods—C# 2

Delegate covariance/contravariance—C# 2

Lambda expressions—C# 3



The use of generics extends well beyond delegates, of course—they’re one of the principle enhancements to the type system, which we’ll look at next.



2.4.2



Features related to the type system

The primary new feature in C# 2 regarding the type system is that of generics. It

largely addresses the issues I raised in section 2.2.2 about strongly typed collections,

although generic types are useful in a number of other situations too. As a feature, it’s

elegant, it solves a real problem, and despite a few wrinkles it generally works very

well. We’ve seen examples of this in quite a few places already, and it’s described fully

in the next chapter, so I won’t go into any more details here. It’ll be a brief reprieve,



C# 2 and 3: new features on a solid base



57



though—generics form probably the most important feature in C# 2 with respect to

the type system, and you’ll see generic types throughout the rest of the book.

C# 2 doesn’t tackle the general issue of covariant return types and contravariant

parameters, but it does cover it for creating delegate instances in certain situations, as

we saw in section 2.4.1. C# 3 introduces a wealth of new concepts in the type system,

most notably anonymous types, implicitly typed local variables, and extension methods. Anonymous types themselves are mostly present for the sake of LINQ, where it’s

useful to be able to effectively create a data transfer type with a bunch of read-only

properties without having to actually write the code for them. There’s nothing to stop

them from being used outside LINQ, however, which makes life easier for demonstrations. Listing 2.6 shows both features in action.

Listing 2.6



Demonstration of anonymous types and implicit typing



var jon = new { Name="Jon", Age=31 };

var tom = new { Name="Tom", Age=4 };

Console.WriteLine ("{0} is {1}", jon.Name, jon.Age);

Console.WriteLine ("{0} is {1}", tom.Name, tom.Age);



The first two lines each show implicit typing (the use of var) and anonymous object

initializers (the new {…} bit), which create instances of anonymous types.

There are two things worth noting at this stage, long before we get into the

details—points that have caused people to worry needlessly before. The first is that

C# is still statically typed. The C# compiler has declared jon and tom to be of a

particular type, just as normal, and when we use the properties of the objects they

are normal properties—there’s no dynamic lookup going on. It’s just that we (as

source code authors) couldn’t tell the compiler what type to use in the variable

declaration because the compiler will be generating the type itself. The properties

are also statically typed—here the Age property is of type int, and the Name property

of type string.

The second point is that we haven’t created two different anonymous types here.

The variables jon and tom both have the same type because the compiler uses the

property names, types, and order to work out that it can generate just one type and

use it for both statements. This is done on a per-assembly basis, and makes life a lot

simpler in terms of being able to assign the value of one variable to another (for

example, jon=tom; would be permitted in the previous code) and similar operations.

Extension methods are also there for the sake of LINQ but can be useful outside it.

Think of all the times you’ve wished that a framework type had a certain method, and

you’ve had to write a static utility method to implement it. For instance, to create a

new string by reversing an existing one you might write a static StringUtil.Reverse

method. Well, the extension method feature effectively lets you call that static method

as if it existed on the string type itself, so you could write

string x = "dlrow olleH".Reverse();



58



CHAPTER 2



Core foundations: building on C# 1



Extension methods also let you appear to add methods with implementations to

interfaces—and indeed that’s what LINQ relies on heavily, allowing calls to all kinds of

methods on IEnumerable that have never previously existed.

Here’s the quick-view list of these features, along with which version of C# they’re

introduced in:













Generics—C# 2

Limited delegate covariance/contravariance—C# 2

Anonymous types—C# 3

Implicit typing—C# 3

Extension methods—C# 3



After that fairly diverse set of features on the type system in general, let’s look at the

features added to one very specific part of typing in .NET—value types.



2.4.3



Features related to value types

There are only two features to talk about here, and C# 2 introduces them both. The

first goes back to generics yet again, and in particular collections. One common complaint about using value types in collections with .NET 1.1 was that due to all of the

“general purpose” APIs being specified in terms of the object type, every operation

that added a struct value to a collection would involve boxing it, and when retrieving it

you’d have to unbox it. While boxing is pretty cheap on a “per call” basis, it can cause

a significant performance hit when it’s used every time with frequently accessed collections. It also takes more memory than it needs to, due to the per-object overhead.

Generics fix both the speed and memory deficiencies by using the real type involved

rather than just a general-purpose object. As an example, it would have been madness

to read a file and store each byte as an element in an ArrayList in .NET 1.1—but in

.NET 2.0 it wouldn’t be particularly crazy to do the same with a List.

The second feature addresses another common cause of complaint, particularly

when talking to databases—the fact that you can’t assign null to a value type variable.

There’s no such concept as an int value of null, for instance, even though a database

integer field may well be nullable. At that point it can be hard to model the database

table within a statically typed class without a bit of ugliness of some form or another.

Nullable types are part of .NET 2.0, and C# 2 includes extra syntax to make them easy

to use. Listing 2.7 gives a brief example of this.

Listing 2.7



Demonstration of a variety of nullable type features



int? x = null;

Declares and sets nullable variable

x = 5;

if (x != null)

Tests for presence of “real” value

{

int y = x.Value;

Obtains “real” value

Console.WriteLine (y);

}

int z = x ?? 10;

Uses null-coalescing operator



Summary



59



Listing 2.7 shows a number of the features of nullable types and the shorthand that C#

provides for working with them. We’ll get around to the details of each feature in

chapter 4, but the important thing to think about is how much easier and cleaner all

of this is than any of the alternative workarounds that have been used in the past.

The list of enhancements is smaller this time, but they’re very important features

in terms of both performance and elegance of expression:







2.5



Generics—C# 2

Nullable types—C# 2



Summary

This chapter has provided some revision of a few topics from C# 1. The aim wasn’t to

cover any of the topics in its entirety, but merely to get everyone on the same page so

that I can describe the C# 2 and 3 features without worrying about the ground that

I’m building on.

All of the topics we’ve covered are core to C# and .NET, but within community discussions, blogs, and even occasionally books often they’re either skimmed over too

quickly or not enough care is taken with the details. This has often left developers with

a mistaken understanding of how things work, or with an inadequate vocabulary to

express themselves. Indeed, in the case of characterizing type systems, computer science

has provided such a variety of meanings for some terms that they’ve become almost useless. Although this chapter hasn’t gone into much depth about any one point, it will

hopefully have cleared up any confusion that would have made the rest of the book

harder to understand.

The three core topics we’ve briefly covered in this chapter are all significantly

enhanced in C# 2 and 3, and some features touch on more than one topic. In particular,

generics has an impact on almost every area we’ve covered in this chapter—it’s probably the most widely used and important feature in C# 2. Now that we’ve finished all our

preparations, we can start looking at it properly in the next chapter.



Part 2

C#2:

solving the issues

of C#1



I



n part 1 we took a quick look at a few of the features of C# 2. Now it’s time to

do the job properly. We’ll see how C# 2 fixes various problems that developers

ran into when using C# 1, and how C# 2 makes existing features more useful by

streamlining them. This is no mean feat, and life with C# 2 is much more pleasant

than with C# 1.

The new features in C# 2 have a certain amount of independence. That’s not

to say they’re not related at all, of course; many of the features are based on—or

at least interact with—the massive contribution that generics make to the language. However, the different topics we’ll look at in the next five chapters don’t

combine into one cohesive whole.

The first four chapters of this part cover the biggest new features. We’ll look

at the following:

















Generics-—The most important new feature in C# 2 (and indeed in the

CLR for .NET 2.0), generics allow type and method parameterization.

Nullable types —Value types such as int and DateTime don’t have any concept of “no value present”—nullable types allow you to represent the

absence of a meaningful value.

Delegates —Although delegates haven’t changed at the CLR level, C# 2

makes them a lot easier to work with. As well as a few simple shortcuts, the

introduction of anonymous methods begins the movement toward a more

functional style of programming—this trend continues in C# 3.

Iterators —While using iterators has always been simple in C# with the

foreach statement, it’s a pain to implement them in C# 1. The C# 2 compiler is happy to build a state machine for you behind the scenes, hiding a

lot of the complexity involved.



Having covered the major, complex new features of C# 2 with a chapter dedicated to

each one, chapter 7 rounds off our coverage by covering several simpler features. Simpler doesn’t necessarily mean less useful, of course: partial types in particular are very

important for better designer support in Visual Studio 2005.

As you can see, there’s a lot to cover. Take a deep breath, and let’s dive into the

world of generics…



Parameterized

typing with generics



This chapter covers





Generic types and methods







Generic collections in .NET 2.0







Limitations of generics







Comparisons with other languages



True1 story: the other day my wife and I did our weekly grocery shopping. Just

before we left, she asked me if I had the list. I confirmed that indeed I did have the

list, and off we went. It was only when we got to the grocery store that our mistake

made itself obvious. My wife had been asking about the shopping list whereas I’d

actually brought the list of neat features in C# 2. When we asked an assistant

whether we could buy any anonymous methods, we received a very strange look.

If only we could have expressed ourselves more clearly! If only she’d had some

way of saying that she wanted me to bring the list of items we wanted to buy! If only

we’d had generics…

1



By which I mean “convenient for the purposes of introducing the chapter”—not, you know, accurate as such.



63



64



CHAPTER 3



Parameterized typing with generics



For most people, generics will be the most important new feature of C# 2. They

enhance performance, make your code more expressive, and move a lot of safety

from execution time to compile time. Essentially they allow you to parameterize types

and methods—just as normal method calls often have parameters to tell them what

values to use, generic types and methods have type parameters to tell them what types

to use. It all sounds very confusing to start with—and if you’re completely new to

generics you can expect a certain amount of head scratching—but once you’ve got

the basic idea, you’ll come to love them.

In this chapter we’ll be looking at how to use generic types and methods that others

have provided (whether in the framework or as third-party libraries), and how to write

your own. We’ll see the most important generic types within the framework, and take a

look just under the surface to understand some of the performance implications of

generics. To conclude the chapter, I’ll present some of the most frequently encountered limitations of generics, along with possible workarounds, and compare generics

in C# with similar features in other languages.

First, though, we need to understand the problems that caused generics to be

devised in the first place.



3.1



Why generics are necessary

Have you ever counted how many casts you have in your C# 1 code? If you use any of

the built-in collections, or if you’ve written your own types that are designed to work

with many different types of data, you’ve probably got plenty of casts lurking in your

source, quietly telling the compiler not to worry, that everything’s fine, just treat the

expression over there as if it had this particular type. Using almost any API that has

object as either a parameter type or a return type will probably involve casts at some

point. Having a single-class hierarchy with object as the root makes things more

straightforward, but the object type in itself is extremely dull, and in order to do anything genuinely useful with an object you almost always need to cast it.

Casts are bad, m’kay? Not bad in an “almost never do this” kind of way (like mutable structs and nonprivate fields) but bad in a “necessary evil” kind of way. They’re an

indication that you ought to give the compiler more information somehow, and that

the way you’re choosing is to get the compiler to trust you at compile time and generate a check to run at execution time, to keep you honest.

Now, if you need to tell the compiler the information somewhere, chances are that

anyone reading your code is also going to need that same information. They can see it

where you’re casting, of course, but that’s not terribly useful. The ideal place to keep

such information is usually at the point of declaring a variable or method. This is even

more important if you’re providing a type or method which other people will call without

access to your code. Generics allow library providers to prevent their users from compiling

code that calls the library with bad arguments. Previously we’ve had to rely on manually

written documentation—which is often incomplete or inaccurate, and is rarely read anyway. Armed with the extra information, everyone can work more productively: the compiler is able to do more checking; the IDE is able to present IntelliSense options based



Simple generics for everyday use



65



on the extra information (for instance, offering the members of string as next steps

when you access an element of a list of strings); callers of methods can be more certain

of correctness in terms of arguments passed in and values returned; and anyone maintaining your code can better understand what was running through your head when you

originally wrote it in the first place.

NOTE



Will generics reduce your bug count? Every description of generics I’ve read

(including my own) emphasizes the importance of compile-time type

checking over execution-time type checking. I’ll let you in on a secret: I

can’t remember ever fixing a bug in released code that was directly due

to the lack of type checking. In other words, the casts we’ve been putting

in our C# 1 code have always worked in my experience. Those casts have

been like warning signs, forcing us to think about the type safety explicitly rather than it flowing naturally in the code we write. Although generics may not radically reduce the number of type safety bugs you encounter,

the greater readability afforded can reduce the number of bugs across

the board. Code that is simple to understand is simple to get right.



All of this would be enough to make generics worthwhile—but there are performance

improvements too. First, as the compiler is able to perform more checking, that leaves

less needing to be checked at execution time. Second, the JIT is able to treat value types

in a particularly clever way that manages to eliminate boxing and unboxing in many situations. In some cases, this can make a huge difference to performance in terms of

both speed and memory consumption.

Many of the benefits of generics may strike you as being remarkably similar to the

benefits of static languages over dynamic ones: better compile-time checking, more

information expressed directly in the code, more IDE support, better performance.

The reason for this is fairly simple: when you’re using a general API (for example,

ArrayList) that can’t differentiate between the different types, you effectively are in a

dynamic situation in terms of access to that API. The reverse isn’t generally true, by the

way—there are plenty of benefits available from dynamic languages in many situations, but they rarely apply to the choice between generic/nongeneric APIs. When you

can reasonably use generics, the decision to do so is usually a no-brainer.

So, those are the goodies awaiting us in C# 2—now it’s time to actually start using

generics.



3.2



Simple generics for everyday use

The topic of generics has a lot of dark corners if you want to know everything about it.

The C# 2 language specification goes into a great deal of detail in order to make sure

that the behavior is specified in pretty much every conceivable case. However, we

don’t need to understand most of those corner cases in order to be productive. (The

same is true in other areas, in fact. For example, you don’t need to know all the exact

rules about definitely assigned variables—you just fix the code appropriately when the

compiler complains.)



Xem Thêm
Tải bản đầy đủ (.pdf) (424 trang)

×