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

Pro CSharp 2008 And The .NET 3.5 Platform [eng]-1

.pdf
Скачиваний:
23
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

292 CHAPTER 9 WORKING WITH INTERFACES

However, if you would prefer to hide the functionality of IEnumerable from the object level, simply make use of explicit interface implementation:

IEnumerator IEnumerable.GetEnumerator()

{

// Return the array object's IEnumerator. return carArray.GetEnumerator();

}

By doing so, the causal object user will not find the Garage’s GetEnumerator() method, while the foreach construct will obtain the interface in the background when necessary.

Source Code The CustomEnumerator project is located under the Chapter 9 subdirectory.

Building Iterator Methods with the yield Keyword

Historically, when you wished to build a custom collection (such as Garage) that supported foreach enumeration, implementing the IEnumerable interface (and possibly the IEnumerator interface) was your only option. However, since the release of .NET 2.0, we are provided with an alternative way to build types that work with the foreach loop via iterators.

Simply put, an iterator is a member that specifies how a container’s internal items should be returned when processed by foreach. While the iterator method must still be named GetEnumerator(), and the return value must still be of type IEnumerator, your custom class does not need to implement any of the expected interfaces.

To illustrate, create a new Console Application project named CustomEnumeratorWithYield and insert the Car, Radio, and Garage types from the previous example (again, renaming your namespace definitions to the current project if you so choose). Now, retrofit the current Garage type as follows:

public class Garage

{

private Car[] carArray = new Car[4];

...

// Iterator method.

public IEnumerator GetEnumerator()

{

foreach (Car c in carArray)

{

yield return c;

}

}

}

Notice that this implementation of GetEnumerator() iterates over the subitems using internal foreach logic and returns each Car to the caller using the yield return syntax. The yield keyword is used to specify the value (or values) to be returned to the caller’s foreach construct. When the yield return statement is reached, the current location is stored, and execution is restarted from this location the next time the iterator is called.

Iterator methods are not required to make use of the foreach keyword to return its contents. It is also permissible to define this iterator method as follows:

public IEnumerator GetEnumerator()

{

yield return carArray[0];

CHAPTER 9 WORKING WITH INTERFACES

293

yield return carArray[1]; yield return carArray[2]; yield return carArray[3];

}

In this implementation, notice that the GetEnumerator() method is explicitly returning a new value to the caller with each pass through. Doing so for this example makes little sense, given that if we were to add more objects to the carArray member variable, our GetEnumerator() method would now be out of sync. Nevertheless, this syntax can be useful when you wish to return local data from a method for processing by the foreach syntax.

Building a Named Iterator

It is also interesting to note that the yield keyword can technically be used within any method, regardless of its name. These methods (which are technically called named iterators) are also unique in that they can take any number of arguments. When building a named iterator, be very aware that the method will return the IEnumerable interface, rather than the expected IEnumerator- compatible type. To illustrate, we could add the following method to the Garage type:

public IEnumerable GetTheCars(bool ReturnRevesed)

{

// Return the items in reverse. if (ReturnRevesed)

{

for (int i = carArray.Length; i != 0; i--)

{

yield return carArray[i-1];

}

}

else

{

// Return the items as placed in the array. foreach (Car c in carArray)

{

yield return c;

}

}

}

Notice that our new method allows the caller to obtain the subitems in a sequential order, as well as in reverse order, if the incoming parameter has the value true. We could now interact with our new method as follows:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with the Yield Keyword *****\n");

Garage carLot = new Garage();

// Get items using GetEnumerator(). foreach (Car c in carLot)

{

Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed);

}

Console.WriteLine();

294CHAPTER 9 WORKING WITH INTERFACES

//Get items (in reverse!) using named iterator. foreach (Car c in carLot.GetTheCars(true))

{

Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed);

}

Console.ReadLine();

}

Named iterators are helpful constructs, in that a single custom container can define multiple ways to request the returned set.

Internal Representation of an Iterator Method

When the C# compiler encounters an iterator method, it will dynamically generate a nested class definition within the scope of the defining type (Garage in this case). The autogenerated nested class implements the GetEnumerator(), MoveNext(), and Current members on your behalf (oddly, the Reset() method is not, and you will receive a runtime exception if you attempt to call it). If you were to load the current application into ildasm.exe, you would find two nested types, each of which accounts for the logic required by a specific iterator method. Notice in Figure 9-10 that these compiler-generated types have been named <GetEnumerator>d__0 and <GetTheCars>d__5.

Figure 9-10. Iterator methods are internally implemented with the help of an autogenerated nested class.

If you used ildasm.exe to view the implementation of the GetEnumerator() method of the Garage type, you’d find that it has been implemented to make use of the <GetEnumerator>d__0 type behind the scenes (the nested <GetTheCars>d__5 type is used by the GetTheCars() method in a similar manner).

.method public hidebysig instance class [mscorlib]System.Collections.IEnumerator

GetEnumerator() cil managed

{

...

newobj instance void CustomEnumeratorWithYield.Garage/'<GetEnumerator>d__0'::.ctor(int32)

...

} // end of method Garage::GetEnumerator

CHAPTER 9 WORKING WITH INTERFACES

295

So, to wrap up our look at building enumerable objects, remember that in order for your custom types to work with the C# foreach keyword, the container must define a method named GetEnumerator(), which has been formalized by the IEnumerable interface type. The implementation of this method is typically achieved by simply delegating it to the internal member that is holding onto the subobjects; however, it is also possible to make use of the yield return syntax to provide multiple “named iterator” methods.

Source Code The CustomEnumeratorWithYield project is located under the Chapter 9 subdirectory.

Building Cloneable Objects (ICloneable)

As you recall from Chapter 6, System.Object defines a member named MemberwiseClone(). This method is used to obtain a shallow copy of the current object. Object users do not call this method directly (as it is protected); however, a given object may call this method itself during the cloning process. To illustrate, create a new Console Application named CloneablePoint that defines a class named Point:

// A class named Point. public class Point

{

//Public for easy access. public int x, y;

public Point(int x, int y) { this.x = x; this.y = y;} public Point(){}

//Override Object.ToString().

public override string ToString()

{return string.Format("X = {0}; Y = {1}", x, y ); }

}

Given what you already know about reference types and value types (Chapter 4), you are aware that if you assign one reference variable to another, you have two references pointing to the same object in memory. Thus, the following assignment operation results in two references to the same Point object on the heap; modifications using either reference affect the same object on the heap:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Object Cloning *****\n");

// Two references to same object!

Point p1 = new Point(50, 50); Point p2 = p1;

p2.x = 0; Console.WriteLine(p1); Console.WriteLine(p2); Console.ReadLine();

}

When you wish to equip your custom types to support the ability to return an identical copy of itself to the caller, you may implement the standard ICloneable interface. As shown at the start of this chapter, this type defines a single method named Clone():

296CHAPTER 9 WORKING WITH INTERFACES

public interface ICloneable

{

object Clone();

}

Note The usefulness of the ICloneable interface is currently under debate within the .NET community. The problem has to do with the fact that the official specification does not explicitly say that objects implementing this interface must return a deep copy of the object (i.e., internal reference types of an object result in brand-new objects with identical state). Thus, it is technically possible that objects implementing ICloneable actually return a shallow copy of the interface (i.e., internal references point to the same object on the heap), which clearly generates a good deal of confusion. In our example, I am assuming we are implementing Clone() to return a full, deep copy of the object.

Obviously, the implementation of the Clone() method varies between objects. However, the basic functionality tends to be the same: copy the values of your member variables into a new object instance of the same type, and return it to the user. To illustrate, ponder the following update to the Point class:

// The Point now supports "clone-ability." public class Point : ICloneable

{

public int x, y; public Point(){ }

public Point(int x, int y) { this.x = x; this.y = y;}

// Return a copy of the current object. public object Clone()

{return new Point(this.x, this.y); }

public override string ToString()

{return string.Format("X = {0}; Y = {1}", x, y ); }

}

In this way, you can create exact stand-alone copies of the Point type, as illustrated by the following code:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Object Cloning *****\n");

//Notice Clone() returns a generic object type.

//You must perform an explicit cast to obtain the derived type.

Point p3 = new Point(100, 100); Point p4 = (Point)p3.Clone();

//Change p4.x (which will not change p3.x).

p4.x = 0;

// Print each object.

Console.WriteLine(p3);

Console.WriteLine(p4);

Console.ReadLine();

}

CHAPTER 9 WORKING WITH INTERFACES

297

While the current implementation of Point fits the bill, you can streamline things just a bit. Because the Point type does not contain any internal reference type variables, you could simplify the implementation of the Clone() method as follows:

public object Clone()

{

// Copy each field of the Point member by member. return this.MemberwiseClone();

}

Be aware, however, that if the Point did contain any reference type member variables, MemberwiseClone() will copy the references to those objects (aka a shallow copy). If you wish to support a true deep copy, you will need to create a new instance of any reference type variables during the cloning process. Let’s see an example.

A More Elaborate Cloning Example

Now assume the Point class contains a reference type member variable of type PointDescription. This class maintains a point’s friendly name as well as an identification number expressed as a System.Guid (if you don’t come from a COM background, know that a globally unique identifier [GUID] is a statistically unique 128-bit number). Here is the implementation:

// This class describes a point. public class PointDescription

{

// Exposed publicly for simplicity. public string petName;

public Guid pointID;

public PointDescription()

{

this.petName = "No-name"; pointID = Guid.NewGuid();

}

}

The initial updates to the Point class itself included modifying ToString() to account for these new bits of state data, as well as defining and creating the PointDescription reference type. To allow the outside world to establish a pet name for the Point, you also update the arguments passed into the overloaded constructor:

public class Point : ICloneable

{

public int x, y;

public PointDescription desc = new PointDescription();

public Point(){}

public Point(int x, int y)

{

this.x = x; this.y = y;

}

public Point(int x, int y, string petname)

{

this.x = x; this.y = y;

desc.petName = petname;

}

298 CHAPTER 9 WORKING WITH INTERFACES

public object Clone()

{ return this.MemberwiseClone(); }

public override string ToString()

{

return string.Format("X = {0}; Y = {1}; Name = {2};\nID = {3}\n", x, y, desc.petName, desc.pointID);

}

}

Notice that you did not yet update your Clone() method. Therefore, when the object user asks for a clone using the current implementation, a shallow (member-by-member) copy is achieved. To illustrate, assume you have updated Main() as follows:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Object Cloning *****\n");

Console.WriteLine("Cloned p3 and stored new Point in p4"); Point p3 = new Point(100, 100, "Jane");

Point p4 = (Point)p3.Clone();

Console.WriteLine("Before modification:"); Console.WriteLine("p3: {0}", p3); Console.WriteLine("p4: {0}", p4); p4.desc.petName = "My new Point";

p4.x = 9;

Console.WriteLine("\nChanged p4.desc.petName and p4.x"); Console.WriteLine("After modification:"); Console.WriteLine("p3: {0}", p3); Console.WriteLine("p4: {0}", p4);

Console.ReadLine();

}

Figure 9-11 shows the output. Notice that while the value types have indeed been changed, the internal reference types maintain the same values, as they are “pointing” to the same objects in memory.

Figure 9-11. MemberwiseClone() returns a shallow copy of the current object.

CHAPTER 9 WORKING WITH INTERFACES

299

In order for your Clone() method to make a complete deep copy of the internal reference types, you need to configure the object returned by MemberwiseClone() to account for the current point’s name (the System.Guid type is in fact a structure, so the numerical data is indeed copied). Here is one possible implementation:

// Now we need to adjust for the PointDescription member. public object Clone()

{

// First get a shallow copy.

Point newPoint = (Point)this.MemberwiseClone();

// Then fill in the gaps.

PointDescription currentDesc = new PointDescription(); currentDesc.petName = this.desc.petName; newPoint.desc = currentDesc;

return newPoint;

}

If you rerun the application once again as shown in Figure 9-12, you see that the Point returned from Clone() does copy its internal reference type member variables (note the pet name is now unique for both p3 and p4).

Figure 9-12. Now you have a true deep copy of the object.

To summarize the cloning process, if you have a class or structure that contains nothing but value types, implement your Clone() method using MemberwiseClone(). However, if you have a custom type that maintains other reference types, you need to establish a new type that takes into account each reference type member variable.

Source Code The CloneablePoint project is located under the Chapter 9 subdirectory.

Building Comparable Objects (IComparable)

The System.IComparable interface specifies a behavior that allows an object to be sorted based on some specified key. Here is the formal definition:

300CHAPTER 9 WORKING WITH INTERFACES

//This interface allows an object to specify its

//relationship between other like objects. public interface IComparable

{

int CompareTo(object o);

}

Let’s assume you have a new Console Application named ComparableCar that defines the following updated Car type (notice that we have basically just added a new member variable to represent a unique ID for each car as well as ways to get and set the value):

public class Car

{

...

private int carID; public int ID

{

get { return carID; } set { carID = value; }

}

public Car(string name, int currSp, int id)

{

currSpeed = currSp; petName = name; carID = id;

}

...

}

Now assume you have an array of Car types as follows:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Object Sorting *****\n");

// Make an array of Car types.

Car[] myAutos = new Car[5]; myAutos[0] = new Car("Rusty", 80, 1); myAutos[1] = new Car("Mary", 40, 234); myAutos[2] = new Car("Viper", 40, 34); myAutos[3] = new Car("Mel", 40, 4); myAutos[4] = new Car("Chucky", 40, 5); Console.ReadLine();

}

The System.Array class defines a static method named Sort(). When you invoke this method on an array of intrinsic types (int, short, string, etc.), you are able to sort the items in the array in numeric/alphabetic order as these intrinsic data types implement IComparable. However, what if you were to send an array of Car types into the Sort() method as follows?

// Sort my cars?

Array.Sort(myAutos);

If you run this test, you would find that an ArgumentException exception is thrown by the runtime, with the following message:

"At least one object must implement IComparable."

CHAPTER 9 WORKING WITH INTERFACES

301

When you build custom types, you can implement IComparable to allow arrays of your types to be sorted. When you flesh out the details of CompareTo(), it will be up to you to decide what the baseline of the ordering operation will be. For the Car type, the internal carID seems to be the logical candidate:

//The iteration of the Car can be ordered

//based on the CarID.

public class Car : IComparable

{

...

// IComparable implementation.

int IComparable.CompareTo(object obj)

{

Car temp = (Car)obj; if(this.carID > temp.carID)

return 1;

if(this.carID < temp.carID) return -1;

else return 0;

}

}

As you can see, the logic behind CompareTo() is to test the incoming type against the current instance based on a specific point of data. The return value of CompareTo() is used to discover whether this type is less than, greater than, or equal to the object it is being compared with (see Table 9-1).

Table 9-1. CompareTo() Return Values

CompareTo() Return Value

Meaning in Life

Any number less than zero

This instance comes before the specified object in the sort

 

order.

Zero

This instance is equal to the specified object.

Any number greater than zero

This instance comes after the specified object in the sort order.

 

 

We can streamline the previous implementation of CompareTo() given the fact that the C# int data type (which is just a shorthand notation for the CLR System.Int32) implements IComparable; you could implement the Car’s CompareTo() as follows:

int IComparable.CompareTo(object obj)

{

Car temp = (Car)obj;

return this.carID.CompareTo(temp.carID);

}

In either case, so that your Car type understands how to compare itself to like objects, you can write the following user code:

// Exercise the IComparable interface. static void Main(string[] args)

{

//Make an array of Car types.

...

//Display current array.

Console.WriteLine("Here is the unordered set of cars:");