
C# ПІДРУЧНИКИ / c# / Apress - Accelerated C# 2005
.pdf
364 C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S
■Note Consider implementing IEquatable<T> on your type to get a type-safe version of Equals(). This is especially important for value types, since type-specific versions of methods avoid unnecessary boxing.
If You Override Equals(), Override GetHashCode() Too
GetHashCode() is called when objects are used as keys of a hash table. When a hash table searches for an entry after given a key to look for, it asks the key for its hash code and then uses that to identify which hash bucket the key lives in. Once it finds the bucket, it can then see if that key is in the bucket. Theoretically, the search for the bucket should be quick, since the buckets should have very few keys in them. This occurs if your GetHashCode method returns a reasonably unique value for instances of your object that support value equivalence semantics.
Given the previous discussion, you can see that it would be very bad if your hash code algorithm could return a different value between two instances that contain values that are equivalent. In such a case, the hash table may fail to find the bucket your key is in. For this reason, it is imperative that you override GetHashCode() if you override Equals() for an object. In fact, if you override Equals() and not GetHashCode(), the C# compiler will let you know about it with a friendly warning. And since you’re all diligent with regards to building your release code with zero warnings, you should take the compiler’s word seriously.
■Note The previous discussion should be plenty of evidence that any type used as a hash table key should be immutable. After all, the GetHashCode() value is normally computed based upon the state of the object itself. If that state changes, then the GetHashCode() result will likely change with it.
GetHashCode() implementations should adhere to the following rules:
• If, for two instances, x.Equals(y) is true, then x.GetHashCode() == y.GetHashCode().
•Hash codes generated by GetHashCode() need not be unique.
•GetHashCode() is not permitted to throw exceptions.
If two instances return the same hash code value, they must be further compared with Equals() to determine if they’re equivalent. Incidentally, if your GetHashCode method is very efficient, you can base the inequality code path of your operator!=() and operator==() implementations on it, since different hash codes for objects of the same type imply inequality. Implementing the operators this way can be more efficient in some cases, but it all depends on the efficiency of your GetHashCode() implementation and the complexity of your Equals method. In some cases, when using this technique, the calls to the operators could be less efficient than just calling Equals(), but in other cases, they can be remarkably more efficient. For example, consider an object that models a multidimensional point in space. Suppose the number of dimensions (rank) of this point could easily approach into the hundreds. Internally, you could represent the dimensions of the point by using an array of integers. Say you want to implement the GetHashCode method by computing a CRC32 on the dimension points in the array. This also implies that this Point type is immutable. This GetHashCode() call could potentially be expensive if you compute the CRC32 each time it is called. Therefore, it may be wise to precompute the hash and store it in the object. In such a case, you could write the equality operators as shown in the following code:
sealed public class Point
{
// other methods removed for clarity

C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S |
365 |
public override bool Equals( object other ) { bool result = false;
Point that = other as Point; if( that != null ) {
result = (this.coordinates == that.coordinates);
}
return result;
}
public override int GetHashCode() { return precomputedHash;
}
public static bool operator ==( Point pt1, Point pt2 ) { if( pt1.GetHashCode() != pt2.GetHashCode() ) {
return false; } else {
return Object.Equals( pt1, pt2 );
}
}
public static bool operator !=( Point pt1, Point pt2 ) { if( pt1.GetHashCode() != pt2.GetHashCode() ) {
return true; } else {
return !Object.Equals( pt1, pt2 );
}
}
private float[] coordinates; private int precomputedHash;
}
In this example, as long as the precomputed hash is sufficiently unique, the overloaded operators will execute quickly in some cases. In the worst case, one more compare between two integers—the hash values—is executed along with the function calls to acquire them. If the call to Equals() is expensive, then this optimization will return some gains on a lot of the comparisons. If the call to Equals() is not expensive, then this technique could add overhead and make the code less efficient. It’s best to apply the old adage, which states that premature optimization is poor optimization. You should only apply such an optimization after a profiler has pointed you in this direction and if you’re sure it will help.
Object.GetHashCode() exists because the developers of the Standard Library felt it would be convenient to be able to use any object as a key to a hash table. The fact is, not all objects are good candidates for hash keys. Usually, it’s best to use immutable types as hash keys. A good example of an immutable type in the Standard Library is System.String. Once created, you cannot ever change it. Therefore, calling GetHashCode() on a string instance is guaranteed to always return the same value for the same string instance. It becomes more difficult to generate hash codes for objects that are mutable. In those cases, it’s best to base your GetHashCode() implementation on calculations performed on immutable fields inside the mutable object.
Detailing algorithms for generating hash codes is outside the scope of this book. I recommend that you reference Donald E. Knuth’s The Art of Computer Programming, Volume 3: Sorting and Searching, Second Edition (Boston, MA: Addison-Wesley Professional, 1998). For the sake of example, suppose you want to implement GetHashCode() for a ComplexNumber type. One solution is to compute the hash based on the magnitude of the complex number, as in the following example:

366C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S
using System;
public sealed class ComplexNumber
{
public ComplexNumber( double real, double imaginary ) { this.real = real;
this.imaginary = imaginary;
}
public override bool Equals( object other ) { bool result = false;
ComplexNumber that = other as ComplexNumber; if( that != null ) {
result = (this.real == that.real) && (this.imaginary == that.imaginary);
}
return result;
}
public override int GetHashCode() {
return (int) Math.Sqrt( Math.Pow(this.real, 2) * Math.Pow(this.imaginary, 2) );
}
public static bool operator ==( ComplexNumber num1, ComplexNumber num2 ) { return Object.Equals(num1, num2);
}
public static bool operator !=( ComplexNumber num1, ComplexNumber num2 ) { return !Object.Equals(num1, num2);
}
// Other methods removed for clarity
private readonly double real; private readonly double imaginary;
}
The GetHashCode() algorithm is not meant as a highly efficient example. In fact, it’s not efficient at all, since it is based on nontrivial floating-point mathematical routines. Also, due to the rounding, it could potentially cause many complex numbers to fall within the same bucket. In that case, the efficiency of the hash table would degrade. I’ll leave a more efficient algorithm as an exercise to the reader. Notice that I don’t use the GetHashCode method to implement operator!=() because of the efficiency concerns. But more importantly, I rely on the static Object.Equals method to compare them for equality. This handy method checks the references for null before calling the instance Equals method, saving you from having to do that. Had I used GetHashCode() to implement operator!=(), then I would have had to check the references for null values before calling GetHashCode() on them. Also, note that both fields used to calculate the hash code are immutable. Thus, this instance of this object will always return the same hash code value as long as it lives. In fact, you may consider caching the hash code value once you compute it the first time to gain greater efficiency.
Does the Object Support Ordering?
Sometimes you’ll design a class for objects that are meant to be stored within a collection. When the objects in that collection need to be sorted, such as by calling Sort() on an ArrayList, you need

C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S |
367 |
a well-defined mechanism for comparing two objects. The pattern that the Base Class Library designers provided hinges on implementing the following IComparable interface:5
public interface IComparable
{
int CompareTo( object obj );
}
Again, another one of these interfaces merely contains one method. Thankfully, IComparable doesn’t contain the depth of pitfalls as ICloneable and IDisposable. The CompareTo method is fairly straightforward. It can return a value that is either positive, negative, or zero. Table 13-1 lists the return value meanings.
Table 13-1. Meaning of Return Values of IComparable.CompareTo()
CompareTo() Return Value |
Meaning |
||
Positive |
this > |
obj |
|
Zero |
this |
== obj |
|
Negative |
this |
< |
obj |
You should be aware of a few points when implementing IComparable.CompareTo(). First, notice that the return value specification says nothing about the actual value of the returned integer. It only defines the sign of the return values. So, to indicate a situation where this is less than obj, you can simply return -1. When your object represents a value that carries an integer meaning, an efficient way to compute the comparison value is by subtracting one from the other. It can be tempting to treat the return value as an indication of the degree of inequality. Although this is possible, I don’t recommend it, since relying on such an implementation is outside the bounds of the IComparable specification, and not all objects can be expected to do that.
Second, keep in mind that CompareTo() provides no return value definition for when two objects cannot be compared. Since the parameter type to CompareTo() is System.Object, you could easily attempt to compare an Apple instance to an Orange instance. In such a case, there is no comparison, and you’re forced to indicate such by throwing an ArgumentException object.
Finally, semantically, the IComparable interface is a superset of Object.Equals(). If you derive from an object that overrides Equals() and implements IComparable, then you’re wise to override both Equals() and reimplement IComparable in your derived class, or do neither. You want to make certain that your implementation of Equals() and CompareTo() are aligned with each other.
Based upon all of this information, a compliant IComparable interface should adhere to the following rules:
• x.CompareTo(x) must return 0. This is the reflexive property.
• |
If x.CompareTo(y) |
== |
0, then y.CompareTo(x) must equal 0. This is the symmetric property. |
• |
If x.CompareTo(y) |
== |
0, and y.CompareTo(z) == 0, then x.CompareTo(z) must equal 0. This is |
|
the transitive property. |
• If x.CompareTo(y) returns a value other than 0, then y.CompareTo(x) must return a non-0 value of the opposite sign. In other terms, this statement says that if x < y, then y > x, or if
x> y, then y < x.
5.For value types, you should consider using the generic IComparable<T> interface as shown in Chapter 10.

368C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S
•If x.CompareTo(y) returns a value other than 0, and y.CompareTo(z) returns a value other than 0 with the same sign as the first, then x.CompareTo(y) is required to return a non-0 value of the same sign as the previous two. In other terms, this statement says that if x < y and y < z, then x < z, or if x > y and y > z, then x > z.
The following code shows a modified form of the ComplexNumber class that implements IComparable and consolidates some code at the same time in some private helper methods:
using System;
public sealed class ComplexNumber : IComparable
{
public ComplexNumber( double real, double imaginary ) { this.real = real;
this.imaginary = imaginary;
}
public override bool Equals( object other ) { bool result = false;
ComplexNumber that = other as ComplexNumber; if( that != null ) {
result = InternalEquals( that );
}
return result;
}
public override int GetHashCode() { return (int) this.Magnitude;
}
public static bool operator ==( ComplexNumber num1, ComplexNumber num2 ) { return Object.Equals(num1, num2);
}
public static bool operator !=( ComplexNumber num1, ComplexNumber num2 ) { return !Object.Equals(num1, num2);
}
public int CompareTo( object other ) { ComplexNumber that = other as ComplexNumber; if( that == null ) {
throw new ArgumentException( "Bad Comparison!" );
}
int result;
if( InternalEquals(that) ) { result = 0;
}else if( this.Magnitude > that.Magnitude ) { result = 1;
}else {
result = -1;
}
return result;
}

C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S |
369 |
private bool InternalEquals( ComplexNumber that ) { return (this.real == that.real) &&
(this.imaginary == that.imaginary);
}
public double Magnitude { get {
return Math.Sqrt( Math.Pow(this.real, 2) + Math.Pow(this.imaginary, 2) );
}
}
// Other methods removed for clarity
private readonly double real; private readonly double imaginary;
}
Is the Object Formattable?
When you create a new object, or an instance of a value type for that matter, it inherits a method from System.Object called ToString(). This method accepts no parameters and simply returns a string representation of the object. In all cases, if it makes sense to call ToString() on your object, you’ll need to override this method. The default implementation provided by System.Object merely returns a string representation of the object’s type name, which of course is not useful for an object requiring a string representation based upon its internal state. You should always consider overriding Object.ToString() for all of your types, even if only for the convenience of logging the object state to a debug output log.
Object.ToString() is useful for getting a quick string representation of an object; however, it’s sometimes not useful enough. For example, consider the previous ComplexNumber example. Suppose you want to provide a ToString() override for that class. An obvious implementation would output the complex number as an ordered pair within a pair of parentheses such as “(1, 2)” for example. However, the real and imaginary components of ComplexNumber are of type double. Also, floating-point numbers don’t always appear the same across all cultures. Americans use a period to separate the fractional element of a floating-point number, whereas most Europeans use a comma. This problem is solved easily if you utilize the default culture information attached to the thread. By accessing the System.Threading.Thread.CurrentThread.CurrentCulture property, you can get references to the default cultural information detailing how to represent numerical values, including monetary amounts, as well as information on how to represent time and date values.
■Note I cover globalization and cultural information in greater detail in Chapter 8.
By default, the CurrentCulture property gives you access to System.Globalization. DateTimeFormatInfo and System.Globalization.NumberFormatInfo. Using the information provided by these objects, you can output the ComplexNumber in a form that is appropriate for the default culture of the machine the application is running on. Check out Chapter 8 for an example of how this works.
That solution seems easy enough. However, you must realize that there are times where using the default culture is not sufficient, and a user of your objects may need to specify which culture to use. Not only that, the user may want to specify the exact formatting of the output. For example, a user may prefer to say that the real and imaginary portions of a ComplexNumber instance should be displayed with only five significant digits while using the German cultural information. If you develop software

370C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S
for servers, you know that you need this capability. A company that runs a financial services server in the United States and services requests from Japan will want to display Japanese currency in the format customary for the Japanese culture. You need to specify how to format an object when it is converted to a string via ToString() without having to change the CurrentCulture on the thread beforehand.
In fact, the Standard Library provides an interface for doing just that. When a class or struct needs the capability to respond to such requests, it implements the IFormattable interface. The following code shows the simple-looking IFormattable interface. However, don’t be fooled by its simplistic looks, because depending on the complexity of your object, it may be tricky to implement:
public interface IFormattable
{
string ToString( string format, IFormatProvider formatProvider );
}
Let’s consider the second parameter first. If the client passes null for formatProvider, you should default to using the culture information attached to the current thread as previously described. However, if formatProvider is not null, you’ll need to acquire the formatting information from the provider via the IFormatProvider.GetFormat method. IFormatProvider looks like this:
public interface IFormatProvider
{
object GetFormat( Type formatType );
}
In an effort to be as generic as possible, the designers of the Standard Library designed GetFormat() to accept an object of type System.Type. Thus, it is extensible as to what types the object that implements IFormatProvider may support. This flexibility is handy if you intend to develop custom format providers that need to return as-of-yet-undefined formatting information.
The Standard Library provides a System.Globalization.CultureInfo type that will most likely suffice for all of your needs. The CultureInfo object implements the IFormatProvider interface, and you can pass instances of it as the second parameter to IFormattable.ToString(). Soon, I’ll show an example of its usage when I make modifications to the ComplexNumber example, but first, let’s look at the first parameter to ToString().
The format parameter of ToString() allows you to specify how to format a specific number. The format provider can describe how to display a date or how to display currency based upon cultural preferences, but you still need to know how to format the object in the first place. All of the types within the Standard Library, such as Int32, support the standard format specifiers as described under “Standard Numeric Format Strings” in the MSDN. In a nutshell, the format string consists of a single letter specifying the format, and then an optional number between 0 and 99 that declares the precision. For example, you can specify that a double be output as a 5 significant digit floatingpoint number with F5. Not all types are required to support all formats except for one—the G format— which stands for “general.” In fact, the G format is what you get when you call the parameterless Object.ToString() on most objects in the Standard Library. Some types will ignore the format specification in special circumstances. For example, a System.Double can contain special values that represent NaN (Not a Number), PositiveInfinity, or NegativeInfinity. In such cases, System.Double ignores the format specification and displays a symbol appropriate for the culture as provided by
NumberFormatInfo.
The format specifier may also consist of a custom format string. Custom format strings allow the user to specify the exact layout of numbers as well as mixed-in string literals and so on by using the syntax described under “Custom Numeric Format String” in the MSDN. The client can specify one format for negative numbers, another for positive numbers, and a third for zero values. I won’t spend any time detailing these various formatting capabilities. Instead, I encourage you to reference the MSDN material for detailed information regarding them.

C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S |
371 |
As you can see, implementing IFormattable.ToString() can be quite a tedious experience, especially since your format string could be highly customized. However, in many cases—and the ComplexNumber example is one of those cases—you can rely upon the IFormattable implementations of standard types. Since ComplexNumber uses System.Double to represent its real and imaginary parts, you can defer most of your work to the implementation of IFormattable on System.Double. Let’s look at modifications to the ComplexNumber example to support IFormattable. Assume that the ComplexNumber type will accept a format string exactly the same way that System.Double does and that each component of the complex number will be output using this same format. Of course, a better implementation may provide more capabilities such as allowing you to specify whether the output should be in Cartesian or polar format, but I’ll leave that to you as an exercise:
using System;
using System.Globalization;
public sealed class ComplexNumber : IFormattable
{
public ComplexNumber( double real, double imaginary ) { this.real = real;
this.imaginary = imaginary;
}
public override string ToString() { return ToString( "G", null );
}
// IFormattable implementation
public string ToString( string format,
IFormatProvider formatProvider ) { string result = "(" +
real.ToString(format, formatProvider) + " " +
real.ToString(format, formatProvider) + ")";
return result;
}
// Other methods removed for clarity
private readonly double real; private readonly double imaginary;
}
public sealed class EntryPoint
{
static void Main() {
ComplexNumber num1 = new ComplexNumber( 1.12345678, 2.12345678 );
Console.WriteLine( "US format: {0}", num1.ToString( "F5",
new CultureInfo("en-US") ) ); Console.WriteLine( "DE format: {0}",
num1.ToString( "F5",
new CultureInfo("de-DE") ) ); Console.WriteLine( "Object.ToString(): {0}",
num1.ToString() );
}
}

372 C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S
Here’s the output from running the previous example:
US format: (1.12346 1.12346)
DE format: (1,12346 1,12346)
Object.ToString(): (1.12345678 1.12345678)
In Main(), notice the creation and use of two different CultureInfo instances. First, the ComplexNumber is output using American cultural formatting, and second, using German cultural formatting. In both cases, I specify to output the string using only five significant digits. You will see that System.Double’s implementation of IFormattable.ToString() even rounds the result as expected. Finally, you can see that the Object.ToString() override is implemented to defer to the IFormattable. ToString method using the G (general) format.
IFormattable provides the clients of your objects with powerful capabilities when they have specific formatting needs for your objects. However, that power comes at an implementation cost. Implementing IFormattable.ToString() can be a very detail-oriented task that takes a lot of time and attentiveness.
Is the Object Convertible?
The C# compiler provides support for converting instances of simple built-in value types, such as int and long, from one type to another via casting by generating IL code that uses the conv IL instruction. The conv instruction works well for the simple built-in types, but what do you do when you want to convert a string to an integer or vice versa? The compiler cannot do this for you automatically, because such conversions are potentially complex and even require parameters, such as cultural information, when converting numbers into strings and vice versa.
The .NET Framework provides several ways to get the job done. For nontrivial conversions that you cannot do with casting, you should rely upon the System.Convert class. I won’t list the functions that Convert implements here, as the list is extremely long. I encourage you to look it up in the MSDN library. The Convert class contains methods to convert from just about any built-in type to another as long as it makes sense. So, if you want to convert a double to a String, you would simply call the ToString static method, passing it the double as follows:
static void Main()
{
double d = 12.1;
string str = Convert.ToString( d );
}
In similar form to IFormattable.ToString(), Convert.ToString() has various overloads that also allow you to pass a CultureInfo object or any other object that supports IFormatProvider, in order to specify cultural information when doing the conversion. You can use other methods as well, such as ToBoolean() and ToUint32(). The general pattern of the method names is obviously ToXXX(), where XXX is the type you’re converting to. System.Convert even has methods to convert byte arrays to and from base64-encoded strings. If you store any binary data in XML text or any other text-based medium, you’ll find these methods very handy.
Convert will generally serve most of your conversion needs between built-in types. It’s a one-stop shop for converting an object of one type to another. You can see this just by looking at the wealth of methods that it supports. However, what happens when your conversion involves a custom type that Convert doesn’t know about? The answer lies in the Convert.ChangeType method.
ChangeType() is System.Convert’s extensibility mechanism. It has several overloads, including some that takes a format provider for cultural information. However, the general idea is that it takes an object reference and converts it to the type represented by the passed-in System.Type object. Consider the following code that uses the ComplexNumber from previous examples and tries to convert it into a string using System.Convert.ChangeType():

C H A P T E R 1 3 ■ I N S E A R C H O F C # C A N O N I C A L F O R M S |
373 |
using System;
public sealed class ComplexNumber
{
public ComplexNumber( double real, double imaginary ) { this.real = real;
this.imaginary = imaginary;
}
// Other methods removed for clarity
private readonly double real; private readonly double imaginary;
}
public sealed class EntryPoint
{
static void Main() {
ComplexNumber num1 = new ComplexNumber( 1.12345678, 2.12345678 );
string str =
(string) Convert.ChangeType( num1, typeof(string) );
}
}
You’ll find that the code compiles just fine. However, you’ll get a surprise at run time when you find that it throws an InvalidCastException with the message, “Object must implement IConvertible.” Even though ChangeType() is System.Convert’s extensibility mechanism, extensibility doesn’t come for free. You must do some work to make ChangeType() work with ComplexNumber. And, as you probably guessed, the work required is to implement the IConvertible interface.
The IConvertible interface is the last defense when it comes to converting objects. If you want your custom objects to play nice with System.Convert and the types of conversions the user may desire to perform, then you had better implement IConvertible. As with System.Convert, I won’t list the IConvertible methods here, since there are quite a few of them. I encourage you to look them up in the MSDN documentation. You’ll see one method for converting to each of the built-in types. In addition, Convert uses a catch-all method, IConvertible.ToType(), to convert one custom type to another custom type. Also, the IConvertible methods accept a format provider so that you can provide cultural information to the conversion method.
Remember, when you implement an interface, you’re required to provide implementations for all of the interface’s methods. However, if a particular conversion makes no sense for your object, then you can throw an InvalidCastException. Naturally, your implementation will most definitely throw an exception inside IConvertible.ToType() for any generic type that it doesn’t support conversion to.
To sum up, it may appear that there are many ways to convert one type to another in C#, and in fact, there are. However, the general rule of thumb is to rely on System.Convert when casting won’t do the trick. Moreover, your custom objects, such as the ComplexNumber class, should implement IConvertible so they can work in concert with the System.Convert class.
C# offers conversion operators that allow you to do essentially the same thing you can do by implementing IConvertible. However, C# implicit and explicit conversion operators aren’t CLS-compliant. Therefore, not every language that consumes your C# code may call them to do the conversion. It is recommended that you not rely on them exclusively to handle conversion. Of course, if your project is coded using .NET languages that do support conversion operators, then you can use them exclusively, but it’s recommended that you also support IConvertible.