Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C# ПІДРУЧНИКИ / c# / Apress - Accelerated C# 2005

.pdf
Скачиваний:
81
Добавлен:
12.02.2016
Размер:
2.09 Mб
Скачать

254 C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

Figure 10-1. Typical interface-based implementation of the Strategy pattern

Delegates offer a lighter-weight alternative to using interfaces to implement a simple strategy. Interfaces are merely a mechanism to implement a programming contract. Instead, imagine that your delegate declaration is used to implement the contract, and any method that matches the delegate signature is a potential concrete strategy. Now, instead of the consumer holding onto a reference to the abstract strategy interface, it simply holds onto a delegate instance. The following example illustrates this scenario:

using System;

using System.Collections;

public delegate Array SortStrategy( ICollection theCollection );

public class Consumer

{

public Consumer( SortStrategy defaultStrategy ) { this.strategy = defaultStrategy;

}

private SortStrategy strategy; public SortStrategy Strategy { get { return strategy; }

set { strategy = value; }

}

public void DoSomeWork() { // Employ the strategy.

Array sorted = strategy( myCollection );

// Do something with the results.

}

private ArrayList myCollection;

}

public class SortAlgorithms

{

static Array SortFast( ICollection theCollection ) { // Do the fast sort.

}

C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

255

static Array SortSlow( ICollection theCollection ) { // Do the slow sort.

}

}

When the Consumer object is instantiated, it is passed a default sort strategy, which is nothing more than a method that implements the SortStrategy delegate signature. If the conditions are right at run time, the sort strategy is swapped out and the Consumer.DoSomeWork method automatically calls into the replacement strategy. You could argue that implementing a strategy pattern this way is even more flexible than using interfaces, since delegates can bind to both static methods and instance methods. Therefore, you could create a concrete implementation of the strategy that also contains some state data that is needed for the operation, as long as the delegate points to an instance method on a class that contains that state data. Similarly, the delegate could be an anonymous method returned by a property of that class.

Summary

Delegates offer a first-class system-defined and system-implemented mechanism for uniformly representing callbacks. In this chapter, you saw various ways to declare and create delegates of different types, including single delegates, chained delegates, open instance delegates, and anonymous methods, which are themselves delegates. Additionally, I showed how to use delegates as the building blocks of events. You can use delegates to implement a wide variety of design patterns, since delegates are a great means for defining a programming contract. And at the heart of just about all design patterns is a well-defined contract.

The next chapter covers the details of generics, which is arguably one of the most exciting additions to C# 2005.

C H A P T E R 1 1

■ ■ ■

Generics

Support for generics is one of the biggest additions to C# 2005 and the .NET Framework 2.0. Generics allow you to create open-ended types that are converted into closed types at run time. Each unique closed type is itself a unique type. Only closed types may be instantiated.

Introduction to Generics

If you’re at all familiar with C++ templates, then you already know the general idea behind generics, even though many significant differences exist between the two. When you declare a generic type, you specify a list of type parameters in the declaration for which type arguments are given to create closed types, as in the following example:

public class MyCollection<T>

{

public MyCollection() {

}

private T[] storage;

}

In this case, I’ve declared a generic type, MyCollection<T>, which treats the type within the collection as an unspecified type. In this example, the type parameter list consists of only one type, and it is described with a syntax where the generic types are listed, separated by commas, between angle brackets. The identifier T is really just a placeholder for any type. At some point, a consumer of MyCollection<T> will declare what’s called a closed type, by specifying the concrete type that T is supposed to represent. For example, suppose some other assembly wants to create a MyCollection<T> constructed type that contains members of type int. Then it would do so as shown in the following code:

public void SomeMethod() {

MyCollection<int> collectionOfNumbers = new MyCollection<int>();

}

MyCollection<int> in the previous code is the closed type. MyCollection<int> is usable just as any other declared type, and it also follows all of the same rules that other nongeneric types follow. The only difference is that it was born from a generic type. At the point of instantiation, the IL code behind the implementation of MyCollection<T> gets JIT-compiled in a way that all of the usages of type T in the implementation of MyCollection<T> get replaced with type int.

Note that all unique constructed types created from the same generic type are, in fact, completely different types that share no implicit conversion capabilities. For example, MyCollection<long> is a completely different type than MyCollection<int>, and you cannot do something like the following:

257

258 C H A P T E R 1 1 G E N E R I C S

// THIS WILL NOT WORK!!!

public void SomeMethod( MyCollection<int> intNumbers ) {

MyCollection<long> longNumbers = intNumbers;

// ERROR!

}

 

If you’re familiar with the array covariance rules that allow you to do the following

public void ProcessStrings( string[] myStrings ) { object[] objs = myStrings;

foreach( object o in objs ) { Console.WriteLine( o );

}

}

then you might be surprised that you cannot accomplish the same thing using constructed generic types. The difference is that with array covariance, the source and the destination of the assignment are of the same type, System.Array. The array covariance rules simply allow you to assign one array from another, as long as the declared type of the elements in the array are implicitly convertible at compile time. However, in the case of two constructed generic types, they are completely separate types.

Difference Between Generics and C++ Templates

It’s no accident that the syntax of generics is similar to that of C++ templates when, after all, every other syntax in C# is based off of the C++ syntax to allow you to leverage your existing knowledge. As is typical throughout C#, the C# designers have streamlined the syntax and removed some of the verbosity.

However, the similarities end there, because C# generics are very different behaviorally than C++ templates, and you must make sure that you understand the differences. Otherwise, you may find yourself attempting to apply your C++ template knowledge in ways that simply won’t work with generics.

The main difference between the two is that expansion of generics is dynamic, whereas expansion of C++ templates is static. In other words, C++ templates are always expanded at compile time. Therefore, the C++ compiler must have access to all template types—generally through header files— and any types used to create the closed types from the template types at compile time. For this reason alone, it is impossible to package C++ templates into libraries. I know that many developers get confused by this fact when learning C++ templates for the first time. I remember plenty of times where it would have been nice to be able to package a C++ template into a static library or a DLL. Unfortunately, this is just not possible. That’s why all of the code for C++ template types usually lives in headers. This makes it difficult to package proprietary library code within C++ templates, since you have to essentially give your code away to anyone who needs to consume it. The STL is a perfect example: Notice how almost every bit of your favorite STL implementation exists in header files.

Generics, on the other hand, can be packaged in assemblies and consumed later. Instead of forming constructed types at compile time, constructed types are formed at run time, or more specifically, at JIT-compile time. In many ways, this makes generics more flexible. However, as with just about anything in the engineering world, advantages come with disadvantages. And it’s these disadvantages that will force you to treat generics significantly differently at design time than C++ templates, as you’ll see at the end of this chapter.

Note Each time the JIT compiler forms a closed type, a new type is initialized for the application domain that uses it. Naturally, this places a demand on the memory consumption of the application, also known as the working set. Once a type is initialized and loaded into an application domain, you cannot uninitialize and unload it without destroying the application domain as well. Under some rare circumstances, you may need to consider these ramifications when designing systems that use generics. In general, though, such concerns are typically minimal.

C H A P T E R 1 1 G E N E R I C S

259

Efficiency and Type Safety of Generics

Arguably, efficiency is one of the greatest gains from generics in C#. Whereas a regular array based on System.Array can contain a heterogeneous collection of instances created from many types as long as it holds references to a common base type such as System.Object, it does come with its drawbacks. Take a look at the following usage:

public void SomeMethod( ArrayList col ) { foreach( object o in col ) {

ISomeInterface iface = (ISomeInterface) o; o.DoSomething();

}

}

Since everything in the CLR is derived from System.Object, the ArrayList passed in via the col variable could possibly contain a hodgepodge of things. Some of those things may not actually implement ISomeInterface. As you’d expect, an InvalidCastException could erupt from this code. However, wouldn’t it be nice to be able to utilize the C# compiler’s type engine to help sniff out such things at compile time? That’s exactly what generics allow you to do. Using generics, you can devise something like the following:

public void SomeMethod( IList<ISomeInterface> col ) { foreach( ISomeInterface iface in col ) {

o.DoSomething();

}

}

In the previous example, the method accepts an interface of IList<T>. Since the type parameter to the constructed type is of type ISomeInterface, the only type of objects that the list may hold are those of type ISomeInterface. Instantly, the compiler has a bigger stick to wield while enforcing type safety.

Note Added type safety at compile time is always a good thing, because it’s much better to capture bugs based on type mismatches earlier at compile time rather than later at run time.

You could have solved the same problem without using generics, but you would have had to have written a class by hand that would have served the same purpose as the List<ISomeInterface> constructed type. Thus, another beauty of generics is similar to that of C++ templates: They provide an easy-to-specialize shell for new types to be built from.

The compiler is your friend, and you should always provide it with as much type information as possible to help it do its job. Since everything in C# and the CLR derives from System.Object one way or another, you can easily cast away all type information from objects, thus crippling the compiler. If you come from a C++ environment, just imagine how ugly things could get if you preferred to pass around pointers to objects as void*. And that’s not even mentioning the hard-to-find bugs that would come from such madness.

The previous example shows how to use generics for better type safety. However, you haven’t really gained much yet from an efficiency standpoint. The real efficiency gain comes into play when the type argument is a value type. Remember that a value type inserted into a collection in the System.Collections namespace, such as ArrayList, must first be boxed, since the ArrayList maintains a collection of System.Object types. An ArrayList meant to hold nothing but a bunch of integers suffers from severe efficiency problems, since the integers must be boxed and unboxed each time they are inserted and referenced or extracted from the ArrayList, respectively. Also, an unboxing operation in C# is normally formed with an IL unbox operation paired with a copy operation

260C H A P T E R 1 1 G E N E R I C S

on the value type’s data. Generics come to the rescue and stop this madness. As an example, compile the following code, and then load the assembly into ILDASM to compare the IL generated for each of the methods that accept a stack:

using System;

using System.Collections;

using System.Collections.Generic;

public class EntryPoint

{

static void Main() {

}

public void NonGeneric( Stack stack ) { foreach( object o in stack ) {

int number = (int) o; Console.WriteLine( number );

}

}

public void Generic( Stack<int> stack ) { foreach( int number in stack ) {

Console.WriteLine( number );

}

}

}

You’ll notice that the IL code generated by the NonGeneric method has at least 10 more instructions than the generic version. Most of this is attributed to the type coercing and unboxing that the NonGeneric method must do. Furthermore, the NonGeneric method could possibly throw an InvalidCastException if it encounters an object that cannot be explicitly cast and unboxed into an integer at run time.

Clearly, generics offer the compiler much greater latitude to help it do its job by not stripping away precious type information at compile time. However, you could argue that the efficiency gain is so high that the primary motivator for generics in the CLR was to avoid unnecessary boxing operations. Either way, both gains are extremely significant and worth utilizing to the fullest extent.

Generic Type Placeholder Naming Conventions

Although there are no hard-and-fast rules for naming generic parameter placeholders, it is recommended that you at least provide a name that is somewhat descriptive for how the type is going to be used. Additionally, placeholder identifiers conventionally make the first letter a capital T to denote it as a type. Naming conventions like these, similar to the naming convention where interface names start with a capital I, provide for code that is generally easier to read. If the generic type definition has only one type parameter and it’s simple to understand, it’s conventional to name it T.

Generic Type Definitions and Constructed Types

As I touched upon previously, a generic type is a compiled type that is unusable until a closed type is created from it. A nongeneric type is also known as a closed type, whereas a generic type is known as an open type. However, it is possible to define a new open type via a generic, as shown in the following example:

C H A P T E R 1 1 G E N E R I C S

261

public class MyClass<T>

{

private T innerObject;

}

public class Consumer<T>

{

private MyClass< Stack<T> > obj;

}

In this case, a generic type, Consumer<T>, is defined and also contains a field that is based on another generic type. When declaring the type of the Consumer<T>.obj field, MyClass< Stack<T> > remains open until someone declares a constructed type based on Consumer<T>, thus creating

a closed type for the contained field.

Generic Classes and Structs

So far, all of the examples I’ve shown you have been generic classes, but all of the rules of generics map equally to structs. In fact, the most common types of generic declaration you will use are generic classes and structs. Also, I’ve been running pretty fast and loose with my terminology, so from now on, I’ll be more explicit.

Overall, declarations of all generic struct and class types follow the same rules as those for regular struct and class types. Any time a class declaration contains a type parameter list, it is, from that point on, a generic type. Likewise, any nested class declaration—whether it’s generic or not— that is declared within the scope of a generic type is a generic type itself. That’s because the enclosing type’s fully qualified name requires a type argument in order to completely specify the nested type.

Generic types are overloaded based upon the number of arguments in their type argument lists. The following example illustrates what I mean:

public class Container {} public class Container<T> {} public class Container<T, R> {}

Each of the previous declarations is valid within the same namespace. You can declare as many generic types based on the Container identifier as you want, as long as each one has a different count of type parameters. You cannot declare another type named Container<X, Y>, even though the identifiers used in the type parameters list are different. The name overloading rules for generic declarations are based on the count of type parameters rather than the names given to their placeholders.

When you declare a generic type, you’re declaring what is called an open type. It’s called an open type because its fully specified type is not yet known. When you declare another type based upon the generic type definition, you’re declaring what’s called a constructed type, as shown here:

public class MyClass<T>

{

private Container<int> field1; private Container<T> field2;

}

Both fields in the previous declaration of MyClass<T> are constructed types, since they declare a new type based upon the generic type Container<T>. However, not every constructed type is a closed type. Only field1 is a closed type, whereas field2 is an open type, since its final type must still be determined at run time based on the type arguments from MyClass<T>.

In C#, all identifiers are declared and are valid within a specific scope. Within the confines of

a method, for example, any local variable identifiers declared within the curly braces of the method are only available within that scope. Similar rules exist for type parameter identifiers within

262C H A P T E R 1 1 G E N E R I C S

generics. In the previous example, the identifier T is only valid within the scope of the class declaration itself. Consider the following nested class example:

public class MyClass<T>

{

public class MyNestedClass<R>

{

}

}

The identifier R is only valid within the scope of the nested class, and you may not use it within the outer scope of the declaration for MyClass<T>. However, you may use T in the nested class, since the nested class is defined within the scope within which T is valid. It is generally considered to be bad form to hide outer argument identifiers within nested scopes, just as it is with variable name identifiers within nested execution scopes. For example, try to follow this confusing code:

public class MyClass<T>

{

public class MyNestedClass<T>

{

}

private Containter<T> field1;

static void Main() {

// What does this mean for MyNestedClass? MyClass<int> closedTypeInstance = null;

}

}

When the closed type MyClass<int> is declared in Main(), what does it mean for the nested type? The answer is, nothing. Even though the MyNestedClass<T> declaration uses the same type argument, it does not expand into the following:

// This is NOT what happens! public class MyClass<int>

{

public class MyNestedClass<int>

{

}

private Containter<int> field1;

}

Just because the type parameter for the MyClass<T> type has been specified, it does not mean that the MyNestedClass<T> has been specified as well. In fact, it would be more accurate to describe the resultant MyClass<int> as follows:

public class MyClass<int>

{

public class MyNestedClass<T>

{

}

private Containter<int> field1;

}

MyNestedClass<T> still remains open, even though it used the same identifier in its parameter list as the containing type. What’s actually happening is that within the curly braces of MyNestedClass<T>,

Note

C H A P T E R 1 1 G E N E R I C S

263

the outer type argument to MyClass<T> is hidden from access by the identifier of the inner scope. It is better to declare it as follows:

public class MyClass<T>

{

public class MyNestedClass<R>

{

private T innerfield1; private R innerfield2;

}

private Containter<T> field1;

static void Main() {

// What does this mean for MyNestedClass? MyClass<int> closedTypeInstance = null;

}

}

Now, the declaration scope of MyNestedClass<R> has access to both the T and R type parameters as illustrated.

Generic structs and classes, just like normal structs and classes, may contain static types. However, each closed type based on the generic type contains its own instance of the static type. When you consider that each closed type is a separate concrete type, this fact makes perfect sense. Therefore, if you need to share static data between different closed types based on the same generic type, you must devise some other means to do so. One technique involves a separate, nongeneric type that contains static data that is referenced by the generic types. Such a device is typically implemented with the Singleton pattern.

Note Keep in mind that generic types with static initializers require that the initialization code be run each and every time the CLR creates a closed type based upon the generic type. Complex type initializers, or static constructors, can possibly increase the working set of the application if too many closed types are created based upon such a generic type. For example, if you create a sizable per-type data structure in a generic type initializer, you could create a hidden source of memory consumption if many types are formed from it.

Generic Interfaces

Along with classes and structs, you can also create generic interface declarations. This concept is a natural progression from struct and class generics. Naturally, a whole host of interfaces declared

within the .NET 1.1 base class library make excellent candidates to have generic versions fashioned after them. A perfect example is IEnumerable<T>. Generic containers create much more efficient code than nongeneric containers when they contain value types, since they avoid any unnecessary boxing. It’s only natural that any generic enumerable interface must have a means of enumerating the generic items within. Thus, IEnumerable<T> exists and any enumerable containers you implement yourself should implement this interface, or you could get it for free by deriving your custom containers from Collection<T>.

When creating your own custom collection types, you should derive them from Collection<T> in the System.Collections.ObjectModel namespace. Other types, such as List<T>, are not meant to be derived from and are intended as a lower-level storage mechanism. Collection<T> implements protected virtual methods that you can override to customize its behavior, whereas List<T> does not.

Соседние файлы в папке c#