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

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

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

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

}

void set_salary( long salary ) { this->salary = salary;

}

private:

long salary;

};

template< class T > class MyClass : public T

{

};

void main()

{

MyClass<Employee> myInstance; myInstance.get_salary();

}

In the main function, pay attention to the call to get_salary(). Even though it looks odd at first, it works just fine, because MyClass<T> inherits the implementation of whatever type is specified for T at compile time. In this case, that type, Employee, implements get_salary(), and MyClass<Employee> inherits that implementation. Clearly, an assumption is being placed on the type that is provided for T in MyClass<T>, and that is that the type will support a method named get_salary(). If it does not, the C++ compiler will complain at compile time. This is a form of static polymorphism or policybased programming. In traditional cases, polymorphism is explained within the context of virtual methods known as dynamic polymorphism. You cannot implement static polymorphism with C# generics. However, you can require that the type arguments given when forming a closed type support a specific contract by using a mechanism called constraints, which I cover in the following section.

Constraints

So far, the majority of generics examples that I’ve shown involve some sort of collection-style class that holds a bunch of objects or values of a specific type. But many times, you’ll need to create generic types that not only contain instances of various types but also use those objects directly. For example, suppose you have a generic type that holds instances of arbitrary geometric shapes that all implement a property named Area. Also, you need the generic type to implement a property— say, TotalArea—where all the areas of the contained shapes are accumulated. The guarantee here is that each geometric shape in the generic container will implement the Area property. You may be inclined to write code like the following:

using System;

using System.Collections.Generic;

public interface IShape

{

double Area { get;

}

}

public class Circle : IShape

{

public Circle( double radius ) {

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

275

this.radius = radius;

}

public double Area { get {

return 3.1415*radius*radius;

}

}

private double radius;

}

public class Rect : IShape

{

public Rect( double width, double height ) { this.width = width;

this.height = height;

}

public double Area { get {

return width*height;

}

}

private double width; private double height;

}

public class Shapes<T>

{

public double TotalArea { get {

double acc = 0;

foreach( T shape in shapes ) { // THIS WON'T COMPILE!!! acc += shape.Area;

}

return acc;

}

}

public void Add( T shape ) { shapes.Add( shape );

}

private List<T> shapes = new List<T>();

}

public class EntryPoint

{

static void Main() {

Shapes<IShape> shapes = new Shapes<IShape>();

shapes.Add( new Circle(2) ); shapes.Add( new Rect(3, 5) );

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

Console.WriteLine( "Total Area: {0}", shapes.TotalArea );

}

}

There is one major problem, as the code won’t compile. The offending line of code is inside the TotalArea property of Shapes<T>. The compiler complains with the following error:

error CS0117: 'T' does not contain a definition for 'Area'

Those of you familiar with C++ templates know that this will work with C++ templates. After all, in the C++ template world, this technique is an alternate way of implementing polymorphic behavior through so-called policies, or static polymorphism. Static polymorphism doesn’t require virtual methods in C++. In this case, the only requirement on type T is that it should support the property Area. In C++, if it does not, the compiler will complain at compile time.

All of this talk of requiring the contained type T to support the Area property sounds a lot like a contract, because it is! C# generics are dynamic as opposed to static in nature, so you cannot achieve the same effect without some extra information. Whenever you hear the word contract within the C# world, you may start thinking about interfaces. Therefore, I’ve chosen to have both of my shapes implement the IShape interface. Thus, the IShape interface defines the contract, and the shapes implement that contract. However, that still is not enough for the C# compiler to be able to compile the previous code.

C# generics must have a way to enforce the rule that the type T supports a specific contract at run time, since constructed types are formed dynamically at run time. A naïve attempt to solve the problem could look like the following:

public class Shapes<T>

{

public double TotalArea { get {

double acc = 0;

foreach( T shape in shapes ) { // DON'T DO THIS!!!

IShape theShape = (IShape) shape; acc += theShape.Area;

}

return acc;

}

}

public void Add( T shape ) { shapes.Add( shape );

}

private List<T> shapes = new List<T>();

}

This modification to Shapes<T> indeed does compile and work, most of the time. However, this generic has lost some of its innocence due to the type cast within the foreach loop. Just imagine if, during a late-night caffeine-induced trance, you attempted to create a constructed type Shapes<int>. The compiler would happily oblige. But what would happen if you tried to get the TotalArea property from a Shapes<int> instance? As expected, you would be treated to a runtime exception as the TotalArea property accessor attempts to cast an int into an IShape. One of the primary benefits of using generics is better type safety, but in this example, I’ve tossed type safety right out the window. So, what are you supposed to do? The answer lies in a concept called generic constraints. Check out the following correct implementation:

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

277

public class Shapes<T> where T: IShape

{

public double TotalArea { get {

double acc = 0;

foreach( T shape in shapes ) { acc += shape.Area;

}

return acc;

}

}

public void Add( T shape ) { shapes.Add( shape );

}

private List<T> shapes = new List<T>();

}

Notice the extra line under the first line of the class declaration using the where keyword. This says, “Define class Shapes<T> where T must implement IShape.” Now the compiler has everything it needs to enforce type safety, and the JIT compiler has everything it needs to build working code at run time. The compiler has been given a hint to help it notify you, with a compile-time error, when you attempt to create constructed types where T does not implement IShape.

The syntax for constraints is pretty simple. Multiple where clauses can have one where clause for each type parameter. Any number of interfaces may be listed following the type parameter in the where clause, but only one class at most. This restriction is intuitive, since a given type may only derive from one class but may implement an unlimited amount of interfaces. Additionally, you may use special keywords in the constraint clause for a particular type argument. Only one constraint can name a class type (since the CLR has no concept of multiple inheritance), so that constraint is known as the primary constraint. Additionally, instead of specifying a class name, the primary constraint may list the special words class or struct, which are used to indicate that the type parameter must be a class or a struct. The constraint clause can then include as many secondary constraints as possible, and they are usually a list of interfaces. Finally, you can list a constructor constraint that takes the form new(). This constrains the parameterized type such that it is required to have a default, parameterless constructor. Class types must have an explicitly defined default constructor, whereas the new() constraint is automatic for value types since they have a system-generated default constructor.

It is customary to list each where clause on a separate line in any order under the class header. A comma separates each constraint following the colon in the where clause. That said, let’s take

a look at some constraint examples:

using System.Collections.Generic;

public class MyValueList<T> where T: struct

//But can't do the following

//where T: struct, new()

{

public void Add( T v ) { imp.Add( v );

}

private List<T> imp = new List<T>();

}

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

public class EntryPoint

{

static void Main() { MyValueList<int> intList =

new MyValueList<int>();

intList.Add( 123 );

//CAN'T DO THIS.

//MyValueList<object> objList =

//new MyValueList<object>();

}

}

In the previous code, you can see an example of the struct constraint. For one reason or another, you may find it necessary to create a container that can only contain value types. Alternatively, the constraint could have also claimed to only allow class types. Incidentally, in the Visual Studio 2005 version of the C# 2.0 compiler, I’m unable to create a constraint that includes both class and struct. Of course, doing so is pointless, since the same effect comes from including neither struct nor class in the constraints list. Nevertheless, the compiler complains with an error if you try to do so, claiming the following:

error CS0449: The 'class' or 'struct' constraint must come before any other constraints

This looks like the compiler error could be better stated by saying that only one primary constraint is allowed in a constraint clause. You’ll also see that I commented out an alternate constraint line, where I attempted to include the new() constraint to force the type given for T to support

a default constructor. Clearly, for value types, this constraint is redundant and should be harmless to specify. Even so, the compiler won’t allow you to provide the new() constraint together with the struct constraint. Now let’s look at a slightly more complex example that shows two constraint clauses:

using System;

using System.Collections.Generic;

public interface IValue

{

// IValue methods.

}

public class MyDictionary<TKey, TValue> where TKey: struct, IComparable<TKey> where TValue: IValue, new()

{

public void Add( TKey key, TValue val ) { imp.Add( key, val );

}

private Dictionary<TKey, TValue> imp = new Dictionary<TKey, TValue>();

}

I’ve declared MyDictionary<TKey, TValue> in such a way that the key value is constrained to value types. I also want those key values to be comparable, so I’ve required the TKey type to implement IComparable<TKey>. This example shows two constraint clauses, one for each type parameter.

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

279

In this case, I’m allowing the TValue type to be either a struct or a class, but I do require that it support the defined IValue interface as well as a default constructor.

Overall, the constraint mechanism built into C# generics is simple and straightforward. The complexity of constraints is easy to manage and decipher with little or no surprises. As the language and the CLR evolve, I suspect that this area will see some additions as more and more applications for generics are explored. For example, the ability to use the class and struct constraints within

a constraint clause was a relatively late addition to the standard.

Finally, the format for constraints on generic interfaces is identical to that of generic classes and structs.

Constraints on Nonclass Types

So far, I’ve discussed constraints within the context of classes, structs, and interfaces. In reality, any entity that you can declare generically is capable of having an optional constraints clause. For generic method and delegate declarations, the constraints clauses follow the formal parameter list to the method or delegate. When using constraint clauses with method and delegate declarations, it does provide for some odd-looking syntax, as shown in the following example:

using System;

public delegate R Operation<T1, T2, R>( T1 val1, T2 val2 )

where T1: struct where T2: struct where R: struct;

public class EntryPoint

{

public static double Add( int val1, float val2 ) { return val1 + val2;

}

static void Main() {

Operation<int, float, double> op =

new Operation<int, float, double>( EntryPoint.Add );

Console.WriteLine( "{0} + {1} = {2}",

1, 3.2, op(1, 3.2f) );

}

}

I’ve declared a generic delegate for an operator method that accepts two parameters and has

a return value. My constraint is that the parameters and the return value all must be value types. For generic methods, the constraints clauses follow the method declaration but precede the method body. Notice that at the point of creation in the Main method, I had to tell the compiler what exact constructed type of the Operation<T1, T2, R> delegate I needed.

Generic System Collections

It seems that the most natural use of generics within C# and the CLR is for collection types. Maybe that’s because you can gain a huge amount of efficiency when using generic containers to hold value types when compared to the collection types within the System.Collections namespace. Of course, you cannot overlook the added type safety that comes with using the generic collections. Any time

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

you get added type safety, you’re guaranteed to reduce runtime type conversion exceptions, since the compiler can catch many of those at compile time.

I encourage you to look at the .NET Framework documentation for the System.Collections. Generic namespace. There you will find all of the generic collection classes made available by the Framework. Included in the namespace are Dictionary<TKey, TValue>, LinkedList<T>, List<T>, Queue<T>, SortedDictionary<TKey, TValue>, SortedList<T>, and Stack<T>.

Based on their names, the uses of these types should feel familiar when compared to the nongeneric classes under System.Collections. Although the collections of containers within the System.Collections.Generic namespace may not seem complete for your needs, it opens up the possibility for you to create your own collections, especially given the extendable types in

System.Collections.ObjectModel.

When creating your own collection types, you’ll often find the need to be able to compare the contained objects. When coding in C#, it feels natural to use the built-in equality and inequality operators to perform the comparison. However, I suggest that you stay away from them, because the support of operators by classes and structs—although possible—is not part of the CLS. Some languages have been slow to pick up support for operators. Therefore, your container must be prepared for the case when it contains types that don’t support operators for comparison. This is one of the reasons that interfaces such as IComparer and IComparable exist.

When you create an instance of the SortedList type within System.Collections, you have the opportunity to provide an instance of an object that supports IComparer. The SortedList then utilizes that object when it needs to compare two key instances that it contains. If you don’t provide an object that supports IComparer, the SortedList looks for an IComparable interface on the contained key objects to do the comparison. Naturally, you’ll need to provide an explicit comparer if the contained key objects don’t support IComparable. The overloaded versions of the constructor that accept an IComparer type exist specifically for that case.

The generic version of the sorted list, SortedList<TKey, TValue>, follows the same sort of pattern. When you create a SortedList<TKey, TValue>, you have the option of providing an object that implements the IComparer<T> interface so it can compare two keys. If you don’t provide one, the SortedList<TKey, TValue> defaults to using what’s called the generic comparer. The generic comparer is simply an object that derives from the abstract Comparer<T> class and can be obtained through the static property Comparer<T>.Default. Based upon the nongeneric SortedList, you might think that if the creator of SortedList<TKey, TValue> did not provide a comparer, it would just look for IComparable<T> on the contained key type. This approach would cause problems, since the contained key type could either support IComparable<T> or the nongeneric IComparable. Therefore, the default comparer acts as an extra level of indirection. The default comparer checks to see if the type provided in the type parameter implements IComparable<T> and if it does not, it looks to see if it supports IComparable, thus using the first one that it finds. Using this extra level of indirection provides greater flexibility with regards to the contained types. Let’s look at an example to illustrate what I’ve just described:

using System;

using System.Collections.Generic;

public class EntryPoint

{

static void Main() { SortedList<int, string> list1 =

new SortedList<int, string>();

SortedList<int, string> list2 =

new SortedList<int, string>( Comparer<int>.Default );

list1.Add( 1, "one" );

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

281

list1.Add( 2, "two" ); list2.Add( 3, "three" ); list2.Add( 4, "four" );

}

}

I’ve declared two instances of SortedList<TKey, TValue>. In the first instance, I’ve used the parameterless constructor, and in the second instance, I’ve explicitly provided a comparer for integers. In both cases, the result is the same because I provided the default generic comparer in the list2 constructor. I did this mainly so you could see the syntax used to pass in the default generic comparer. You could have just as easily provided any other type in the type parameter list for Comparer as long as it supports either IComparable or IComparable<T>.

Generic System Interfaces

Given the fact that the runtime library provides generic versions of container types, it should be no surprise that it also provides generic versions of commonly used interfaces. This is a great thing for those trying to achieve maximum type safety. For example, your classes and structs may implement

IComparable<T> and/or IComparable as well as IEquatable<T>. Naturally, IComparable<T> is a more type-safe version of IComparable and should be preferred when possible.

Note IEquatable<T> is new to .NET 2.0 and provides a type-safe interface through which you can perform equality comparisons on value types or reference types.

The System.Collections.Generics namespace also defines a whole host of interfaces

that are generic versions of the ones in System.Collections. These include ICollection<T>, IDictionary<TKey, TValue>, and IList<T>. Two of these interfaces deserve special mention: IEnumerator<T> and IEnumerable<T>.2 Late in the game, the development team at Microsoft decided it would be a good idea for IEnumerator<T> to derive from IEnumerator and for IEnumerable<T> to derive from IEnumerable. This decision has proven to be a controversial one. Anders Hejlsberg, one of the developers of the C# language, indicates that IEnumerable<T> inherits from IEnumerable because it can.

His argument goes a little something like this: You could imagine that it would be nice if the container that implements IList<T> also implemented IList. If IList<T> inherits from IList, it would be forced upon the author of the container to implement two versions of the Add method: Add<T>() and Add(). If the end user is able to call the nongeneric Add(), then the whole benefit of added type safety through IList<T> would be lost, since the very existence of Add() opens up the container implementation for runtime cast exceptions. So, deriving IList<T> from IList is a bad idea. IEnumerable<T> and IEnumerator<T>, on the other hand, differ from the other generic interfaces in that the type T is only used in return value positions. Therefore, no type safety is lost when implementing both.

That is the basis of the justification of the statement that IEnumerable<T> can derive from IEnumerable and that IEnumerator<T> can derive from IEnumerator because they can. One of the developers at Microsoft working on the Framework library indicated that IEnumerable<T> and IEnumerator<T> are implemented this way in order to work around the lack of covariance with regard to generics. Yes, it’s dizzying indeed.

2.Chapter 9 covers the facilities provided by IEnumerator<T> and IEnumerable<T> and how you can implement them easily using C# iterators.

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

Coding a type that implements IEnumerable<T> requires a little bit of a trick in that you must implement the IEnumerable method using explicit interface implementation. Moreover, in order to keep the compiler from becoming confused, you may have to fully qualify the IEnumerable with its namespace, as in the following example:

using System;

using System.Collections.Generic;

public class MyContainer<T> : IEnumerable<T>

{

public void Add( T item ) { impl.Add( item );

}

public void Add<R>( MyContainer<R> otherContainer, Converter<R, T> converter ) {

foreach( R item in otherContainer ) { impl.Add( converter(item) );

}

}

public IEnumerator<T> GetEnumerator() { foreach( T item in impl ) {

yield return item;

}

}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator();

}

private List<T> impl = new List<T>();

}

Select Problems and Solutions

In this section, I want to illustrate some examples of creating generic types that show some useful techniques when creating generic code. I assure you that the pathway to learning how to use generics effectively will contain many surprises from time to time, since you must sometimes develop an unnatural or convoluted way of doing something that conceptually is very natural. Many of you will undoubtedly get that unnatural feeling if you’re transitioning from the notion of C++ templates to generics, as you discover the constraints that the dynamic nature of generics places upon you.

Conversion and Operators Within Generic Types

Converting from one type to another or applying operators to parameterized types within generics can prove to be tricky. To illustrate, let’s develop a generic Complex struct that represents a complex number. Suppose that, for some reason, you want to be able to designate what value type is used internally to represent the real and imaginary portions of a complex number. This example is a tad contrived, since you would normally represent the components of an imaginary number using something such as System.Double. However, for the sake of example, let’s imagine that you may want to be able to represent the components using System.Int64. Throughout this discussion, in order to reduce clutter and focus on the issues regarding generics, I’m going to ignore all of the canonical constructs that the generic Complex struct should implement.

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

283

You could start out by defining the Complex number as follows:

using System;

public struct Complex<T> where T: struct

{

public Complex( T real, T imaginary ) { this.real = real;

this.imaginary = imaginary;

}

public T Real {

get { return real; } set { real = value; }

}

public T Img {

get { return imaginary; } set { imaginary = value; }

}

private T real; private T imaginary;

}

public class EntryPoint

{

static void Main() { Complex<Int64> c =

new Complex<Int64>( 4, 5 );

}

}

This is a good start, but now let’s start to make this value type a little more useful. You could benefit from having a Magnitude property that returns the square root of the two components multiplied together. Let’s attempt to create such a property:

using System;

public struct Complex<T> where T: struct

{

public Complex( T real, T imaginary ) { this.real = real;

this.imaginary = imaginary;

}

public T Real {

get { return real; } set { real = value; }

}

public T Img {

get { return imaginary; } set { imaginary = value; }

}

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