Overriding object Members


Overriding object Members

Chapter 6 discussed how all types derive from object. In addition, it reviewed each method available on object and discussed how some of them are virtual. This section discusses the details concerning overloading the virtual methods.

Overriding ToString()

By default, calling ToString() on any object will return the fully qualified name of the class. Calling ToString() on a System.IO.FileStream object will return the string System.IO.FileStream, for example. For some classes, however, ToString() can be more meaningful. On string, for example, ToString() returns the string value itself. Similarly, returning a Contact's name would make more sense. Listing 9.1 overrides ToString() to return a string representation of Coordinate.

Listing 9.1. Overriding ToString()

 public struct Coordinate  {    public Coordinate(Longitude longitude, Latitude latitude)     {          _Longitude = longitude;          _Latitude = latitude;     }   public readonly Longitude Longitude;   public readonly Latitude Latitude;   public override string ToString()                                     {                                                                           return string .Format("{0} {1}", Longitude, Latitude);            }                                                                      // ... }

Write methods such as Console.WriteLine() call an object's ToString() method, so overloading it often outputs more meaningful information than the default implementation.

Overriding GetHashCode()

Overriding GetHashCode() is more complex than overriding ToString(). Regardless, you should override GetHashCode() when you are overriding Equals(), and there is a compiler warning to indicate this. Overriding GetHashCode() is also a good practice when using it as a key into a hash table collection (System.Collections.Hashtable and System.Collections.Generic.Dictionary, for example).

The purpose of the hash code is to generate a number that corresponds to the value of an object. Here are some implementation principles for a good GetHashCode() implementation.

  • The values returned should be mostly unique. Since hash code returns only an int, there has to be an overlap in hash codes for objects that have potentially more values than an int can holdvirtually all types. (An obvious example is long, since there are more possible long values than an int could uniquely identify.)

  • The possible hash code values should be distributed evenly over the range of an int. For example, creating a hash that doesn't consider the fact that distribution of a string in Latin-based languages primarily centers on the initial 128 ASCII characters would result in a very uneven distribution of string values and would not be a strong GetHashCode() algorithm.

  • GetHashCode() should be optimized for performance. GetHashCode() is generally used in Equals() implementations to short-circuit a full equals comparison if the hash codes are different. As a result, it is frequently called when the type is used as a key type in dictionary collections.

  • GetHashCode()'s returns over the life of a particular object should be constant (the same value), even if the objects data changes. In many cases, you should cache the method return to enforce this.

  • Equal objects must have equal hash codes (if a.Equals(b), then a.GetHashCode() == b.GetHashCode()).

  • GetHashCode() should not throw any exceptions.

Consider the GetHashCode() implementation for the Coordinate type shown in Listing 9.2.

Listing 9.2. Implementing GetHashCode()

 public struct Coordinate {  public Coordinate(Longitude longitude, Latitude latitude)  {       _Longitude = longitude;       _Latitude = latitude;  }  public readonly Longitude Longitude;  public readonly Latitude Latitude;  public override int  GetHashCode()                                         {                                                         int hashCode = Longitude.GetHashCode();                                //   As long as the hash codes are not equal                                            if(Longitude != Latitude)                                             {                                                               hashCode ^= Latitude.GetHashCode(); // eXclusive OR                }                                                            return hashCode;        }                                                          // ... } 

Generally, the key is to use the XOR operator over the hash codes from the relevant types, and to make sure the XOR operands are not identical, or else the result will be all zeroes. The alternative operands, AND and OR, have similar restrictions, but the restrictions occur more frequently. Applying AND multiple times tends toward all 0 bits, and applying OR tends toward all 1 bits.

For finer-grained control, split larger-than-int types using the shift operator. For example, GetHashCode() for a long called value is implemented as follows:

int GetHashCode() { return ((int)value ^ (int)(value >> 32)) };


Also, note that if the base class is not object, then base.GetHashCode() should be included in the XOR assignment.

Finally, Coordinate does not cache the value of the hash code. Since each field in the hash code calculation is readonly, the value can't change. However, implementations should cache the hash code if calculated values could change or if a cached value could offer a significant performance advantage.

Overriding Equals()

Overriding Equals() without overriding GetHashCode() results in a warning such as that shown in Output 9.1.

Output 9.1.

warning CS0659: '<Class Name>' overrides Object.Equals(object o) but does not override Object.GetHashCode()

Generally, programmers expect overriding Equals() to be trivial, but it includes a surprising number of subtleties that require careful thought and testing.

Object Identity versus Equal Object Values

Two references are identical if both refer to the same instance. object, and therefore, all objects, include a static method called ReferenceEquals() that explicitly checks for this object identity (see Figure 9.1).

Figure 9.1. Identity


However, identical reference is not the only type of equality. Two object instances can also be equal if the values that identify them are equal. Consider the comparison of two ProductSerialNumbers shown in Listing 9.3.

Listing 9.3. Equal

public sealed class ProductSerialNumber {  // See Appendix B } _______________________________________________________________________________ _______________________________________________________________________________ class Program {  static void Main()  {      ProductSerialNumber serialNumber1 =           new ProductSerialNumber("PV", 1000, 09187234);      ProductSerialNumber serialNumber2 = serialNumber1;      ProductSerialNumber serialNumber3 =           new ProductSerialNumber("PV", 1000, 09187234);      // These serial numbers ARE the same object identity.      if(!ProductSerialNumber.ReferenceEquals(serialNumber1,           serialNumber2))      {           throw new Exception("serialNumber1 does NOT " + "reference equal serialNumber2");      }      // and, therefore, they are equal      else if(!serialNumber1.Equals(serialNumber2))      {           throw new Exception("serialNumber1 does NOT equal serialNumber2");      }      else      {           Console.WriteLine("serialNumber1 reference equals serialNumber2");           Console.WriteLine("serialNumber1 equals serialNumber2");      }      // These serial numbers are NOT the same object identity.      if (ProductSerialNumber.ReferenceEquals(serialNumber1,  serialNumber3))      {           throw new Exception(           "serialNumber1 DOES reference " +           "equal serialNumber3");  }  // but they are equal (assuming Equals is overloaded).  else if(!serialNumber1.Equals(serialNumber3) ||  serialNumber1 != serialNumber3)  {  throw new Exception(  "serialNumber1 does NOT equal serialNumber3");  }  Console.WriteLine( "serialNumber1 equals serialNumber3" );  Console.WriteLine( "serialNumber1 == serialNumber3" );  } }

The results of Listing 9.3 appear in Output 9.2.

Output 9.2.

serialNumber1 reference equals and Equals serialNumber2 serialNumber1 equals serialNumber3 serialNumber1 == serialNumber3

As the last assertion demonstrates with ReferenceEquals(), serialNumber1 and serialNumber3 are not the same reference. However, the code constructs them with the same values and both logically associate with the same physical product. If one instance was created from data in the database and another was created from manually entered data, you would expect the instances would be equal and, therefore, that the product would not be duplicated (re-entered) in the database. Two identical references are obviously equal; however, two different objects could be equal but not reference equal. Such objects will not have identical object identities, but they may have key data that identifies them as being equal objects.

Only reference types can be reference equal, thereby supporting the concept of identity. Calling ReferenceEquals() on value types will always return false since, by definition, the value type directly contains its data, not a reference. Even when ReferenceEquals() passes the same variable in both (value type) parameters to ReferenceEquals(), the result will still be false because the very nature of value types is that they are copied into the parameters of the called method. Listing 9.4 demonstrates the behavior.

Listing 9.4. Value Types Do Not Even Reference Equal Themselves

public struct Coordinate {   public readonly Longitude Longitude;   public readonly Latitude Latitude;   // ... } ______________________________________________________________________________ ______________________________________________________________________________ class Program {   public void Main()   {       //...       Coordinate coordinate1 = new Coordinate( new Longitude(48, 52),  new Latitude(-2, -20));       // Value types will never be reference equal.  if ( Coordinate.ReferenceEquals(coordinate1,  coordinate1) )       {       throw new Exception("coordinate1 reference equals coordinate1");       }       Console.WriteLine("coordinate1 does NOT reference equal itself" );  } }

In contrast to the definition of Coordinate as a reference type in Chapter 8, the definition going forward is that of a value type (struct) because the combination of Longitude and Latitude data is less than 16 bytes. (In Chapter 8, Coordinate aggregated Angle rather than Longitude and Latitude.)

Implementing Equals()

To determine whether two objects are equal (the same identifying data), you use an object's Equal() method. The implementation of this virtual method on object uses ReferenceEquals() to evaluate equality. Since this implementation is often inadequate, it is necessary to sometimes override Equals() with a more appropriate implementation.

For objects to equal each other, the expectation is that the identifying data within them be equal. For ProductSerialNumbers, for example, the ProductSeries, Model, and Id must be the same; however, for an Employee object, perhaps comparing EmployeeIds would be sufficient for equality. To correct object.Equals() implementation, it is necessary to override it. Value types, for example, override the Equals() implementation to instead use the fields that the type includes.

The steps for overriding Equals() are as follows.

1.

Check for null if type is nullable (i.e., a reference type).

2.

Check for reference equals if the data is a reference type.

3.

Check for equivalent data types.

4.

Possibly check for equivalent hash codes to short-circuit an extensive, field-by-field comparison.

5.

Check base.Equals() if base class overrides Equals().

6.

Compare each identifying field for equality.

7.

Override GetHashCode().

8.

Override the == and != operators (see the next section).

Listing 9.5 shows a sample Equals() implementation.

Listing 9.5. Overriding Equals()

public struct Longitude {   // ... } _______________________________________________________________________________ _______________________________________________________________________________ public struct Latitude {   // ... } _______________________________________________________________________________ _______________________________________________________________________________ public struct Coordinate {   public Coordinate(Longitude longitude, Latitude latitude)   {       _Longitude = longitude;       _Latitude = latitude;   }  public readonly Longitude Longitude; public readonly Latitude Latitude; public override bool Equals(object obj) {      // STEP 1: Check for null      if (obj == null)      {           return false;      }      // STEP 3: equivalent data types      if (this.GetType() != obj.GetType())      {           return false;      }      return Equals((Coordinate)obj); } public bool Equals(Coordinate obj) {      // STEP 1: Check for null if nullable      // (e.g., a reference type)      // if (obj == null)      // {      //     return false;      // }      // STEP 2: Check for ReferenceEquals if this      // is a reference type      // if ( ReferenceEquals(this, obj))      // {      //     return true;      // }      // STEP 4: Possibly check for equivalent hash codes      // if (this.GetHashCode() != obj.GetHashCode())      // {      //     return false;      // }      // STEP 5: Check base.Equals if base overrides Equals()      // System.Diagnostics.Debug.Assert(      // base.GetType() != typeof(object));      // if ( !base.Equals(obj) )      // {      //     return false;      // }      // STEP 6: Compare identifying fields for equality.      return ( (Longitude.Equals(obj.Longitude)) && (Latitude.Equals(obj.Latitude)) );      }      // STEP 7: Override GetHashCode.      public override int GetHashCode()      {           int hashCode = Longitude.GetHashCode();           hashCode ^= Latitude.GetHashCode(); // Xor (eXclusive OR)           return hashCode;      } }

In this implementation, the first two checks are relatively obvious. Checks 46 occur in an overload of Equals() that takes the Coordinate data type specifically. This way, a comparison of two Coordinates will avoid Equals(object obj) and its GetType() check altogether.

Since GetHashCode() is not cached and is no more efficient than step 5, the GetHashCode() comparison is commented out. Similarly, base.Equals() is not used since the base class is not overriding Equals(). (The assertion checks that base is not of type object, however it does not check that the base class overrides Equals(), which is required to appropriately call base.Equals().) Regardless, since GetHashCode() does not necessarily return a unique value (it only identifies when operands are different), on its own it does not conclusively identify equal objects.

Like GetHashCode(), Equals() should also never throw any exceptions. It is valid to compare any object with any other object, and doing so should never result in an exception.

Guidelines for Implementing Equality

While learning the details for overriding an object's virtual members, several guidelines emerge.

  • Equals(), the == operator, and the != operator should be implemented together.

  • A type should use the same algorithm within Equals(), ==, and != implementations.

  • When implementing Equals(), ==, and !=, a type's GetHashCode() method should also be implemented.

  • GetHashCode(), Equals(), ==, and != should never throw exceptions.

  • When implementing IComparable, equality-related methods should also be implemented.




Essential C# 2.0
Essential C# 2.0
ISBN: 0321150775
EAN: 2147483647
Year: 2007
Pages: 185

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net