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
Console.WriteLine (func(5, 20));
Func
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
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.)