Overriding object Members

Chapter 6 discussed how all classes and structs ultimately 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 overriding these virtual methods. Doing so is automatic for records, but the generated code also provides an example on what is required if you choose to customize the generated code or implement an override on a non-record type implementation.

Overriding ToString()

By default, calling ToString() on any object will return the fully qualified type name of the object. 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, when invoking ToString() on an Angle (Listing 9.1) the result returns:

Angle { Degrees = 90, Minutes = 0, Seconds = 0, Name = }

(Which also happens to be the output of Listing 9.2.)

Write methods such as System.Console.WriteLine() and System.Diagnostics.Trace.Write() call an object’s ToString() method7, so overriding the method often outputs more meaningful information than the default implementation.

Overriding ToString() requires nothing more than declaring the ToString() method as override and returning a string. For example:

public override string ToString()

{

     string prefix =

     string.IsNullOrWhiteSpace(Name)?string.Empty : Name+": ";

     return $"{prefix}{Degrees}° {Minutes}' {Seconds}\"";

}

Regardless, do not return an empty string or null, as the lack of output will be very confusing. ToString() is useful for debugging from within a developer IDE or writing to a log file. The lack of localization and other advanced formatting features make this approach less suitable for general end-user text display. For this reason, you should keep the strings relatively short (one screen width) so that they are not truncated off the end of the screen.

Consider overriding the ToString() method whenever relevant diagnostic information can be provided from the output—specifically, when the target audience is developers, since the default object.ToString() output is a type name and is not end-user friendly.

Guidelines
DO override ToString() whenever useful developer-oriented diagnostic strings can be returned.
CONSIDER trying to keep the string returned from ToString() short.
DO NOT return an empty string or null from ToString().
DO NOT throw exceptions or make observable side effects (change the object state) from ToString().
DO provide an overloaded ToString(string format) or implement IFormattable if the return value requires formatting or is culture-sensitive (e.g., DateTime).
CONSIDER returning a unique string from ToString() so as to identify the object instance.
Implementing Value Equality

Frequently, developers expect implementing value equivalence to be trivial. How hard could it be to compare two objects? However, it includes a surprising number of subtleties and complexities that require careful thought and testing. Fortunately, there are two approaches that significantly simplify the code. Firstly, if you use a record type, then the C# compiler takes care of generating the Equals() method and all its associated members for you, comparing all fields of the record including those of automatically implemented properties – whether they are included as positional parameters or not. Secondly, you can group the identifying data elements (automatically implemented properties and fields) into a tuple and then compare the tuples. In the case of reference type, you also need to pay attention to any potential null values.

Listing 9.3 shows the implementation of Equals() that was generated for a record and enables value equality. One of the big advantages of C# records is that they generate an value equality implementation for you, relieving you of the exercise to manually implement what could be surprisingly complex method considering inheritance, null, varying data types, and value/reference type in addition to comparing the contained values themselves. That said, if the record generated implementation doesn’t suit your needs, then you will need to write your own. One reason to do that is that you may not want some of the properties to be include as part of the equality determination. For example, perhaps you don’t want the Name property on Angle to be part of an Angle’s value identity, instead relying solely on Degree’s, Minute’s, and Second’s. Listing 9.8 provides such a full customized record struct with the following snippet of a custom equality implementation:

     public bool Equals(Angle other) =>

         (Degrees, Minutes, Seconds).Equals(

             (other.Degrees, other.Minutes, other.Seconds));

Notice that each of value identifying properties are combined into tuple and compared with the same values of other. In addition, be aware that the equals implementation is overloaded – The previous snippet shows the implementation with an Angle parameter. That is because the C# compiler generates all the additional value equality functionality for you. However, this is not the case on types that are not records – you must provide all such customization yourself. Also, whenever you update the Equals() method you should update GetHashCode() so use the same data elements – a failure to override GetHashCode() when overriding Equals() and vice-versa will generate a warning:

While this snippet looks different than the record-generated version of Equals() shown in Listing 9.3, internally the tuple (System.ValueTuple<...>) uses EqualityComparer<T> as well. In turn, EqualityComparer<T> relies on the type parameters implementation of IEquatable<T> (which contains only a single Equals<T>(T other) member). Therefore, to correctly override Equals, you need to implement IEquatable<T>.

Once Equals() is overridden, there is a possible inconsistency. That is, two objects could return true for Equals() but false for the == operator because == performs a reference equality check by default. To correct this flaw, it is important to override the equals (==) and not equals (!=) operators as well. Of course, this code is generated for you as part of the record implementation as follows:

public static bool operator ==(Coordinate left, Coordinate right) =>

    ReferenceEquals(left, right) || (left?.Equals(right) ?? false);

public static bool operator !=(Angle left, Angle right) =>

    !(left == right);

For the most part, the implementation for these operators can delegate the logic to Equals(), or vice versa. Take care, however, not to invoke an infinite recursive loop with the == operator calling back on itself. In this case, such a recursion is avoided because we use ReferenceEquals() initially and it will return true if both values are null. For value types, there is, of course, no need to check for null. See Chapter 10 for more information on operator overloading.

Lastly, consider implementing IComparable<T> if the custom type can be sorted along with and IFormattable if localization is needed. For the full implementation of value equality without using a record construct, see the Advanced Blocks - Value Equality Principles and Overriding GetHashCode(). There are two more important topics related to value types that we still need to cover. The first is boxing and the second is enums and both are discussed following the Advanced Blocks.

Guidelines
CONSIDER using ReferenceEquals() to check for identity or if both operands are null.
DO check for null when invoking the Equals() methods from the == operator implementation on a reference type.
AVOID invoking the equality comparison operator (==) from within the implementation of the == operator.

________________________________________

7. Unless there is an implicit cast operator, as described in Advanced Topic: Cast Operator.
{{ snackbarMessage }}