Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 59 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
59
Dung lượng
1,3 MB
Nội dung
CHAPTER 11 ■ GENERICS 325 Constructed Types Control Accessibility When you build constructed types from generic types, you must consider the accessibility of both the generic type and the types provided as the type arguments, in order to determine the accessibility of the whole constructed type. For example, the following code is invalid and will not compile: public class Outer { private class Nested { } public class GenericNested<T> { } private GenericNested<Nested> field1; public GenericNested<Nested> field2; // Ooops! } The problem is with field2. The Nested type is private, so how can GenericNested<Nested> possibly be public? Of course, the answer is that it cannot. With constructed types, the accessibility is an intersection of the accessibility of the generic type and the types provided in the argument list. Generics and Inheritance C# generic types cannot directly derive from a type parameter. However, you can use the following type parameters to construct the base types they do derive from: // This is invalid!! public class MyClass<T> : T { } // But this is valid. public class MyClass<T> : Stack<T> { } ■ Tip With C++ templates, deriving directly from a type parameter provides a special flexibility. If you’ve ever used the Active Template Library (ATL) to do COM development, you have no doubt come across this technique because ATL employs it extensively to avoid the need for virtual method calls. The same technique is used with C++ templates to generate entire hierarchies at compile time. For more examples, I suggest you read Andrei Alexandrescu’s Modern C++ Design: Generic Programming and Design Patterns Applied (Boston, MA: Addison- CHAPTER 11 ■ GENERICS 326 Wesley Professional, 2001). This is yet another example showing how C++ templates are static in nature, whereas C# generics are dynamic. Let’s examine techniques that you can use to emulate the same behavior to some degree. As is often the case, you can add one more level of indirection to achieve something similar. In C++, when a template type derives directly from one of the type arguments, it is often assumed that the type specified for the type argument exhibits a certain desired behavior. For example, you can do the following using C++ templates: // NOTE: This is C++ code used for the sake of example class Employee { public: long get_salary() { return salary; } 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> 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 policy-based 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. CHAPTER 11 ■ GENERICS 327 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 you’ll often need to create generic types that not only contain instances of various types but also use those objects directly by calling methods or accessing properties on them. For example, suppose that 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—in which 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 might 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 ) { 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; } CHAPTER 11 ■ GENERICS 328 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) ); 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' All 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 desired effect without some extra information. Whenever you hear the word contract within the C# world, you might start thinking about interfaces. Therefore, I chose 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 runtime. A naïve attempt to solve the problem could look like the following: public class Shapes<T> { CHAPTER 11 ■ GENERICS 329 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 that 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 attempted to cast an int into an IShape. One of the primary benefits of using generics is better type safety, but in this example I 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: 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 runtime. The CHAPTER 11 ■ GENERICS 330 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. There can be one where clause for each type parameter. Any number of constraints can be listed following the type parameter in the where clause. However, only one constraint can name a class type (because 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 can list the special words class or struct, which are used to indicate that the type parameter must be any class or any struct. The constraint clause can then include as many secondary constraints as possible, such as a list of interfaces that the parameterized type must implement. Finally, you can list a constructor constraint that takes the form new() at the end of the constraint list. This constrains the parameterized type so it is required to have a default parameterless constructor. Class types must have an explicitly defined default constructor to satisfy this constraint, whereas value types 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>(); } 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 this code, you can see an example of the struct constraint in the declaration for a container that can contain only value types. The constraint prevents one from declaring the objList variable that I have commented out in this example because the result of attempting to compile it presents the following error: CHAPTER 11 ■ GENERICS 331 error CS0453: The type 'object' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'MyValueList<T>' Alternatively, the constraint could have also claimed to allow only class types. Incidentally, in the Visual Studio version of the C# compiler, I can’t create a constraint that includes both class and struct. Of course, doing so is pointless because 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, in which 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 declared MyDictionary<TKey, TValue> so 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. 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 few if any 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. CHAPTER 11 ■ GENERICS 332 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. Using constraint clauses with method and delegate declarations 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() { var op = new Operation<int, float, double>( EntryPoint.Add ); Console.WriteLine( "{0} + {1} = {2}", 1, 3.2, op(1, 3.2f) ); } } I 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. Similarly, for generic methods, the constraints clauses follow the method declaration but precede the method body. Co- and Contravariance Variance is all about convertibility and being able to do what makes type-sense. For example, consider the following code, which demonstrates array covariance that has been possible in C# since the 1.0 days: using System; static class EntryPoint { static void Main() { string[] strings = new string[] { "One", CHAPTER 11 ■ GENERICS 333 "Two", "Three" }; DisplayStrings( strings ); // Array covariance rules allow the following // assignment object[] objects = strings; // But what happens now? objects[1] = new object(); DisplayStrings( strings ); } static void DisplayStrings( string[] strings ) { Console.WriteLine( " Printing strings " ); foreach( var s in strings ) { Console.WriteLine( s ); } } } At the beginning of the Main method, I create an array of strings and then immediately pass it to DisplayStrings to print them to the console. Then, I assign a variable of type objects[] from the variable strings. After all, because strings and objects are reference type variables, at first glance it makes logical sense to be able to assign strings to objects because a string is implicitly convertible to an object. However, notice right after doing so, I modify slot one and replace it with an object instance. What happens when I call DisplayStrings the second time passing the strings array? As you might expect, the runtime throws an exception of type ArrayTypeMismatchException shown as follows: Unhandled Exception: System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array. Array covariance in C# has been in the language since the beginning for Java compatibility. But because it is flawed, and some say broken, then how can we fix this problem? There are a few ways indeed. Those of you familiar with functional programming will naturally suggest invariance as the solution. That is, if an array is invariant similar to System.String, a copy is made typically in a lazy fashion at the point where one is assigned into another variable. However, let’s see how we might fix this problem using generics: using System; using System.Collections.Generic; static class EntryPoint { static void Main() { List<string> strings = new List<string> { "One", "Two", "Three" CHAPTER 11 ■ GENERICS 334 }; // THIS WILL NOT COMPILE!!! List<object> objects = strings; } } The spirit of the preceding code is identical to the array covariance example, but it will not compile. If you attempt to compile this, you will get the following compiler error: error CS0029: Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>' The ultimate problem is that each constructed type is an individual type, and even though they might originate from the same generic type, they have no implicit type relation between them. For example, there is no implicit relationship between List<string> and List<object>, and just because they both are constructed types of List<T> and string is implicitly convertible to object does not imply that they are convertible from one to the other. Don’t lose hope, though. There is a syntax added in C# 4.0 that allows you to achieve the desired result. Using this new syntax, you can notate a generic interface or delegate indicating whether it supports covariance or contravariance. Additionally, the new variance rules apply only to constructed types in which reference types are passed for the type arguments to the generic type. Covariance Within strongly typed programming languages such as C#, an operation is covariant if it reflects and preserves the ordering of types so they are ordered from more specific types to more generic types. To illustrate, I’ll borrow from the example in the previous section to show how array assignment rules in C# are covariant: string s = "Hello"; object o = s; string[] strings = new string[3]; object[] objects = strings; The first two lines make perfect sense; after all, variables of type string are implicitly convertible to type object because string derives from object. The second set of lines shows that variables of type string[] are implicitly convertible to variables of type object[]. And because the ordering of types between the two implicit assignments is identical that is, from a more specialized type (string) to a more generic type (object) the array assignment operation is said to be covariant. Now, to translate this concept to generic interface assignment, an interface of type IOperation<T> is covariance-convertible to IOperation<R> if there exists an implicit reference conversion from T to R and IOperation<T> to IOperation<R>. Simply put, if for the two conversion operations just mentioned, T and R [...]... { Complex c = new Complex ( 3, 4, EntryPoint.MultiplyInt 64, EntryPoint.AddInt 64, EntryPoint.DoubleToInt 64 ); Console.WriteLine( "Magnitude is {0}", c.Magnitude ); } static Int 64 MultiplyInt 64( Int 64 val1, Int 64 val2 ) { return val1 * val2; } static Int 64 AddInt 64( Int 64 val1, Int 64 val2 ) { return val1 + val2; } static Int 64 DoubleToInt 64( double d ) { return Convert.ToInt 64( d ); } } 350... Main() { Complex c = new Complex ( 3, 4, EntryPoint.MultiplyInt 64, EntryPoint.AddInt 64, EntryPoint.DoubleToInt 64 ); Console.WriteLine( "Magnitude is {0}", c.Magnitude ); } static Int 64 MultiplyInt 64( Int 64 val1, Int 64 val2 ) { return val1 * val2; } static Int 64 AddInt 64( Int 64 val1, Int 64 val2 ) { return val1 + val2; 352 CHAPTER 11 ■ GENERICS } static Int 64 DoubleToInt 64( double d ) { return... Complex c = new Complex ( 3, 4, EntryPoint.MultiplyInt 64, EntryPoint.AddInt 64, EntryPoint.DoubleToInt 64 ); Console.WriteLine( "Magnitude is {0}", c.Magnitude ); } static void DummyMethod( Complex c ) { } static Int 64 AddInt 64( Int 64 val1, Int 64 val2 ) { return val1 + val2; } static Int 64 MultiplyInt 64( Int 64 val1, Int 64 val2 ) { return val1 * val2; } static Int 64 DoubleToInt 64( double... Main() { Complex c = new Complex ( 3, 4, EntryPoint.MultiplyInt 64, EntryPoint.AddInt 64, EntryPoint.Int64ToDouble, EntryPoint.DoubleToInt 64 ); Console.WriteLine( "Magnitude is {0}", c.Magnitude ); } static void DummyMethod( Complex c ) { } static Int 64 MultiplyInt 64( Int 64 val1, Int 64 val2 ) { return val1 * val2; } static Int 64 AddInt 64( Int 64 val1, Int 64 val2 ) { return val1... val1, Int 64 val2 ) { return val1 + val2; 356 CHAPTER 11 ■ GENERICS } static Int 64 DoubleToInt 64( double d ) { return Convert.ToInt 64( d ); } static double Int64ToDouble( Int 64 i ) { return Convert.ToDouble( i ); } } Now, the Complex type can contain any kind of struct, whether it’s generic or not However, you must provide it with the necessary means to be able to convert to and from double as well as... value, and the Func delegates can be used to hold methods that accept up to 16 parameters and do return a value ■ Note Prior to the NET 4. 0 BCL, the Action and Func delegates only accepted up to four parameters Currently, they support up to 16 Starting with NET 4. 0, these generic delegates have also been marked appropriately for variance Thus, the two parameter versions of these will look like... 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 with 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 you get... imaginary ) { this.real = real; this.imaginary = imaginary; 347 CHAPTER 11 ■ GENERICS } 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 c = new Complex ( 4, 5 ); } } This is a good start, but now let’s make this value... imaginary = value; } } public T Magnitude { get { // WON'T COMPILE!!! return Math.Sqrt( real * real + imaginary * imaginary ); 348 CHAPTER 11 ■ GENERICS } } private T real; private T imaginary; } public class EntryPoint { static void Main() { Complex c = new Complex ( 3, 4 ); Console.WriteLine( "Magnitude is {0}", c.Magnitude ); } } If you attempt to compile this code, you might be surprised... associated with array invariance in C# is avoided by using generics coupled with the variance syntax added to the language in C# 4. 0 In other words, the variance rules for generics are type safe whereas the variance rules for plain old arrays are not Variance and Delegates In general, generic delegates follow the same rules as generic interfaces when applying variance decorations to generic parameters The NET . ■ Note Prior to the .NET 4. 0 BCL, the Action<> and Func<> delegates only accepted up to four parameters. Currently, they support up to 16. Starting with .NET 4. 0, these generic delegates. IEnumerable<T> and IEnumerator<T> types are denoted as covariant with the out keyword starting with the release of C# 4. 0. This is a tremendous help, especially when using LINQ. Contravariance. Therefore, much of the pain associated with array invariance in C# is avoided by using generics coupled with the variance syntax added to the language in C# 4. 0. In other words, the variance rules