Overriding object MembersChapter 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()
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.
Consider the GetHashCode() implementation for the Coordinate type shown in Listing 9.2. Listing 9.2. Implementing GetHashCode()
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.
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 ValuesTwo 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
The results of Listing 9.3 appear in Output 9.2. Output 9.2.
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
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.
Listing 9.5 shows a sample Equals() implementation. Listing 9.5. Overriding Equals()
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 EqualityWhile learning the details for overriding an object's virtual members, several guidelines emerge.
|