Apress - Accelerated C#_5 docx

59 262 0
Apress - Accelerated C#_5 docx

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

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 [...]... action2 = SomeFunction; This type of variance-assignment (contravariance in this case) from method group to delegate instance has been supported in C# for quite some time So, if the preceding is possible, it makes sense to be able to do the following, which one can do starting in C# 4.0: Action action2 = action1; Now, let’s see a short example of contravariance-assignment with Action at work using... Action petAnimal = (Animal a) => { Console.Write( "Petting animal and response is: " ); a.ShowAffection(); 341 CHAPTER 11 ■ GENERICS }; // Contravariance rule in action! // // Since Dog -> Animal and // Action -> Action // then the following assignment is contravariant Action petDog = petAnimal; petDog( new Dog() ); } } In the Main method, I have created an instance of Action... in Chapter 15, you’ll see that it’s even easier using lambda expressions) Functions that accept functions as parameters are often called higher-level functions or functionals So what sort of variance is involved when assigning compatible instances of higher-order functions to each other? Let’s investigate by introducing a new delegate definition that looks like the following: delegate void Task(... static void Main() { Action petAnimal = (Animal a) => { Console.Write( "Petting animal and response is: " ); a.ShowAffection(); }; // Contravariance rule in action! // // Since Dog -> Animal and // Action -> Action // then the following assignment is contravariant Action petDog = petAnimal; petDog( new Dog() ); Task doStuffToADog = BuildTask(); doStuffToADog( petDog );... accept an action to an animal doStuffToADog( petAnimal ); // Therefore, it is logical for Task to be implicitly // convertible to Task // // Covariance in action! // // Since Dog -> Animal and // Task -> Task // then the following assignment is covariant Task doStuffToAnAnimal = doStuffToADog; doStuffToAnAnimal( petAnimal ); doStuffToADog( petAnimal ); } static Task... time to time with generics because they lack the static compile-time capabilities of templates However, I think you’ll agree that the dynamic capabilities of generics make up for that in the end Imagine how handy it is to be able to create closed types from generics at runtime For example, creating a parsing engine for some sort of XML-based language that defines new types from generics is a snap Let’s... efficient code when using value types with containers but they also give the compiler much more power when enforcing type safety As a rule, you should always prefer compile-time type safety over runtime type safety You can fix a compile-time failure before software is deployed, but a runtime failure usually results in an InvalidCastException thrown in a production environment Such a runtime failure could... enforce type safety, so it can do what it’s meant to do best—and that’s to be your friend The next chapter tackles the topic of threading in C# and the NET runtime Along with threading comes the ever-so-important topic of synchronization 359 CHAPTER 11 ■ GENERICS 360 ... covariant in nature T  R IOperation  IOperation ■ Note C# variance rules do not apply to value types; that is, types that are not reference convertible In other words, IOperation is not covariance-convertible to IOperation, even though int is implicitly convertible to double Let’s consider an example of a custom collection called MyCollection that implements the interface IMyCollection:... get away with passing petAnimal in the second invocation of doStuffToADog Now let’s follow the same thought pattern as the previous example, in which you discovered that Action is contravariance-assignable to an Action In Main, I create an instance of Task and assign it to the doStuffToAnAnimal variable When I invoke doStuffToAnAnimal, I can certainly pass an instance of Action . been easy in C# using delegates (and in Chapter 15, you’ll see that it’s even easier using lambda expressions). Functions that accept functions as parameters are often called higher-level functions. 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#. 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,

Ngày đăng: 19/06/2014, 22:20

Mục lục

  • Home Page

  • Prelim

  • Contents at a Glance

  • Contents

  • About the Author

  • About the Technical Reviewer

  • Acknowledgments

  • Preface

    • About This Book

    • C# Preview

      • Differences Between C# and C++

        • C#

        • C++

        • CLR Garbage Collection

        • Example of a C# Program

        • Overview of Features Added in C# 2.0

        • Overview of Features Added in C# 3.0

        • Overview of New C# 4.0 Features

        • Summary

        • C# and the CLR

          • The JIT Compiler in the CLR

          • Assemblies and the Assembly Loader

            • Minimizing the Working Set of the Application

            • Naming Assemblies

            • Loading Assemblies

Tài liệu cùng người dùng

Tài liệu liên quan