19.3. Special Methods And Classes

Every class has a few special methods that belong to it, regardless of whether or not we define them. Exactly how every class obtains these methods will be explored in a future lesson. For now, let’s look at two important examples of these methods.

Since we are now more familiar with clases and objects, we will also introduce the StringBuilder class to you. StringBuilder allows you to create strings from collection types and Arrays. It is common to see these objects paired with a loop.

19.3.1. ToString

The ToString method returns a string representation of a class. Calling ToString on a class that you’ve written will result in something like this:

Example

1
2
Student person = new Student("Violet");
Console.WriteLine(person.ToString());

Here, we called ToString on a Student object. The default ToString implementation is generally not very useful. Most of the time, you’ll want to write your own ToString method. To override the default ToString, you can define new behavior for the method and provide better results.

Here’s how we might do it for Student to produce a much more friendly message:

Example

1
2
3
4
5
6
public override string ToString() {
   return Name + " (Credits: " + NumberOfCredits + ", GPA: " + Gpa + ")";
}

Student person = new Student("Violet");
Console.WriteLine(person.ToString());

Console Output

Violet (Credits: 0, GPA: 0.0)

In the example, we define the ToString method to return a string that reports the values of the name, numberOfCredits, and gpa fields in a clear manner.

Note that ToString is often implicitly called for you. For example, the output above could have been generated by the following code, which calls ToString on person within Console.WriteLine().

1
2
Student person = new Student("Violet");
Console.WriteLine(person);

19.3.2. Equals

Suppose we have two objects of type Student, say student1 and student2, and we want to determine if the two are equal. If we try to compare the objects using ==, we will likely get a result we did not expect. This is because student1 and student2 are reference variables, which means they hold a reference to, or the address of, the actual Student objects. student1 and student2 evaluate as equal only when they have the same memory address.

To state that again: student1 and student2 will be equal (==) only when they refer to, or point at, the exact same object. Consider the example below, which creates two Student objects:

Example

1
2
3
4
5
6
Student student1 = new Student("Maria", 1234);
Student student2 = new Student("Maria", 1234);

Console.WriteLine(student1.Name + ", " + student1.StudentId + ": " + student1);
Console.WriteLine(student2.Name + ", " + student2.StudentId + ": " + student2);
Console.WriteLine(student1 == student2);

Even though the objects have the exact same keys and values, student1 and student2 point to different memory locations. Therefore, the == check returns false.

This is not usually how we want to compare objects. For example, we might want to consider two Student objects equal if they have the same name, email, or student ID.

The Equals() method determines if one object is equal to another in this sense. We introduced the method when discussing strings as it is a method on the object class and String is the object class in C#.

The code below shows how to use Equals() to compare two students. Note that they have different names but the same student ID, indicating they are actually the same student by our definition above.

1
2
3
4
5
6
7
Student bono1 = new Student("Paul David Hewson", 4);
Student bono2 = new Student("Bono", 4);

if (bono1.Equals(bono2)) {
   Console.WriteLine(bono1.Name +
      " is the same as " + bono2.Name);
}

If we don’t provide our own Equals() method, the default option only considers two objects equal if they are the exact same object, which means they point to the same memory address. This is identical to the behavior we see when using the == operator: bono1 == bono2.

In the example above, we created two different Student objects, so the expression bono1.Equals(bono2) evaluates to false. In order to compare two objects based on their fields, rather than their memory references, we need to define our own Equals() method.

The difference between the comparison carried out by the default Equals() method (and by the == operator), and how we would like to compare our classes, is the difference between identity and equality.

  1. Two objects are identical if they both point to the same memory address. In essence, they are the same object. If object1 and object2 are identical, then changing one property value in object1 also changes that value for object2.

  2. Two objects are equal if the values they store are the same at the time of comparison. student1 and student2 point to different memory addresses, but their values are all the same. Thus, we can consider them equal, even though they are not identical.

The default Equals() method and the == operator test for identity, whereas we want to test for equality instead. We can do so by overriding the Equals() method. We will discuss overriding in more detail later, but for now just recognize that it involves defining different behavior for an existing method.

Two things can be considered equal even if they do NOT have all the same values. In the case of the Student class, we might specify that two Student objects are equal if they have the same ID numbers. We would then be tempted to write a new method definition for Equals() returning the result of comparing one studentId value to another studentId value for equality. Now if we evaluated such a method with bono1 and bono2 we could get a result of true, since the student IDs match.

One catch of working with Equals() is that its input parameter must be of type object, even if we’re working in a class like Student. The reason why will become more clear in a later lesson, when we introduce the object class. For now, the practical implication is that we must confirm that we can convert, or cast, the input parameter to be of type Student with the as keyword. Then we compare the converted student’s ID (bono2.StudentId) to that of the current student (bono1.StudentId).

Here’s a visualization of the concepts of equality and identity:

Equality

Equality

When you test for equality, you look at two different objects and compare some aspect of them to each other.

Identity

Identity

When you test for identity, you look at two variables to see if they reference the exact same object.

19.3.2.1. Coding a New Equals Method

You’ll often want to implement Equals() yourself. When you do, be sure you understand the best practices around how the method should behave. These are a little more involved compared to coding a new ToString method.

In fact, the Equals() method we defined above isn’t very good by most C# programmers’ standards. Let’s improve it.

19.3.2.1.1. Problem #1

The method argument cannot be converted to a Student instance.

When we attempt to cast the argument ToBeCompared to type Student, we’ll get an exception if ToBeCompared can’t be properly converted. This happens if something other than a Student object gets passed into Equals(). To prevent this from happening, we’ll return false if ToBeCompared was not created from the Student class. To check this, we use the GetType method, which is available to every object (similarly to ToString).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public override boolean Equals(object toBeCompared) {

   if (toBeCompared.GetType() != this.GetType())
   {
      return false;
   }

   Student s = toBeCompared as Student;
   return s.StudentId == StudentId;
}

Lines 3 - 6 ensure that the two objects that we want to compare were created from the same class. Line 8 uses the as keyword to set a Student object, called s, to the object when toBeCompared is cast as type Student.

19.3.2.1.2. Problem #2

toBeCompared might be null.

If toBeCompared is null, then toBeCompared.GetType() throws an exception. This is an easy issue to fix—just compare the object to null. If the comparison evaluates to true, then we know the object is null and Equals() should return false.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public override boolean Equals(object toBeCompared) {

   if (toBeCompared == null)
   {
      return false;
   }

   if (toBeCompared.GetType() != this.GetType())
   {
      return false;
   }

   Student s = toBeCompared as Student;
   return s.StudentId == StudentId;
}

Line 3 checks toBeCompared for null, preventing an error in line 8. Line 8 checks the class of toBeCompared, preventing an error in line 13.

19.3.2.1.3. Problem #3

The two objects to compare are the same object (identical).

This is less of a problem and more of a way to improve our Equals() method. If toBeCompared is the same literal object that we are comparing it to, then we can make a quick determination and save a few checks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public override boolean Equals(object toBeCompared) {

   if (toBeCompared == this)
   {
      return true;
   }

   if (toBeCompared == null)
   {
      return false;
   }

   if (toBeCompared.GetType() != this.GetType())
   {
      return false;
   }

   Student s = toBeCompared as Student;
   return s.StudentId == StudentId;
}

Line 3 checks for identity. If true, then the remaining checks become unnecessary.

19.3.2.2. Components of Equals

Almost every Equals method you write yourself will look similar to the last example above. It will contain the following segments in this order:

  1. Reference check: If the two objects are the same, return true right away.

  2. Null check: If the argument is null, return false.

  3. Class check: Compare the classes of the two objects to ensure a safe cast. Return false if the classes are different.

  4. Cast: Convert the argument to the type of our class, so getters and other methods can be called.

  5. Custom comparison: Use custom logic to determine whether or not the two objects should be considered equal. This will usually be a comparison of class members.

19.3.2.3. Characteristics of Equals

Now that we know how to write an Equals() method, let’s look at some characteristics that every such method should have. Following the general outline above makes it easier to ensure that your Equals() method has these characteristics.

  1. Reflexivity: For any non-null reference value x, x.Equals(x) should return true.

  2. Symmetry: For any non-null reference values x and y, x.Equals(y) should return true if and only if y.Equals(x) also returns true.

  3. Transitivity: For any non-null reference values x, y, and z, if x.Equals(y) returns true and y.Equals(z) returns true, then x.Equals(z) should return true.

  4. Consistency: As long as x and y do not change, x.Equals(y) should always return the same result.

  5. Non-null: For any non-null reference value x, x.Equals(null) should return false.

If you think back to what your math classes had to say about equality, then these concepts will feel familiar.

Using the general approach outlined above to implement Equals() will make it easier to meet these characteristics. However, always check your method! Missing one or more characteristic can be disastrous for your C# applications.

Tip

Seasoned C# developers, will tell you that every time you implement your own version of Equals() you should also implement your own version of GetHashCode(). GetHashCode() is another special method that every class has. Understanding GetHashCode() would take us a bit far afield at this point, but we would be remiss to not mention it. If you want to read more, check out the documentation on the GetHashCode() method and this Stack Overflow discussion. We will cover how to override Equals() and GetHashCode() in the next section using some fun shortcuts in Visual Studio.

19.3.2.4. Take Away

You may not need to write your own Equals() method for every class you create. However, as a new C# programmer, remember the following:

Always use Equals() to compare objects.

This is especially true when working with objects of types provided by C#, such as string. A class that is part of C# or a third-party library will have implemented Equals() in a way appropriate for the particular class, whereas == will only check to see if two variables refer to the same reference location.

19.3.3. StringBuilder Objects

Up until this point, if you wanted to turn a collection of strings objects into a single string object, you could concatenate them using a loop.

For example, if we have an array of strings containing each word in a sentence, we may want to concatenate each value in the array to reform our sentence.

1
2
3
4
5
6
7
string[] arrayOfWords = {"Books", "Cheese", "Trees", "Laughter"};

string finalSentence = "";

foreach (string word in arrayOfWords) {
   finalSentence += word;
}

This code would work well for this situation. However, because strings are immutable, when the value of word is appended onto finalSentence, a new string object is created. This means that the longer arrayOfWords is, the more intensive and inefficient the code becomes. We can accomplish the same thing with the StringBuilder class. StringBuilder objects are mutable strings of characters and the documentation contains a full list of important properties and methods.

If we wanted to use a StringBuilder object instead of a simple string in the above code, we would modify it like so:

1
2
3
4
5
StringBuilder finalSentence = new StringBuilder();

foreach (string word in arrayOfWords) {
   finalSentence.Append(word);
}

First, we need to initialize a new StringBuilder object, finalSentence, with new StringBuilder(). The Append() method in the StringBuilder class adds the value of word to the end of the finalSentence object.

While concatenating strings is just one of the many use cases of loops in C#, StringBuilder is a fun tool to add to your toolkit. If we don’t use a StringBuilder object, the longer arrayOfWords is, the slower our program will get. While at this level, we may not be too concerned with a program’s performance, in enterprise applications, performance can be everything.

Here is another example, if you would like to practice more with this class.

19.3.4. Check Your Understanding

Question

Given the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Pet {

   public string Name { get; set; }

   Pet(string n) {
      Name = n;
   }
}

string firstPet = "Fluffy";
Pet secondPet = new Pet("Fluffy");
Pet thirdPet = new Pet("Fluffy");

Which of the following statements evaluates to true?

  1. firstPet == secondPet;

  2. secondPet == thirdPet;

  3. thirdPet.Equals(secondPet);

  4. thirdPet.Name == firstPet;

  5. thirdPet.Equals(firstPet);

Question

We add the following code inside the Pet class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public override boolean Equals(object petToCheck) {

   if (petToCheck == this) {
      return true;
   }

   if (petToCheck == null) {
      return false;
   }

   if (petToCheck.GetType() != this.GetType()) {
      return false;
   }

   Pet thePet = petToCheck as Pet;
   return thePet.Name == Name;
}

Which of the following statements evaluated to false before, but now evaluates to true?

  1. firstPet == secondPet;

  2. secondPet == thirdPet;

  3. thirdPet.Equals(secondPet);

  4. thirdPet.Name == firstPet;

  5. thirdPet.Equals(firstPet);