Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 98 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
98
Dung lượng
1,88 MB
Nội dung
ptg Iterators 639 CSharpPrimitiveTypes primitives = new CSharpPrimitiveTypes(); foreach (string primitive in primitives) { Console.WriteLine(primitive); } } } The results of Listing 16.13 appear in Output 16.5. The output from this listing is a listing of the C# primitive types. 1 Iterators and State When an iterator is first called in a foreach statement (such as foreach (string primitive in primitives) in Listing 16.13), its state is initialized within the enumerator. The iterator maintains its state as long as the foreach statement at the call site continues to execute. When you yield a value, process it, and resume the foreach statement at the call site, the iter- ator continues where it left off the previous time around the loop and OUTPUT 16.5: object byte uint ulong float char bool ushort decimal int sbyte short long void double string 1. In alpha versions of the C# 2.0 compiler, yield was a keyword rather than a contextual keyword. However, such a change could result in an incompatibility between C# 1.0 and C# 2.0. Instead, yield became a contextual keyword that must appear before return. As a result, no code-breaking change occurred because C# 1.0 did not allow any text (besides comments) prior to the return keyword. From the Library of Wow! eBook ptg Chapter 16: Building Custom Collections640 continues processing. When the foreach statement at the call site termi- nates, the iterator’s state is no longer saved. It is always safe to call the iter- ator again since the generated code never resets the state of the iterator but instead creates a new one when needed. Figure 16.8 shows a high-level sequence diagram of what takes place. Remember that the MoveNext() method appears on the IEnumerator<T> interface. Figure 16.8: Sequence Diagram with yield return Program primitives: CSharpPrimitiveTypes GetEnumerator() enumerator: Enumerator Instantiate yield return "object" yield return "byte" MoveNext() yield return "string" MoveNext() Console WriteLine() WriteLine() WriteLine() MoveNext() From the Library of Wow! eBook ptg Iterators 641 In Listing 16.13, the foreach statement at the call site initiates a call to GetEnumerator() on the CSharpPrimitiveTypes instance called primi- tives. Given the iterator instance (referenced by iterator), foreach begins each iteration with a call to MoveNext(). Within the iterator, you yield a value back to the foreach statement at the call site. After the yield return statement, the GetEnumerator() method seemingly pauses until the next MoveNext() request. Back at the call site, the foreach statement displays the yielded value on the screen. It then loops back around and calls MoveNext() on the iterator again. Notice that the second time, processing picks up at the second yield return statement. Once again, the foreach displays on the screen what CSharpPrimitiveTypes yielded and starts the loop again. This process continues until there are no more yield return statements within the iterator. At that point, the foreach loop at the call site terminates. More Iterator Examples Before you modify BinaryTree<T>, you must modify Pair<T> to support the IEnumerable<T> interface using an iterator. Listing 16.14 is an example that yields each element in Pair<T>. Listing 16.14: Using yield to Implement BinaryTree<T> public struct Pair<T>: IPair<T>, { public Pair(T first, T second) { _first = first; _second = second; } public T First { get{ return _first; } private set{ _first = value; } } private T _first; public T Second { get{ return _second; } private set{ _second = value; } } IEnumerable<T> From the Library of Wow! eBook ptg Chapter 16: Building Custom Collections642 private T _second; } In Listing 16.14, the iteration over the Pair<T> data type loops twice: first through yield return First, and then through yield return Second. Each time the yield return statement within GetEnumerator() is encoun- tered, the state is saved and execution appears to “jump” out of the GetEnumerator() method context and into the context of the call site. When the second iteration starts, GetEnumerator() begins to execute again with the yield return Second statement. System.Collections.Generic.IEnumerable<T> inherits from System. Collections.IEnumerable. Therefore, when implementing IEnumera- ble<T>, it is also necessary to implement IEnumerable. In Listing 16.14, you do so explicitly, and the implementation simply involves a call to IEnumera- ble<T>’s GetEnumerator() implementation. This call from IEnumerable. GetEnumerator() to IEnumerable<T>.GetEnumerator() will always work because of the type compatibility (via inheritance) between IEnumerable<T> and IEnumerable. Since the signatures for both GetEnumerator()s are identical (the return type does not distinguish a signature), one or both implementations must be explicit. Given the additional type safety offered by IEnumerable<T>’s version, you implement IEnumerable’s implementa- tion explicitly. Listing 16.15 uses the Pair<T>.GetEnumerator() method and displays "Inigo" and "Montoya" on two consecutive lines. #region IEnumerable<T> public IEnumerator<T> GetEnumerator() { yield return First; yield return Second; } #endregion IEnumerable<T> #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator { return GetEnumerator(); } #endregion From the Library of Wow! eBook ptg Iterators 643 Listing 16.15: Using Pair<T>.GetEnumerator() via foreach Pair<string> fullname = new Pair<string>("Inigo", "Montoya"); foreach (string name in fullname) { Console.WriteLine(name); } Notice that the call to GetEnumerator() is implicit within the foreach loop. Placing a yield return within a Loop It is not necessary to hardcode each yield return statement, as you did in both CSharpPrimitiveTypes and Pair<T>. Using the yield return state- ment, you can return values from inside a loop construct. Listing 16.16 uses a foreach loop. Each time the foreach within GetEnumerator() exe- cutes, it returns the next value. Listing 16.16: Placing yield return Statements within a Loop public class BinaryTree<T>: IEnumerable<T> { // #region IEnumerable<T> public IEnumerator<T> GetEnumerator() { // Return the item at this node. yield return Value; // Iterate through each of the elements in the pair. } #endregion IEnumerable<T> foreach (BinaryTree<T> tree in SubItems) { if (tree != null) { // Since each element in the pair is a tree, // traverse the tree and yield each // element. foreach (T item in tree) { yield return item; } } } From the Library of Wow! eBook ptg Chapter 16: Building Custom Collections644 #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } In Listing 16.16, the first iteration returns the root element within the binary tree. During the second iteration you traverse the pair of subele- ments. If the subelement pair contains a non-null value, then you traverse into that child node and yield its elements. Note that foreach(T item in tree) is a recursive call to a child node. As observed with CSharpPrimitiveTypes and Pair<T>, you can now iterate over BinaryTree<T> using a foreach loop. Listing 16.17 demon- strates this, and Output 16.6 shows the results. Listing 16.17: Using foreach with BinaryTree<string> // JFK jfkFamilyTree = new BinaryTree<string>( "John Fitzgerald Kennedy"); jfkFamilyTree.SubItems = new Pair<BinaryTree<string>>( new BinaryTree<string>("Joseph Patrick Kennedy"), new BinaryTree<string>("Rose Elizabeth Fitzgerald")); // Grandparents (Father's side) jfkFamilyTree.SubItems.First.SubItems = new Pair<BinaryTree<string>>( new BinaryTree<string>("Patrick Joseph Kennedy"), new BinaryTree<string>("Mary Augusta Hickey")); // Grandparents (Mother's side) jfkFamilyTree.SubItems.Second.SubItems = new Pair<BinaryTree<string>>( new BinaryTree<string>("John Francis Fitzgerald"), new BinaryTree<string>("Mary Josephine Hannon")); foreach (string name in jfkFamilyTree) { Console.WriteLine(name); } From the Library of Wow! eBook ptg Iterators 645 BEGINNER TOPIC struct versus class An interesting side effect of defining Pair<T> as a struct rather than a class is that SubItems.First and SubItems.Second cannot be assigned directly. The following will produce a compile error indicating that Sub- Items cannot be modified, “because it is not a variable”: jfkFamilyTree.SubItems.First = new BinaryTree<string>("Joseph Patrick Kennedy"); The issue is that SubItems is a property of type Pair<T>, a struct. There- fore, when the property returns the value, a copy of _SubItems is made, and assigning First on a copy that is promptly lost at the end of the state- ment would be misleading. Fortunately, the C# compiler prevents this. To overcome the issue, don’t assign it (see the approach in Listing 16.17), use class rather than struct for Pair<T>, don’t create a SubItems property and instead use a field, or provide properties in BinaryTree<T> that give direct access to _SubItems members. Canceling Further Iteration: yield break Sometimes you might want to cancel further iteration. You can do this by including an if statement so that no further statements within the code are executed. However, you can also jump back to the call site, causing MoveNext() to return false. Listing 16.18 shows an example of such a method. OUTPUT 16.6: John Fitzgerald Kennedy Joseph Patrick Kennedy Patrick Joseph Kennedy Mary Augusta Hickey Rose Elizabeth Fitzgerald John Francis Fitzgerald Mary Josephine Hannon From the Library of Wow! eBook ptg Chapter 16: Building Custom Collections646 Listing 16.18: Escaping Iteration via yield break public System.Collections.Generic.IEnumerable<T> GetNotNullEnumerator() { yield return Second; yield return First; } This method cancels the iteration if either of the elements in the Pair<T> class is null. A yield break statement is similar to placing a return statement at the top of a function when it is determined that there is no work to do. It is a way to exit from further iterations without surrounding all remaining code with an if block. As such, it allows multiple exits, and therefore, you should use it with caution because casual reading of the code may miss the early exit. ADVANCED TOPIC How Iterators Work When the C# compiler encounters an iterator, it expands the code into the appropriate CIL for the corresponding enumerator design pattern. In the generated code, the C# compiler first creates a nested private class to imple- ment the IEnumerator<T> interface, along with its Current property and a MoveNext() method. The Current property returns a type corresponding to the return type of the iterator. Listing 16.14 of Pair<T> contains an iterator that returns a T type. The C# compiler examines the code contained within the iterator and creates the necessary code within the MoveNext method and the Current property to mimic its behavior. For the Pair<T> iterator, the C# compiler generates roughly equivalent code (see Listing 16.19). Listing 16.19: C# Equivalent of Compiler-Generated C# Code for Iterators using System; using System.Collections.Generic; if((First == null) || (Second == null)) { yield break; } From the Library of Wow! eBook ptg Iterators 647 public class Pair<T> : IPair<T>, IEnumerable<T> { // // The iterator is expanded into the following // code by the compiler public virtual IEnumerator<T> GetEnumerator() { __ListEnumerator result = new __ListEnumerator(0); result._Pair = this; return result; } public virtual System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return new GetEnumerator(); } private sealed class __ListEnumerator<T> : IEnumerator<T> { public __ListEnumerator(int itemCount) { _ItemCount = itemCount; } Pair<T> _Pair; T _Current; int _ItemCount; public object Current { get { return _Current; } } public bool MoveNext() { switch (_ItemCount) { case 0: _Current = _Pair.First; _ItemCount++; return true; case 1: _Current = _Pair.Second; _ItemCount++; From the Library of Wow! eBook ptg Chapter 16: Building Custom Collections648 return true; default: return false; } } } } Because the compiler takes the yield return statement and generates cla- sses that correspond to what you probably would have written manually, iterators in C# exhibit the same performance characteristics as classes that implement the enumerator design pattern manually. Although there is no performance improvement, the programmer productivity gained is significant. Creating Multiple Iterators in a Single Class Previous iterator examples implemented IEnumerable<T>.GetEnumera- tor(). This is the method that foreach seeks implicitly. Sometimes you might want different iteration sequences, such as iterating in reverse, fil- tering the results, or iterating over an object projection other than the default. You can declare additional iterators in the class by encapsulating them within properties or methods that return IEnumerable<T> or IEnu- merable. If you want to iterate over the elements of Pair<T> in reverse, for example, you provide a GetReverseEnumerator() method, as shown in Listing 16.20. Listing 16.20: Using yield return in a Method That Returns IEnumerable<T> public struct Pair<T>: IEnumerable<T> { { yield return Second; yield return First; } } public void Main() { Pair<string> game = new Pair<string>("Redskins", "Eagles"); public IEnumerable<T> GetReverseEnumerator() From the Library of Wow! eBook [...]... namespace of System.Collections as obsolete (in fact, it has been excluded from the Silverlight CLR entirely) In other words, don’t go back and necessarily remove all code that already uses this namespace Instead, use System.Collections.Generics for any new code and, over time, consider migrating existing code to use the corresponding generic collections which contain both the interfaces and the classes for... displays a collection of objects Given the collection, a list control could use reflection to iterate over all the properties of an object in the collection, defining a column within the list for each From the Library of Wow! eBook Reflection 653 property Furthermore, by invoking each property on each object, the list control could populate each row and column with the data contained in the object, even... returns a dictionary collection of all the switches, including those from the property names, and associate each name with the corresponding attribute on the command-line object Listing 17.15: Retrieving Custom Attribute Instances using System; using System.Reflection; using System.Collections.Generic; public class CommandLineSwitchAliasAttribute : Attribute { public CommandLineSwitchAliasAttribute(string... no catch block From the Library of Wow! eBook 650 Chapter 16: Building Custom Collections SUMMARY The generic collection classes and interfaces made available in C# 2.0 are universally superior to their nongeneric counterparts; by avoiding boxing penalties and enforcing type rules at compile time, they are faster and safer Unless you are limited to C# 1.0, you should consider the entire namespace of... Aliases using System; using System.Reflection; using System.Collections.Generic; public class CommandLineHandler { // public static bool TryParse( string[] args, object commandLine, out string errorMessage) { bool success = false; errorMessage = null; Dictionary options = CommandLineSwitchAliasAttribute.GetSwitches( commandLine); foreach (string arg in args) { PropertyInfo property;... ProcessPriorityClass _Priority = ProcessPriorityClass.Normal; } } using System; using System.Diagnostics; using System.Reflection; public class CommandLineHandler { From the Library of Wow! eBook Reflection 657 public static void Parse(string[] args, object commandLine) { string errorMessage; if (!TryParse(args, commandLine, out errorMessage)) { throw new ApplicationException(errorMessage); } } public... generic Obtaining Type Parameters for a Generic Class or Method You can obtain a list of generic arguments, or type parameters, from a generic class by calling the GetGenericArguments() method The result is an array of System.Type instances that corresponds to the order in which they are declared as type parameters of the generic class Listing 17.6 reflects into a generic type and obtains each type... attributes are a means of associating additional data with a property (and other constructs) Attributes appear within square brackets preceding the construct they decorate For example, you can modify the CommandLineInfo class to include attributes, as shown in Listing 17.7 Listing 17.7: Decorating a Property with an Attribute class CommandLineInfo { [CommandLineSwitchAlias("?")] public bool Help { get { return... whether to check any overloaded methods (Alternatively, you can call the GetCustomAttributes() method without the attribute type to return all of the attributes.) Although it is possible to place code for finding the CommandLineSwitchRequiredAttribute attribute within the CommandLineHandler’s code directly, it makes for better object encapsulation to place the code within the CommandLineSwitchRequiredAttribute... and in the process create stubs for documentation of the assembly API You can then combine the metadata retrieved from reflection with the XML document created from XML comments (using the/doc switch) to create the API documentation Similarly, programmers use reflection metadata to generate code for persisting (serializing) business objects into a database It could also be used in a list control that . which contain both the interfaces and the classes for working with collec- tions of objects. Providing the System.Collections.Generic namespace is not the only change that C# 2.0 brought to collections list control that displays a collection of objects. Given the collection, a list control could use reflection to iterate over all the proper- ties of an object in the collection, defining a column. all code that already uses this namespace. Instead, use System.Collections.Generics for any new code and, over time, con- sider migrating existing code to use the corresponding generic collections