Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C# ПІДРУЧНИКИ / c# / Apress - Accelerated C# 2005

.pdf
Скачиваний:
83
Добавлен:
12.02.2016
Размер:
2.09 Mб
Скачать

224C H A P T E R 9 A R R AYS, C O L L E C T I O N T Y P E S, A N D I T E R ATO R S

a yield block, it’s easy and dangerous to do so if used improperly. That’s not to say that this technique cannot be useful, as I show in an example in the section “Forward, Reverse, and Bidirectional Iterators,” when I demonstrate how to create a bidirectional iterator.

You can avoid this whole can of worms by introducing the proverbial extra level of indirection. Instead of making the parameter to GetEnumerator() a bool, make it a custom defined type that is immutable, such as ImmutableBool. The following example shows how you can do this to avoid outside entities mucking with public fields of the compiler-generated enumerators:

using System;

using System.Threading; using System.Reflection; using System.Collections;

using System.Collections.Generic;

public struct ImmutableBool

{

public ImmutableBool( bool b ) { this.b = b;

}

public bool Value { get { return b; }

}

private bool b;

}

public class MyColl<T> : IEnumerable<T>

{

public MyColl( T[] items ) { this.items = items;

}

public IEnumerator<T> GetEnumerator(

ImmutableBool synchronized ) { if( synchronized.Value ) {

Monitor.Enter( items.SyncRoot );

}

try {

foreach( T item in items ) { yield return item;

}

}

finally {

if( synchronized.Value ) { Monitor.Exit( items.SyncRoot );

}

}

}

public IEnumerator<T> GetEnumerator() {

return GetEnumerator( new ImmutableBool(false) );

}

IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator();

}

C H A P T E R 9 A R R AYS, C O L L E C T I O N T Y P E S, A N D I T E R ATO R S

225

private T[] items;

}

public class EntryPoint

{

static void Main() { MyColl<int> integers =

new MyColl<int>( new int[] {1, 2, 3, 4} );

IEnumerator<int> enumerator =

integers.GetEnumerator( new ImmutableBool(true) );

// Get reference to synchronized field. object field = enumerator.GetType().

GetField("synchronized").GetValue( enumerator );

while( enumerator.MoveNext() ) { Console.WriteLine( enumerator.Current );

// Throws an exception. field.GetType().GetProperty("Value").

SetValue( field, false, null );

}

}

}

In Main, you can see that I’m using the enumerator directly rather than via foreach. Even though this is not recommended, I’m doing it anyway because I’m playing the part of the diabolical developer who wants to change the enumerator’s state during enumeration. You can see in the while loop that I attempt to change the value of the property on the ImmutableBool in the enumerator using reflection. This throws an exception, since the property has no setter defined on it.

Note Those of you familiar with the intricacies of reflection will recognize that it is technically possible for the code to modify the private field within the ImmutableBool instance. That, however, requires that the code pass the ReflectionPermission code access security (CAS) demand. This demand fails unless the person running this code has granted it explicitly, and that’s unlikely. CAS is beyond the scope of this book, but for all the nittygritty details on CAS, including how to extend it to meet your needs, I recommend reading .NET Framework Security by Brian A. LaMacchia, et al. (Upper Saddle River, NJ: Pearson Education, 2002).

Using an immutable type, you can rest easy knowing that nobody can muck with the state of the enumerator and cause really bad things to happen.

So far, you’ve seen how iterator blocks are handy for creating enumerators. However, you can also use them to generate the enumerable type as well. For example, suppose you want to iterate through the first few powers of 2. You could do the following:

using System;

using System.Collections.Generic;

public class EntryPoint

{

static public IEnumerable<int> Powers( int from, int to ) {

for( int i = from; i <= to; ++i ) { yield return (int) Math.Pow( 2, i );

226 C H A P T E R 9 A R R AYS, C O L L E C T I O N T Y P E S, A N D I T E R ATO R S

}

}

static void Main() {

IEnumerable<int> powers = Powers( 0, 16 ); foreach( int result in powers ) {

Console.WriteLine( result );

}

}

}

In this example, the compiler generates a single type that implements the four interfaces

IEnumerable<int>, IEnumerable, IEnumerator<int>, and IEnumerator. Therefore, this type serves as both the enumerable and the enumerator. The bottom line is that any method that contains a yield block must return a type of IEnumerable<T>, IEnumerable, IEnumerator<T>, or IEnumerator, where T is the yield type of the method. The compiler will handle the rest. I recommend that you strive to use the generic versions, since they will avoid unnecessary boxing for value types and give the type-safety engine more muscle. In the previous example, the from and to values are stored as public fields in the enumerable type, as shown earlier in this section. So, you may want to consider wrapping them up inside an immutable type if you want to prevent users from modifying them during enumeration.

Tip Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries by Krzysztof Cwalina and Brad Abrams (Boston, MA: Addison-Wesley Professional, 2005) suggests that a type should never implement both IEnumerable<T> and IEnumerator<T>, since a single type should semantically be either a collection or an enumerator and not both. However, the objects generated by yield blocks violate this rule. For hand-coded collections, you should try to adhere to the rule, even though it’s clear that C# does this to make yield blocks more useful.

Forward, Reverse, and Bidirectional Iterators

Many libraries that support iterators on their container types support three main flavors of iterators in the form of forward, reverse, and bidirectional iterators. Forward iterators are analogous to regular enumerators implementing IEnumerator<T>, which the GetEnumerator methods of the container types in the .NET library typically expose. However, what if you need a reverse iterator or a bidirectional iterator? C# iterators make creating such things nice and easy.

To get a reverse iterator for your container, all you need to do is create a yield block that loops through the items in the collection in reverse order. Even more convenient, you can typically declare your yield block external to your collection, as shown in the following example:

using System;

using System.Collections.Generic;

public class EntryPoint

{

static void Main() {

List<int> intList = new List<int>(); intList.Add( 1 );

intList.Add( 2 ); intList.Add( 3 ); intList.Add( 4 );

foreach( int n in CreateReverseIterator(intList) ) { Console.WriteLine( n );

C H A P T E R 9 A R R AYS, C O L L E C T I O N T Y P E S, A N D I T E R ATO R S

227

}

}

static IEnumerable<T> CreateReverseIterator<T>( IList<T> list ) { int count = list.Count;

for( int i = count-1; i >= 0; --i ) { yield return list[i];

}

}

}

The meat of the example is in the CreateReverseIterator<T> method. This method only works on collections of type IList<T>, but you could easily write another form of CreateReverseIterator<T> that takes some other collection type. When you create utility methods of this sort, it’s always best to be as generic as possible in the types that you accept. For example, would it be possible to make CreateReverseIterator<T> more general-purpose by accepting a type of ICollection<T>? No, because ICollection<T> doesn’t declare an index operator. IList<T> does declare an index operator, though.

Now let’s turn our attention to a bidirectional iterator. In order to make a bidirectional iterator out of an enumerator, you need to be able to toggle its direction. As I showed previously, enumerators created from methods that accept parameters and contain a yield block have public fields that you can modify. Although you must use reflection to access these fields, you can still do it nevertheless. First, let’s look at a possible usage scenario for a bidirectional iterator:

static void Main() {

LinkedList<int> intList = new LinkedList<int>(); for( int i = 1; i < 6; ++i ) {

intList.AddLast( i );

}

BidirectionalIterator<int> iter =

new BidirectionalIterator<int>(intList, intList.First,

TIteratorDirection.Forward);

foreach( int n in iter ) { Console.WriteLine( n );

if( n == 5 ) {

iter.Direction = TIteratorDirection.Backward;

}

}

}

You need a way to create an iterator object that supports IEnumerable<T> and then use it within a foreach statement to start the enumeration. At any time within the foreach block, you want the ability to reverse the direction of iteration. The following example shows a BidirectionalIterator class that facilitates the previous usage model:

public enum TIteratorDirection { Forward,

Backward

};

public class BidirectionalIterator<T> : IEnumerable<T>

{

public BidirectionalIterator( LinkedList<T> list, LinkedListNode<T> start, TIteratorDirection dir ) {

228 C H A P T E R 9 A R R AYS, C O L L E C T I O N T Y P E S, A N D I T E R ATO R S

enumerator = CreateEnumerator( list, start,

dir ).GetEnumerator(); enumType = enumerator.GetType();

}

public TIteratorDirection Direction { get {

return (TIteratorDirection) enumType.GetField("dir")

.GetValue( enumerator );

}

set {

enumType.GetField("dir").SetValue( enumerator, value );

}

}

private IEnumerator<T> enumerator; private Type enumType;

private IEnumerable<T> CreateEnumerator( LinkedList<T> list, LinkedListNode<T> start, TIteratorDirection dir ) {

LinkedListNode<T> current = null; do {

if( current == null ) { current = start;

} else {

if( dir == TIteratorDirection.Forward ) { current = current.Next;

} else {

current = current.Previous;

}

}

if( current != null ) {

yield return current.Value;

}

} while( current != null );

}

public IEnumerator<T> GetEnumerator() { return enumerator;

}

IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator();

}

}

Technically speaking, I didn’t have to wrap the enumerator inside the BidirectionalIterator class. I could have accessed the direction variable via reflection from within the foreach block directly. However, in order to do that, the code within the foreach block would have needed the name of the parameter passed into the BidirectionalIterator.CreateEnumerator method with the yield block. In order to avoid such disjoint coupling, I tidied it up within the BidirectionalIterator wrapper class and provided a Direction property to access the public field on the enumerator.

C H A P T E R 9 A R R AYS, C O L L E C T I O N T Y P E S, A N D I T E R ATO R S

229

Finally, the following example shows how you can use the same technique to implement

a circular iterator. You could use this for things such as game loops, where you must iterate indefinitely through a collection of entities, updating their state with each pass until requested to quit:

using System;

using System.Collections;

using System.Collections.Generic;

public class EntryPoint

{

static void Main() {

LinkedList<int> intList = new LinkedList<int>(); for( int i = 1; i < 6; ++i ) {

intList.AddLast( i );

}

CircularIterator<int> iter =

new CircularIterator<int>(intList, intList.First);

int counter = 0;

foreach( int n in iter ) { Console.WriteLine( n );

if( counter++ == 100 ) { iter.Stop();

}

}

}

}

public class CircularIterator<T> : IEnumerable<T>

{

public CircularIterator( LinkedList<T> list, LinkedListNode<T> start ) {

enumerator = CreateEnumerator( list, start,

false ).GetEnumerator(); enumType = enumerator.GetType();

}

public void Stop() {

enumType.GetField("stop").SetValue( enumerator, true );

}

private IEnumerator<T> enumerator; private Type enumType;

private IEnumerable<T> CreateEnumerator( LinkedList<T> list, LinkedListNode<T> start, bool stop ) {

LinkedListNode<T> current = null; do {

if( current == null ) { current = start;

} else {

current = current.Next;

230 C H A P T E R 9 A R R AYS, C O L L E C T I O N T Y P E S, A N D I T E R ATO R S

if( current == null ) { current = start;

}

}

yield return current.Value; } while( !stop );

}

public IEnumerator<T> GetEnumerator() { return enumerator;

}

IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator();

}

}

I’ve included a Stop method on CircularIterator<T> so that you can easily tell it to stop iterating. Of course, as with the bidirectional iterator example, the Stop method uses reflection to set the public field stop on the compiler-generated enumerator. I’m sure that you’ll agree that there are many more creative uses for yield blocks for creating complex iteration paths.

Summary

In this chapter, I gave a brief overview of how arrays work in the CLR and in C# in preparation for the discussion on generic collection types. After turning the attention to the generic collection types defined in System.Collections.Generic, I covered efficiency and usage concerns and introduced you to the useful types defined in System.Collections.ObjectModel. Finally, I turned the spotlight on enumerators and showed you how to create effective enumerators efficiently by employing iterator yield blocks new to the C# 2.0 language. Although this chapter didn’t delve into the minute details of each of the collection types, it is my goal that after reading this chapter, you will be effectively armed with the information you need to make informed choices about which generic collection types to use and when. I encourage you to reference the MSDN documentation often for all of the finer details regarding the APIs for the collection types.

In the next chapter, I cover delegates, events, and anonymous methods. Anonymous methods are a nice addition to the language. They’re useful for creating callable code inline at the point where you register the callback with the caller.

C H A P T E R 1 0

■ ■ ■

Delegates, Anonymous Functions,

and Events

Delegates provide a built-in, language-supported mechanism for defining and executing callbacks. Their flexibility allows you to define the exact signature of the callback, and that information becomes part of the delegate type itself. Anonymous functions are forms of delegates that allow you to shortcut some of the delegate syntax that, in many cases, is overkill and mundane. Building on top of delegates is the support for events in C# and the .NET platform. Events provide a uniform pattern for hooking up callback implementations—and possibly multiple instances thereof—to the code that triggers the callback.

Overview of Delegates

The CLR provides a runtime that explicitly supports a flexible callback mechanism. From the beginning of time, or at least from the beginning of Windows time, there has always been the need for a callback function that the system, or some other entity, calls at specific times to notify you of something interesting. After all, callbacks provide a convenient mechanism whereby users can extend functionality of a component. Even the most basic component of a Win32 GUI application— the window procedure—is a callback function that is registered with the system. The system calls the function any time it needs to notify you that a message for the window has arrived. This mechanism works just fine in a C-based programming environment.

Things became a little trickier with the widespread use of object-oriented languages such as C++. Developers immediately wanted the ability for the system to be able to call instance methods on objects rather than global functions or static methods. Many solutions to this problem exist. But no matter which solution you use, the bottom line is that somewhere, someone must store an instance pointer to the object and call the instance method through that instance pointer. Implementations typically consist of a thunk, which is nothing more than an intermediate block of data or code that calls the instance method through the instance pointer. This thunk is the actual function registered with the system. Many creative thunk solutions have been developed in C++ over the years. Your trusty author can recall many iterations of such designs as if they were sentimental in nature.

Delegates are now the preferred method of implementing callbacks in the CLR. A delegate instance is essentially the exact same thing as a thunk, except it is a first-class citizen of the CLR. In fact, when you declare a delegate in your code, the C# compiler generates a class derived from MulticastDelegate, and the CLR implements all of the interesting methods of the delegate dynamically at run time. That’s why you won’t see any IL code behind those delegate methods if you examine the compiled module with ILDASM.

231

232 C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

The delegate contains a couple of useful fields. The first one holds a reference to an object, and the second holds a method pointer. When you invoke the delegate, the instance method is called on the contained reference. However, if the object reference is null, the runtime understands this to mean that the method is a static method. One delegate type can handle callbacks to either an instance or static method. Moreover, invoking a delegate syntactically is the exact same as calling a regular function. Therefore, delegates are perfect for implementing callbacks.

As you can see, delegates provide an excellent mechanism to decouple the method being called on an instance from the actual caller. In fact, the caller of the delegate has no idea, or necessity to know, if it is calling an instance method or a static method or on what exact instance it is calling. To the caller, it is calling arbitrary code. The caller can obtain the delegate instance through any appropriate means, and it can be decoupled completely from the entity it actually calls. Think for a moment about UI elements in a dialog, such as a Commit button, and how many external parties may be interested in knowing when that button is pressed. If the class that represents the button must call directly to the interested parties, it needs to have intimate knowledge of the layout of those parties, or objects, and it must know which method to call on each one of them. Clearly, this requirement adds way too much coupling between the button class and the interested parties, and with coupling comes complexity. Delegates come to the rescue and break this link. Now, interested parties need to only register a delegate with the button that is preconfigured to call whatever method they want. This decoupling mechanism describes events as supported by the CLR. I have more to say about CLR events later in this chapter in the “Events” section. Let’s go ahead and see how to create and use delegates in C#.

Delegate Creation and Use

Delegate declarations look almost exactly like method declarations, except they have one added keyword: the delegate keyword. The following is a valid delegate declaration:

public delegate double ProcessResults( double x, double y );

When the C# compiler encounters this line, it defines a type derived from MulticastDelegate, which also implements a method named Invoke that has the exact same signature of the method described in the delegate declaration. For all practical purposes, that class looks like the following:

public class ProcessResults : System.MulticastDelegate

{

public double Invoke( double x, double y );

// Other stuff omitted for clarity

}

Even though the compiler creates a type similar to that listed, the compiler also abstracts the use of delegates behind syntactical shortcuts. In fact, the compiler won’t allow you to call the Invoke method on a delegate directly. Instead, you use a syntax that looks similar to a function call, which I’ll show shortly.

When you instantiate an instance of a delegate, you must wire it up to a method to call when it is invoked. The method that you wire it up to could be either a static or an instance method that has a signature compatible with that of the delegate. Thus, the parameter types and the return type must either match the delegate declaration, or they must be implicitly convertible to the types in the delegate declaration.

Note In .NET 1.x, the signature of the methods wired up to delegates had to match the delegate declaration exactly. In .NET 2.0, this requirement is relaxed to allow methods with compatible types in the declaration.

C H A P T E R 1 0 D E L E G AT E S, A N O N Y M O U S F U N C T I O N S, A N D E V E N T S

233

Single Delegate

The following example shows the basic syntax of how to create a delegate:

using System;

public delegate double ProcessResults( double x, double y );

public class Processor

{

public Processor( double factor ) { this.factor = factor;

}

public double Compute( double x, double y ) { double result = (x+y)*factor;

Console.WriteLine( "InstanceResults: {0}", result ); return result;

}

public static double StaticCompute( double x, double y ) {

double result = (x+y)*0.5;

Console.WriteLine( "StaticResult: {0}", result ); return result;

}

private double factor;

}

public class EntryPoint

{

static void Main() {

Processor proc1 = new Processor( 0.75 ); Processor proc2 = new Processor( 0.83 );

ProcessResults delegate1 = new ProcessResults( proc1.Compute );

ProcessResults delegate2 = new ProcessResults( proc2.Compute );

ProcessResults delegate3 = new ProcessResults( Processor.StaticCompute );

double combined = delegate1( 4, 5 ) + delegate2( 6, 2 ) + delegate3( 5, 2 );

Console.WriteLine( "Output: {0}", combined );

}

}

In this example, I’ve created three delegates. Two point to instance methods, and one points to a static method. Notice that the delegates are created by creating instances of the ProcessResults type, which is the type created by the delegate declaration. When you create the delegate instances, you pass the methods they must call in the constructor. Take note of the format of the parameter. In the first two cases, you pass an instance method on the proc1 and proc2 instances. However, in the third case, you pass a method pointer on the type rather than an instance. This is the way you create a delegate that points to a static method rather than an instance method. At the point where the delegates are called, the syntax is identical and independent of whether the delegate points to an

Соседние файлы в папке c#