Accelerate C in FPGA_5

Chia sẻ: Up Upload | Ngày: | Loại File: PDF | Số trang:59

0
21
lượt xem
3
download

Accelerate C in FPGA_5

Mô tả tài liệu
  Download Vui lòng tải xuống để xem tài liệu đầy đủ

Tham khảo tài liệu 'accelerate c in fpga_5', công nghệ thông tin, kỹ thuật lập trình phục vụ nhu cầu học tập, nghiên cứu và làm việc hiệu quả

Chủ đề:
Lưu

Nội dung Text: Accelerate C in FPGA_5

  1. CHAPTER 11 ■ GENERICS 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 { } private GenericNested field1; public GenericNested field2; // Ooops! } The problem is with field2. The Nested type is private, so how can GenericNested 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 { } // But this is valid. public class MyClass : Stack { } ■ 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- 325
  2. CHAPTER 11 ■ GENERICS 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 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 inherits the implementation of whatever type is specified for T at compile time. In this case, that type, Employee, implements get_salary, and MyClass inherits that implementation. Clearly, an assumption is being placed on the type that is provided for T in MyClass 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. 326
  3. CHAPTER 11 ■ GENERICS 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; } 327
  4. CHAPTER 11 ■ GENERICS public class Shapes { 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 shapes = new List(); } public class EntryPoint { static void Main() { Shapes shapes = new Shapes(); 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. 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 { 328
  5. CHAPTER 11 ■ GENERICS 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 shapes = new List(); } This modification to Shapes 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. The compiler would happily oblige. But what would happen if you tried to get the TotalArea property from a Shapes 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 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 shapes = new List(); } Notice the extra line under the first line of the class declaration using the where keyword. This says, “Define class Shapes 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 329
  6. CHAPTER 11 ■ GENERICS 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 where T: struct // But can't do the following // where T: struct, new() { public void Add( T v ) { imp.Add( v ); } private List imp = new List(); } public class EntryPoint { static void Main() { MyValueList intList = new MyValueList(); intList.Add( 123 ); // CAN'T DO THIS. // MyValueList objList = // new MyValueList(); } } 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: 330
  7. CHAPTER 11 ■ GENERICS 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' 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 where TKey: struct, IComparable where TValue: IValue, new() { public void Add( TKey key, TValue val ) { imp.Add( key, val ); } private Dictionary imp = new Dictionary(); } I declared MyDictionary 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. 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. 331
  8. CHAPTER 11 ■ GENERICS 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 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( 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", 332
  9. CHAPTER 11 ■ GENERICS "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 strings = new List { "One", "Two", "Three" 333
  10. CHAPTER 11 ■ GENERICS }; // THIS WILL NOT COMPILE!!! List 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' to  'System.Collections.Generic.List' 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 and List, and just because they both are constructed types of List 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 is covariance-convertible to IOperation if there exists an implicit reference conversion from T to R and IOperation to IOperation. Simply put, if for the two conversion operations just mentioned, T and R 334
  11. CHAPTER 11 ■ GENERICS are on the same sides of the conversion operations, the conversion operation is covariant. For example, let the arrow shown following represent the operation. And because T and R appear on the same sides of the operation in both cases, the operation is 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: using System; using System.Collections.Generic; interface IMyCollection { void AddItem( T item ); T GetItem( int index ); } class MyCollection : IMyCollection { public void AddItem( T item ) { collection.Add( item ); } public T GetItem( int index ) { return collection[index]; } private List collection = new List(); } static class EntryPoint { static void Main() { var strings = new MyCollection(); strings.AddItem( "One" ); strings.AddItem( "Two" ); IMyCollection collStrings = strings; PrintCollection( collStrings, 2 ); } static void PrintCollection( IMyCollection coll, 335
  12. CHAPTER 11 ■ GENERICS int count ) { for( int i = 0; i < count; ++i ) { Console.WriteLine( coll.GetItem(i) ); } } } Of course, the collection MyCollection is extremely contrived and we would never author a real collection type like this, but I have written it this way to keep the example brief and to focus on covariance. The preceding code compiles and runs just fine while printing out the two strings in the MyCollection instance to the console. But now, let’s imagine that we want PrintCollection to accept an instance of type IMyCollection rather than IMyCollection. After all, it is logical that a collection of strings is a collection of objects as well. If you simply just change the signature of PrintCollection to accept IMyCollection, you will get a compiler error at the point of invocation. That’s because what is logical to you and me is not necessarily logical to the compiler because, by default, constructed generic types are invariant and there is no implicit conversion from one to the other. Something else is needed. Check out the following modification that compiles and works as expected. I have bolded the differences to pay attention to: using System; using System.Collections.Generic; interface IMyCollection { void AddItem( T item ); } interface IMyEnumerator { T GetItem( int index ); } class MyCollection : IMyCollection, IMyEnumerator { public void AddItem( T item ) { collection.Add( item ); } public T GetItem( int index ) { return collection[index]; } private List collection = new List(); } static class EntryPoint { static void Main() { var strings = new MyCollection(); strings.AddItem( "One" ); strings.AddItem( "Two" ); 336
  13. CHAPTER 11 ■ GENERICS IMyEnumerator collStrings = strings; // Covariance in action! IMyEnumerator collObjects = collStrings; PrintCollection( collObjects, 2 ); } static void PrintCollection( IMyEnumerator coll, int count ) { for( int i = 0; i < count; ++i ) { Console.WriteLine( coll.GetItem(i) ); } } } First, notice that I split the previous implementation of IMyCollection into two interfaces named IMyCollection and IMyEnumerator. I’ll explain why in a moment. Also, notice that PrintCollection accepts a variable of type IMyEnumerator rather than IMyCollection. But most importantly, look very closely at the IMyEnumerator declaration and pay attention to the way the generic parameter is decorated with the out keyword. The out keyword in the generic parameter list is how you denote that a generic interface is covariant in T. In other words, it’s how you tell the compiler that if R is implicitly convertible to S, IMyEnumerator is implicitly convertible to IMyEnumerator. Why is the keyword named out? Because it just so happens that generic interfaces that are covariant in T typically have T in an output position of the methods within. Now you can see why I had to split the original IMyCollection interface into two interfaces because the IMyCollection.AddItem method does not have T in the output position. ■ Note The keywords in and out were likely chosen by the compiler team because, as shown previously, covariant interfaces have the variant type in the output position and vice versa for contravariance. However, I will show in a later section that this oversimplified view becomes rather confusing when higher-order functions (or functionals) via delegates are involved. The venerable IEnumerable and IEnumerator 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 As you might expect, contravariance is the opposite of covariance. That is, for generic interface assignment, an interface of type IOperation is contravariance-convertible to IOperation if there exists an implicit reference conversion from R to T and IOperation to IOperation. Simply put, if T and R are on opposite sides of the conversion operation for both conversions, the conversion operation is contravariant. For example, let the following arrow represent the operation. And because T and R appear on opposite sides of the operation in both cases, the operation is contravariant in nature. RT 337
  14. CHAPTER 11 ■ GENERICS IOperation  IOperation Contravariant generic parameters in generic interfaces and delegates are notated using the new in generic parameter decoration. To illustrate, let’s revisit the contrived MyCollection class in the previous section and imagine that we want the ability to remove items from the collection (the areas of interest are in bold): using System; using System.Collections.Generic; class A { } class B : A { } interface IMyCollection { void AddItem( T item ); } interface IMyTrimmableCollection { void RemoveItem( T item ); } class MyCollection : IMyCollection, IMyTrimmableCollection { public void AddItem( T item ) { collection.Add( item ); } public void RemoveItem( T item ) { collection.Remove( item ); } private List collection = new List(); } static class EntryPoint { static void Main() { var items = new MyCollection(); items.AddItem( new A() ); B b = new B(); items.AddItem( b ); IMyTrimmableCollection collItems = items; // Contravariance in action! IMyTrimmableCollection trimColl = collItems; trimColl.RemoveItem( b ); } 338
  15. CHAPTER 11 ■ GENERICS } I have trimmed some of the code from the covariance example in order to focus squarely on the contravariance case. Notice the use of the in keyword in the declaration for the IMyTrimmableCollection interface. This tells the compiler that with respect to the desired operation in this example (trimming in this case), there exists an implicit contravariance-conversion from IMyTrimmableCollection to IMyTrimmableCollection because there is an implicit conversion from B to A. At first glance, the conversion and the assignment of collItems into the trimColl might feel foreign. But if for MyCollection I can invoke RemoveItem passing an A instance, I should be able to invoke RemoveItem passing a B instance because B is an A based on the inheritance rules. Up to this point, I have shown examples of both covariance and contravariance using modifications to the same contrived collection class. You have seen how enumeration on the collection is covariant and how removal from the collection is contravariant. What about addition to the collection? Which flavor of variance is it? We already have the IMyCollection interface, which is repeated here for convenience: interface IMyCollection { void AddItem( T item ); } If you have an IMyCollection reference, you should be able to add instances of B if B derives from A. So calling AddItem on IMyCollection passing a B instance should be equivalent to calling IMyCollection passing a B instance. Therefore, the operation of adding an instance to the collection is contravariant based on the definition. That is, if B is convertible to A and IMyCollection is convertible to IMyCollection, the operation is contravariant. Now that you have discovered that the operation of adding an item to the collection is contravariant, you should decorate our interface accordingly: interface IMyCollection { void AddItem( T item ); } Invariance A generic interface or delegate type in which the generic parameters are not decorated at all is invariant. Naturally, all such interfaces and delegates were invariant prior to C# 4.0 because the in and out decorations to generic parameters did not exist before then. Remember from an earlier section, the contrived IMyCollection interface looked like the following: interface IMyCollection { void AddItem( T item ); T GetItem( int index ); } If we must keep these two methods in the same interface, we have no choice but to leave the interface as invariant. If the compiler were to allow us to decorate the generic parameter T with the out keyword, then we would be in the same broken boat that the array covariance is in. That is, we would be allowed to compile code that would appear to allow us to add instances of incompatible types to a 339
  16. CHAPTER 11 ■ GENERICS collection. Why is that? Well, let’s imagine for a moment that we could mark the preceding interface as covariant: // This won't work! interface IMyCollection { void AddItem( T item ); T GetItem( int index ); } Then, based on the definition of covariance, a variable of type IMyCollection would be assignable to a variable of type IMyCollection. And then, through the latter variable, we would be able to do something like the following: // Nothing but pure evil! MyCollection strings = …; IMyCollection objects = strings; objects.AddItem( new MonkeyWrench() ); 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 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 Base Class Library (BCL) contains handy generic delegate types such as Action and Func, which are applicable in many instances saving you from having to define your own custom delegate types. The Action delegates can be used to hold methods that accept up to 16 parameters and have no return 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 the following: public delegate void Action< in T1, in T2 >( T1 arg1, T2 arg2 ); public delegate TResult Func< in T1, in T2, out TResult>( T1 arg1, T2 arg2 ); Now for an example of delegate variance, let’s consider a type hierarchy: class Animal { } 340
  17. CHAPTER 11 ■ GENERICS class Dog : Animal { } Suppose that you had a couple of methods like the following defined in some class: static void SomeFuntion( Animal animal ); static void AnotherFunction( Dog dog ); Then because the function signature matches the delegate signature, it makes sense that you could assign SomeFunction to an instance of Action like the following: Action action1 = SomeFunction; When one invokes action1, one can pass a Dog or an Animal because Dog is implicitly convertible to Animal. Let’s suppose that you later create an Action instance such as the following: Action action2 = AnotherFunction; When one invokes action2, one can pass a Dog instance. But also notice that because one can also pass a Dog instance to SomeFunction, it would have been possible to create action2 as shown here: Action 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 the same object hierarchy shown in the previous example: using System; class Animal { public virtual void ShowAffection() { Console.WriteLine( "Response unknown" ); } } class Dog : Animal { public override void ShowAffection() { Console.WriteLine( "Wag Tail..." ); } } static class EntryPoint { static void Main() { Action petAnimal = (Animal a) => { Console.Write( "Petting animal and response is: " ); a.ShowAffection(); 341
  18. 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 that holds a reference to a function that accepts an Animal instance and calls the ShowAffection method on the instance. ■ Note I use the lambda syntax to assign a function to the Action instance for brevity. If you are unfamiliar with this syntax and you are itching to learn more, you can jump to Chapter 15 soon to read all about it. The next line of code in Main is where the fun begins. This is where I assign the instance of Action into a reference to Action. And because Dog is implicitly convertible to Animal, yet Action is implicitly convertible to Action, the assignment is contravariant. If at this point you are struggling to get your head wrapped around how Action is implicitly convertible to Action when Animal is not implicitly convertible to Dog, try to keep in mind that the action is the focal point. If an action can operate on Animal instances, it can certainly operate on Dog instances. But now let’s kick it up a notch! In functional programming disciplines, it is common to pass actual functions as parameters to other functions. This has always 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 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( Action action ); Here we have defined a delegate, Task, which will reference a function that accepts another delegate of type Action. ■ Note Please don’t confuse the Task type in this example with the Task type in the Task Parallel Library (TPL). If we were to mark this delegate as variant, would we notate the type parameter with in or out? Let’s investigate by looking at the following example: static class EntryPoint { 342
  19. CHAPTER 11 ■ GENERICS 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 ); // But a task that accepts an action to a dog can also // 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 BuildTask() where T : new() { return (Action action) => action( new T() ); } } First, notice that I created a BuildTask generic helper method to make my code a little more readable. In Main, I create an instance of Task and assign it to the doStuffToADog variable. doStuffToADog holds a reference to a delegate that accepts an Action instance as a parameter. I then invoke doStuffToADog passing petDog, which is an instance of Action. But in the previous example we discovered that Action is implicitly convertible to Action, so that’s how I can 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. But because Action can also be passed to Task at invocation time, it implies that an instance of Task can be assigned to an instance of 343
  20. CHAPTER 11 ■ GENERICS Task. Indeed, that is what I am demonstrating in this example. But is it contravariance or covariance? At first glance, because T is used on the right side in the declaration of the Task delegate, one might be inclined to say that we must decorate the type parameter T with the in keyword. However, let’s analyze the situation. Because Dog is implicitly convertible to Animal, and Task is implicitly convertible to Task, the assignment is covariant because the direction of conversion with respect to T is the same direction in both operations. Therefore, the type parameter must be decorated with the out keyword, thus making the declaration for Task look like the following: delegate void Task( Action action ); The point to understand here is that you cannot choose the in or out keyword based solely on which side of the delegate declaration the generic parameter is used. You must analyze the conversion to determine whether it is covariant or contravariant, and then make your choice accordingly. Of course, if you choose the wrong one, the compiler will certainly let you know about it. 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 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 added type safety, you’re guaranteed to reduce runtime type conversion exceptions because the compiler can catch many of them at compile time. I encourage you to look at the .NET Framework documentation for the System.Collections.Generic namespace. There you will find all the generic collection classes made available by the Framework. Included in the namespace are Dictionary, LinkedList, List, Queue, SortedDictionary, SortedList, HashSet, and Stack. Based on their names, the uses of these types should feel familiar compared to the nongeneric classes under System.Collections. Although the containers within the System.Collections.Generic namespace might not seem complete for your needs, you have the possibility 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 why 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, follows the same sort of pattern. When you create a SortedList, you have the option of providing an object that implements the IComparer interface so it can compare two keys. If you don’t provide one, the SortedList defaults to using what’s called the generic comparer. The generic comparer is simply an object that derives from the abstract Comparer class and can be obtained through the static property Comparer.Default. Based upon the nongeneric SortedList, you might think that if the 344
Đồng bộ tài khoản