Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 64 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
64
Dung lượng
7,29 MB
Nội dung
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 CHAPTER 11 ■ GENERICS 335 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<T> IOperation<R> ■ Note C# variance rules do not apply to value types; that is, types that are not reference convertible. In other words, IOperation<int> is not covariance-convertible to IOperation<double>, even though int is implicitly convertible to double. Let’s consider an example of a custom collection called MyCollection<T> that implements the interface IMyCollection<T>: using System; using System.Collections.Generic; interface IMyCollection<T> { void AddItem( T item ); T GetItem( int index ); } class MyCollection<T> : IMyCollection<T> { public void AddItem( T item ) { collection.Add( item ); } public T GetItem( int index ) { return collection[index]; } private List<T> collection = new List<T>(); } static class EntryPoint { static void Main() { var strings = new MyCollection<string>(); strings.AddItem( "One" ); strings.AddItem( "Two" ); IMyCollection<string> collStrings = strings; PrintCollection( collStrings, 2 ); } static void PrintCollection( IMyCollection<string> coll, CHAPTER 11 ■ GENERICS 336 int count ) { for( int i = 0; i < count; ++i ) { Console.WriteLine( coll.GetItem(i) ); } } } Of course, the collection MyCollection<T> 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<string> instance to the console. But now, let’s imagine that we want PrintCollection to accept an instance of type IMyCollection<object> rather than IMyCollection<string>. 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<object>, 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<T> { void AddItem( T item ); } interface IMyEnumerator<out T> { T GetItem( int index ); } class MyCollection<T> : IMyCollection<T>, IMyEnumerator<T> { public void AddItem( T item ) { collection.Add( item ); } public T GetItem( int index ) { return collection[index]; } private List<T> collection = new List<T>(); } static class EntryPoint { static void Main() { var strings = new MyCollection<string>(); strings.AddItem( "One" ); strings.AddItem( "Two" ); CHAPTER 11 ■ GENERICS 337 IMyEnumerator<string> collStrings = strings; // Covariance in action! IMyEnumerator<object> collObjects = collStrings; PrintCollection( collObjects, 2 ); } static void PrintCollection( IMyEnumerator<object> 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<object> rather than IMyCollection<string>. But most importantly, look very closely at the IMyEnumerator<T> 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<R> is implicitly convertible to IMyEnumerator<S>. 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<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 As you might expect, contravariance is the opposite of covariance. That is, for generic interface assignment, an interface of type IOperation<T> is contravariance-convertible to IOperation<R> if there exists an implicit reference conversion from R to T and IOperation<T> to IOperation<R>. 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 CHAPTER 11 ■ GENERICS 338 IOperation<T> IOperation<R> 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<T> 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<T> { void AddItem( T item ); } interface IMyTrimmableCollection<in T> { void RemoveItem( T item ); } class MyCollection<T> : IMyCollection<T>, IMyTrimmableCollection<T> { public void AddItem( T item ) { collection.Add( item ); } public void RemoveItem( T item ) { collection.Remove( item ); } private List<T> collection = new List<T>(); } static class EntryPoint { static void Main() { var items = new MyCollection<A>(); items.AddItem( new A() ); B b = new B(); items.AddItem( b ); IMyTrimmableCollection<A> collItems = items; // Contravariance in action! IMyTrimmableCollection<B> trimColl = collItems; trimColl.RemoveItem( b ); } [...]... 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,... 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... 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 C H A P T E R 12 ■■■ Threading in C# The mere mention of multithreading can strike fear in the hearts of some programmers For others, it fires them up for... because we all know what impatient users tend to do 361 CHAPTER 12 ■ THREADING IN C# when desktop applications become unresponsive: They kill them! But it’s important to realize that there is much more to the threading puzzle than creating an extra thread to run some random code That task is actually quite easy in the C# environment, so let’s take a look and see how easy it really is Starting Threads... 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... 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... would be a good idea for IEnumerator to derive from IEnumerator and for IEnumerable to derive from IEnumerable This decision has proven to be a controversial one Anders Hejlsberg, the father of the C# language, indicates that IEnumerable inherits from IEnumerable because it can His argument goes something like this: you can imagine that it would be nice if the container that implements IList... indicated that IEnumerable and IEnumerator are implemented this way in order to work around the lack of covariance with regard to generics Yes, it’s dizzying indeed However, that point is moot because C# 4.0 introduced syntax that allows one to implement covariant generic interfaces Coding a type that implements IEnumerable requires a bit of a trick in that you must implement the IEnumerable method... { foreach( R item in otherContainer ) { impl.Add( converter(item) ); } } 2 Chapter 9 covers the facilities provided by IEnumerator and IEnumerable and how you can implement them easily by using C# iterators 346 CHAPTER 11 ■ GENERICS public IEnumerator GetEnumerator() { foreach( T item in impl ) { yield return item; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()... could start out by defining the Complex number as follows: using System; public struct Complex where T: struct { public Complex( T real, T imaginary ) { this.real = real; this.imaginary = imaginary; 3 47 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 . 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. 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<Dog> action2. and IEnumerable<T> and how you can implement them easily by using C# iterators. CHAPTER 11 ■ GENERICS 3 47 public IEnumerator<T> GetEnumerator() { foreach( T item in