Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 50 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
50
Dung lượng
461,14 KB
Nội dung
370 Part III Creating Components 9. Press the Enter key to return to Visual Studio 2010. 10. Add the following statements shown in bold type to the end of the Main method in the Program class, after the existing code: static void Main(string[] args) { Tree<string> tree2 = new Tree<string>("Hello"); tree2.Insert("World"); tree2.Insert("How"); tree2.Insert("Are"); tree2.Insert("You"); tree2.Insert("Today"); tree2.Insert("I"); tree2.Insert("Hope"); tree2.Insert("You"); tree2.Insert("Are"); tree2.Insert("Feeling"); tree2.Insert("Well"); tree2.Insert("!"); tree2.WalkTree(); } These statements create another binary tree for holding strings, populate it with some test data, and then print the tree. This time, the data is sorted alphabetically. 11. On the Build menu, click Build Solution. Verify that the solution compiles, and correct any errors if necessary. 12. On the Debug menu, click Start Without Debugging. The program runs and displays the integer values as before, followed by the strings in the following sequence: !, Are, Are, Feeling, Hello, Hope, How, I, Today, Well, World, You, You 13. Press the Enter key to return to Visual Studio 2010. Creating a Generic Method As well as defining generic classes, you can also use the .NET Framework to create generic methods. With a generic method, you can specify parameters and the return type by using a type parameter in a manner similar to that used when defining a generic class. In this way, you can define generalized methods that are type-safe and avoid the overhead of casting (and boxing in some cases). Generic methods are frequently used in conjunction with generic classes—you need them for methods that take a generic class as a parameter or that have a return type that is a generic class. Chapter 18 Introducing Generics 371 You define generic methods by using the same type parameter syntax that you use when creating generic classes. (You can also specify constraints.) For example, you can call the following generic Swap<T> method to swap the values in its parameters. Because this func- tionality is useful regardless of the type of data being swapped, it is helpful to define it as a generic method: static void Swap<T>(ref T first, ref T second) { T temp = first; first = second; second = temp; } You invoke the method by specifying the appropriate type for its type parameter. The following examples show how to invoke the Swap<T> method to swap over two ints and two strings: int a = 1, b = 2; Swap<int>(ref a, ref b); string s1 = "Hello", s2 = "World"; Swap<string>(ref s1, ref s2); Note Just as instantiating a generic class with different type parameters causes the compiler to generate different types, each distinct use of the Swap<T> method causes the compiler to gener- ate a different version of the method. Swap<int> is not the same method as Swap<string>; both methods just happen to have been generated from the same generic method, so they exhibit the same behavior, albeit over different types. Defining a Generic Method to Build a Binary Tree The preceding exercise showed you how to create a generic class for implementing a binary tree. The Tree<TItem> class provides the Insert method for adding data items to the tree. However, if you want to add a large number of items, repeated calls to the Insert method are not very convenient. In the following exercise, you will define a generic method called InsertIntoTree that you can use to insert a list of data items into a tree with a single method call. You will test this method by using it to insert a list of characters into a tree of characters. Write the InsertIntoTree method 1. Using Visual Studio 2010, create a new project by using the Console Application tem- plate. In the New Project dialog box, name the project BuildTree. If you are using Visual Studio 2010 Standard or Visual Studio 2010 Professional, set the Location to \ Microsoft Press\Visual CSharp Step By Step\Chapter 18 under your Documents folder, and select Create a new Solution from the Solution drop-down list. Click OK. 372 Part III Creating Components 2. On the Project menu, click Add Reference. In the Add Reference dialog box, click the Browse tab. Move to the folder \Microsoft Press\Visual CSharp Step By Step\Chapter 18\ BinaryTree\BinaryTree\bin\Debug, click BinaryTree.dll, and then click OK. The BinaryTree assembly is added to the list of references shown in Solution Explorer. 3. In the Code and Text Editor window displaying the Program.cs file, add the following using directive to the top of the Program.cs file: using BinaryTree; This namespace contains the Tree<TItem> class. 4. Add a method called InsertIntoTree to the Program class after the Main method. This should be a static method that takes a Tree<TItem> variable and a params array of TItem elements called data. The method definition should look like this: static void InsertIntoTree<TItem>(Tree<TItem> tree, params TItem[] data) { } Tip An alternative way of implementing this method is to create an extension method of the Tree<TItem> class by prefixing the Tree<TItem> parameter with the this keyword and defining the InsertIntoTree method in a static class, like this: public static class TreeMethods { public static void InsertIntoTree<TItem>(this Tree<TItem> tree, params TItem[] data) { } } The principal advantage of this approach is that you can invoke the InsertIntoTree method directly on a Tree<TItem> object rather than pass the Tree<TItem> in as a parameter. However, for this exercise, we will keep things simple. 5. The TItem type used for the elements being inserted into the binary tree must implement the IComparable<TItem> interface. Modify the definition of the InsertIntoTree method and add the appropriate where clause, as shown in bold type in the following code: static void InsertIntoTree<TItem>(Tree<TItem> tree, params TItem[] data) where TItem : IComparable<TItem> { } Chapter 18 Introducing Generics 373 6. Add the following statements shown in bold type to the InsertIntoTree method. These statements check to make sure that the user has actually passed some parameters into the method (the data array might be empty), and then they iterate through the params list, adding each item to the tree by using the Insert method. The tree is passed back as the return value: static void InsertIntoTree<TItem>(Tree<TItem> tree, params TItem[] data) where TItem : IComparable<TItem> { if (data.Length == 0) throw new ArgumentException("Must provide at least one data value"); foreach (TItem datum in data) { tree.Insert(datum); } } Test the InsertIntoTree method 1. In the Main method of the Program class, add the following statements shown in bold type that create a new Tree for holding character data, populate it with some sample data by using the InsertIntoTree method, and then display it by using the WalkTree method of Tree: static void Main(string[] args) { Tree<char> charTree = new Tree<char>('M'); InsertIntoTree<char>(charTree, 'X', 'A', 'M', 'Z', 'Z', 'N'); charTree.WalkTree(); } 2. On the Build menu, click Build Solution. Verify that the solution compiles, and correct any errors if necessary. 3. On the Debug menu, click Start Without Debugging. The program runs and displays the character values in the following order: A, M, M, N, X, Z, Z 4. Press the Enter key to return to Visual Studio 2010. Variance and Generic Interfaces In Chapter 8, you learned that you can use the object type to hold a value or reference of any other type. For example, the following code is completely legal: string myString = "Hello"; object myObject = myString; 374 Part III Creating Components Remember that in inheritance terms, the String class is derived from the Object class, so all strings are objects. Now consider the following generic interface and class: interface IWrapper<T> { void SetData(T data); T GetData(); } class Wrapper<T> : IWrapper<T> { private T storedData; void IWrapper<T>.SetData(T data) { this.storedData = data; } T IWrapper<T>.GetData() { return this.storedData; } } The Wrapper<T> class provides a simple wrapper around a specified type. The IWrapper interface defines the SetData method that the Wrapper<T> class implements to store the data, and the GetData method that the Wrapper<T> class implements to retrieve the data. You can create an instance of this class and use it to wrap a string like this: Wrapper<string> stringWrapper = new Wrapper<string>(); IWrapper<string> storedStringWrapper = stringWrapper; storedStringWrapper.SetData("Hello"); Console.WriteLine("Stored value is {0}", storedStringWrapper.GetData()); The code creates an instance of the Wrapper<string> type. It references the object through the IWrapper<string> interface to call the SetData method. (The Wrapper<T> type imple- ments its interfaces explicitly, so you must call the methods through an appropriate interface reference.) The code also calls the GetData method through the IWrapper<string> interface. If you run this code, it outputs the message “Stored value is Hello”. Now look at the following line of code: IWrapper<object> storedObjectWrapper = stringWrapper; This statement is similar to the one that creates the IWrapper<string> reference in the previous code example, the difference being that the type parameter is object rather than string. Is this code legal? Remember that all strings are objects (you can assign a string value to an object reference, as shown earlier), so in theory this statement looks promising. Chapter 18 Introducing Generics 375 However, if you try it, the statement will fail to compile with the message “Cannot implicitly convert type ‘Wrapper<string>’ to ‘IWrapper<object>’.” You can try an explicit cast such as this: IWrapper<object> storedObjectWrapper = (IWrapper<object>)stringWrapper; This code compiles, but will fail at runtime with an InvalidCastException exception. The problem is that although all strings are objects, the converse is not true. If this statement was allowed, you could write code like this, which ultimately attempts to store a Circle object in a string field: IWrapper<object> storedObjectWrapper = (IWrapper<object>)stringWrapper; Circle myCircle = new Circle(); storedObjectWrapper.SetData(myCircle); The IWrapper<T> interface is said to be invariant. You cannot assign an IWrapper<A> object to a reference of type IWrapper<B>, even if type A is derived from type B. By default, C# implements this restriction to ensure the type-safety of your code. Covariant Interfaces Suppose you defined the IStoreWrapper<T> and IRetrieveWrapper<T> interfaces shown next in place of IWrapper<T> and implemented these interfaces in the Wrapper<T> class, like this: interface IStoreWrapper<T> { void SetData(T data); } interface IRetrieveWrapper<T> { T GetData(); } class Wrapper<T> : IStoreWrapper<T>, IRetrieveWrapper<T> { private T storedData; void IStoreWrapper<T>.SetData(T data) { this.storedData = data; } T IRetrieveWrapper<T>.GetData() { return this.storedData; } } 376 Part III Creating Components Functionally, the Wrapper<T> class is the same as before, except that you access the SetData and GetData methods through different interfaces: Wrapper<string> stringWrapper = new Wrapper<string>(); IStoreWrapper<string> storedStringWrapper = stringWrapper; storedStringWrapper.SetData("Hello"); IRetrieveWrapper<string> retrievedStringWrapper = stringWrapper; Console.WriteLine("Stored value is {0}", retrievedStringWrapper.GetData()); Now, is the following code legal? IRetrieveWrapper<object> retrievedObjectWrapper = stringWrapper; The quick answer is “no”, and it fails to compile with the same error as before. But if you think about it, although the C# compiler has deemed that this statement is not type-safe, the rea- sons for assuming this are no longer valid. The IRetrieveWrapper<T> interface only allows you to read the data held in the IWrapper<T> object by using the GetData method, and it does not provide any way to change the data. In situations such as this where the type parameter occurs only as the return value of the methods in a generic interface, you can inform the compiler that some implicit conversions are legal and that it does not have to enforce strict type-safety. You do this by specifying the out keyword when you declare the type parameter, like this: interface IRetrieveWrapper<out T> { T GetData(); } This feature is called covariance. You can assign an IRetrieveWrapper<A> object to an IRetrieveWrapper<B> reference as long as there is a valid conversion from type A to type B, or type A derives from type B. The following code now compiles and runs as expected: // string derives from object, so this is now legal IRetrieveWrapper<object> retrievedObjectWrapper = stringWrapper; You can specify the out qualifier with a type parameter only if the type parameter occurs as the return type of methods. If you use the type parameter to specify the type of any method parameters, the out qualifier is illegal and your code will not compile. Also, covariance works only with reference types. This is because value types cannot form inheritance hierarchies. The following code will not compile because int is a value type: Wrapper<int> intWrapper = new Wrapper<int>(); IStoreWrapper<int> storedIntWrapper = intWrapper; // this is legal // the following statement is not legal – ints are not objects IRetrieveWrapper<object> retrievedObjectWrapper = intWrapper; Several of the interfaces defined by the .NET Framework exhibit covariance, including the IEnumerable<T> interface that you will meet in Chapter 19, “Enumerating Collections.” Chapter 18 Introducing Generics 377 Contravariant Interfaces Contravariance is the corollary of covariance. It enables you to use a generic interface to reference an object of type B through a reference to type A as long as type B derives type A. This sounds complicated, so it is worth looking at an example from the .NET Framework class library. The System.Collections.Generic namespace in the .NET Framework provides an interface called IComparer, which looks like this: public interface IComparer<in T> { int Compare(T x, T y); } A class that implements this interface has to define the Compare method, which is used to compare two objects of the type specified by the T type parameter. The Compare method is expected to return an integer value: zero if the parameters x and y have the same value, negative if x is less than y, and positive if x is greater than y. The following code shows an example that sorts objects according to their hash code. (The GetHashCode method is imple- mented by the Object class. It simply returns an integer value that identifies the object. All reference types inherit this method and can override it with their own implementations.) class ObjectComparer : IComparer<Object> { int Comparer<object>.Compare(Object x, Object y) { int xHash = x.GetHashCode(); int yHash = y.GetHashCode(); if (xHash == yHash) return 0; if (xHash < yHash) return -1; return 1; } } You can create an ObjectComparer object and call the Compare method through the IComparer<Object> interface to compare two objects, like this: Object x = ; Object y = ; ObjectComparer comparer = new ObjectComparer(); IComparer<Object> objectComparator = objectComparer; int result = objectComparator(x, y); 378 Part III Creating Components That’s the boring bit. What is more interesting is that you can reference this same object through a version of the IComparer interface that compares strings, like this: IComparer<String> stringComparator = objectComparer; At first glance, this statement seems to break every rule of type-safety that you can imagine. However, if you think about what the IComparer<T> interface does, this makes some sense. The purpose of the Compare method is to return a value based on a comparison between the parameters passed in. If you can compare Objects, you certainly should be able to com- pare Strings, which are just specialized types of Objects. After all, a String should be able to do anything that an Object can do—that is the purpose of inheritance. This still sounds a little presumptive, however. How does the C# compiler know that you are not going to perform any type-specific operations in the code for the Compare method that might fail if you invoke the method through an interface based on a different type? If you revisit the definition of the IComparer interface, you can see the in qualifier prior to the type parameter: public interface IComparer<in T> { int Compare(T x, T y); } The in keyword tells the C# compiler that either you can pass the type T as the parameter type to methods or you can pass any type that derives from T. You cannot use T as the return type from any methods. Essentially, this enables you to reference an object either through a generic interface based on the object type or through a generic interface based on a type that derives from the object type. Basically, if a type A exposes some operations, properties, or fields, then if type B derives from type A it must also expose the same operations (which might behave differently if they have been overridden), properties, and fields. Consequently, it should be safe to substitute an object of type B for an object of type A. Covariance and contravariance might seem like fringe topics in the world of generics, but they are useful. For example, the List<T> generic collection class uses IComparer<T> objects to implement the Sort and BinarySearch methods. A List<Object> object can contain a col- lection of objects of any type, so the Sort and BinarySearch methods need to be able to sort objects of any type. Without using contravariance, the Sort and BinarySearch methods would need to include logic that determines the real types of the items being sorted or searched and then implement a type-specific sort or search mechanism. However, unless you are a mathematician it can be quite difficult to recall what covariance and contravariance actually do. The way I remember, based on the examples in this section, is as follows: n Covariance If the methods in a generic interface can return strings, they can also return objects. (All strings are objects.) Chapter 18 Introducing Generics 379 n Contravariance If the methods in a generic interface can take object parameters, they can take string parameters. (If you can perform an operation by using an object, you can perform the same operation by using a string because all strings are objects.) Note Only interface and delegate types can be declared as covariant or contravariant. You cannot use the in or out modifiers with generic classes. In this chapter, you learned how to use generics to create type-safe classes. You saw how to instantiate a generic type by specifying a type parameter. You also saw how to implement a generic interface and define a generic method. Finally, you learned how to define covariant and contravariant generic interfaces that can operate with a hierarchy of types. n If you want to continue to the next chapter Keep Visual Studio 2010 running, and turn to Chapter 19. n If you want to exit Visual Studio 2010 now On the File menu, click Exit. If you see a Save dialog box, click Yes and save the project. Chapter 18 Quick Reference To Do this Instantiate an object by using a generic type Specify the appropriate generic type parameter. For example: Queue<int> myQueue = new Queue<int>(); Create a new generic type Define the class using a type parameter. For example: public class Tree<TItem> { } Restrict the type that can be substituted for the generic type parameter Specify a constraint by using a where clause when defining the class. For example: public class Tree<TItem> where TItem : IComparable<TItem> { } Define a generic method Define the method by using type parameters. For example: static void InsertIntoTree<TItem> (Tree<TItem> tree, params TItem[] data) { } [...]... the OrderByDescending method instead If you want to order by more than one key value, you can use the ThenBy or ThenByDescending method after OrderBy or OrderByDescending To group data according to common values in one or more fields, you can use the GroupBy method The next example shows how to group the companies in the addresses array by country: var companiesGroupedByCountry = addresses.GroupBy(addrs... hidden from the user iterating through the elements of the binary tree! Create the TreeEnumerator class 1 Start Microsoft Visual Studio 2010 if it is not already running 2 Open the BinaryTree solution located in the \Microsoft Press \Visual CSharp Step By Step\ Chapter 19\BinaryTree folder in your Documents folder This solution contains a working copy of the BinaryTree project you created in Chapter 18 ... recursive mechanism, similar to the WalkTree method discussed in Chapter 18 Add an enumerator to the Tree class 1 Using Visual Studio 2010, open the BinaryTree solution located in the \Microsoft Press\ Visual CSharp Step By Step\ Chapter 19\IteratorBinaryTree folder in your Documents folder This solution contains another copy of the BinaryTree project you created in Chapter 18 392 Part III Creating... enumerator 1 In Solution Explorer, right-click the BinaryTree solution, point to Add, and then click New Project Add a new project by using the Console Application template Name the project EnumeratorTest, set the Location to \Microsoft Press \Visual CSharp Step By Step\ Chapter 19 in your Documents folder, and then click OK 2 Right-click the EnumeratorTest project in Solution Explorer, and then click Set as... returned by GroupBy contains all the fields in the original source collection, but the rows are ordered into a set of enumerable collections based on the field identified by the method specified by GroupBy In other words, the result of the GroupBy method is an enumerable set of groups, each of which is an enumerable set of rows In the example just shown, the enumerable set companiesGroupedByCountry... new enumerator 1 In Solution Explorer, right-click the BinaryTree solution, point to Add, and then click Existing Project In the Add Existing Project dialog box, move to the folder \Microsoft Press \Visual CSharp Step By Step\ Chapter 19\EnumeratorTest, select the EnumeratorTest project file, and then click Open This is the project that you created to test the enumerator you developed manually earlier... companiesGroupedByCountry) { Console.WriteLine("Country: {0}\t{1} companies", companiesPerCountry.Key, companiesPerCountry.Count()); foreach (var companies in companiesPerCountry) { Console.WriteLine("\t{0}", companies.CompanyName); } } By now, you should recognize the pattern! The GroupBy method expects a method that specifies the fields to group the data by There are some subtle differences between the GroupBy... Enter and return to Visual Studio 2010 In this chapter, you saw how to implement the IEnumerable and IEnumerator interfaces with a collection class to enable applications to iterate through the items in the collection You also saw how to implement an enumerator by using an iterator 394 Part III Creating Components n If you want to continue to the next chapter Keep Visual Studio 2010 running, and turn... Defining an Enumerator for the Tree Class by Using an Iterator In the next exercise, you will implement the enumerator for the Tree class by using an iterator Unlike the preceding set of exercises, which required the data in the tree to be preprocessed into a queue by the MoveNext method, you can define an iterator that traverses the tree by using the more natural recursive mechanism, similar... the values in the following sequence: –12, –8, 0, 5, 5, 10, 10, 11, 14, 15 9 Press Enter to return to Visual Studio 2010 Implementing an Enumerator by Using an Iterator As you can see, the process of making a collection enumerable can become complex and potentially error prone To make life easier, C# includes iterators that can automate much of this process An iterator is a block of code that yields . BuildTree. If you are using Visual Studio 2010 Standard or Visual Studio 2010 Professional, set the Location to Microsoft Press Visual CSharp Step By Step Chapter 18 under your Documents folder, and. Start Microsoft Visual Studio 2010 if it is not already running. 2. Open the BinaryTree solution located in the Microsoft Press Visual CSharp Step By Step Chapter 19BinaryTree folder in your Documents. a new project by using the Console Application template. Name the proj- ect EnumeratorTest, set the Location to Microsoft Press Visual CSharp Step By Step Chapter 19 in your Documents folder,