Skip to main content

41. Object Methods in C#

Level: Intermediate
Goal: Override ToString, Equals, GetHashCode to make objects printable and comparable.

Problem: Default ToString prints Student.SchoolNamespace.Student - not helpful.

Solution: Override ToString():

public override string ToString()
{
return $"Student: {Name} ({RollNumber})";
}

Also override Equals() to compare objects properly, and GetHashCode() for use in collections.

ℹ️ What You'll Learn
  • ToString() - human-readable string representation
  • Equals() - value equality vs reference equality
  • GetHashCode() - contract with Equals, hash-based collections
  • GetType() - runtime type information
  • When to override each method
  • The Equals/GetHashCode contract
  • IEquatable<T> pattern for type-safe equality
  • Common mistakes and best practices

Object Methods Reference

MethodPurposeOverrideUse CaseExample
ToString()String representation? YesLogging, debugging, display"Student[1]: Sahasra"
Equals()Value equality? YesDictionary/HashSet, comparisonss1.Equals(s2)
GetHashCode()Hash for collections? With EqualsDictionary/HashSet keysHashCode.Combine(Id)
GetType()Runtime type info? RarelyType checking, reflectionobj.GetType() == typeof(Student)
ReferenceEquals()Reference equality? NoCheck same object (rare)ReferenceEquals(s1, s2)
IEquatable<T>Type-safe equality? RecommendedAvoid boxing, cleanerEquals(Student other)
== operatorEquality operatorOverrideConsistency with Equalss1 == s2
!= operatorInequality operatorOverrideOpposite of ==s1 != s2

The 4 Object Methods

object.ToString() // string representation
object.Equals() // value equality
object.GetHashCode() // hash for Dictionary/HashSet
object.GetType() // runtime type info

ToString - String Representation

// Default - prints type name (useless)
var student = new Student { Id=1, Name="Sahasra", Percentage=87.5 };
Console.WriteLine(student); // "SchoolManagement.Models.Student" - not helpful

// Override ToString - meaningful output
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string ClassName { get; set; } = "";
public double Percentage { get; set; }

public override string ToString()
{
return $"Student[{Id}]: {Name} | {ClassName} | {Percentage:F1}%";
}
}

Console.WriteLine(student);
// "Student[1]: Sahasra | 10th | 87.5%"

// Also used by string interpolation, Console.WriteLine, debugger
var students = new List<Student> { student };
Console.WriteLine(string.Join(", ", students));
// "Student[1]: Sahasra | 10th | 87.5%"

Equals - Value Equality

// Default class behavior - reference equality (same object?)
var s1 = new Student { Id=1, Name="Sahasra", Percentage=87.5 };
var s2 = new Student { Id=1, Name="Sahasra", Percentage=87.5 };

Console.WriteLine(s1 == s2); // False - different objects
Console.WriteLine(s1.Equals(s2)); // False - reference equality

// Override Equals for value equality
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";
public double Percentage { get; set; }

public override bool Equals(object? obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj is not Student other) return false;

return Id == other.Id && Name == other.Name;
}

// MUST override GetHashCode when overriding Equals
public override int GetHashCode()
=> HashCode.Combine(Id, Name);

// Also override == operator for consistency
public static bool operator ==(Student? a, Student? b)
=> a?.Equals(b) ?? b is null;

public static bool operator !=(Student? a, Student? b)
=> !(a == b);
}

var s1 = new Student { Id=1, Name="Sahasra" };
var s2 = new Student { Id=1, Name="Sahasra" };
Console.WriteLine(s1.Equals(s2)); // True ?
Console.WriteLine(s1 == s2); // True ?

GetHashCode - Contract with Equals

// RULE: If a.Equals(b) == true, then a.GetHashCode() == b.GetHashCode()
// (reverse not required - hash collisions are ok)

// BAD - violates contract
public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id;

// public override int GetHashCode() => 42; // [X] works but kills performance
// all items go to same hash bucket

// GOOD - use HashCode.Combine (C# 8+)
public override int GetHashCode()
=> HashCode.Combine(Id, Name);

// Why it matters - Dictionary and HashSet use GetHashCode
var studentSet = new HashSet<Student>();
studentSet.Add(new Student { Id=1, Name="Sahasra" });
studentSet.Add(new Student { Id=1, Name="Sahasra" }); // duplicate?

// Without override - 2 items added (different references)
// With correct override - 1 item (same hash + equals = duplicate)
Console.WriteLine(studentSet.Count); // 1 with correct override

GetType - Runtime Type Info

Student student = new Student { Name = "Sahasra" };
Person person = student; // stored as Person reference

// GetType() - actual runtime type (not reference type)
Console.WriteLine(student.GetType().Name); // "Student"
Console.WriteLine(person.GetType().Name); // "Student" - actual type!

// Practical uses
static void ProcessMember(Person member)
{
Console.WriteLine($"Processing {member.GetType().Name}: {member.Name}");

// Type checking
if (member.GetType() == typeof(Student))
{
var s = (Student)member;
Console.WriteLine($" Grade: {s.GetGrade()}");
}
}

// GetType vs typeof
// typeof(Student) - compile time, no instance needed
// student.GetType() - runtime, needs instance
// Both return Type object but used in different contexts

Complete Student with All Overrides

💻 Try It — Console App
💡 Paste into Program.cs and press F5⌥ GitHub
public class Student : IEquatable<Student>
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string ClassName { get; set; } = "";
public double Percentage { get; set; }

// IEquatable<T> - type-safe, avoids boxing
public bool Equals(Student? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Id == other.Id && Name == other.Name;
}

// Override from object
public override bool Equals(object? obj)
=> Equals(obj as Student);

public override int GetHashCode()
=> HashCode.Combine(Id, Name);

public override string ToString()
{
return $"Student[{Id}]: {Name} | {ClassName} | {Percentage:F1}%";
}

public static bool operator ==(Student? a, Student? b)
=> EqualityComparer<Student>.Default.Equals(a, b);

public static bool operator !=(Student? a, Student? b)
=> !(a == b);
}

// Usage
var s1 = new Student { Id=1, Name="Sahasra", Percentage=87.5, ClassName="10th" };
var s2 = new Student { Id=1, Name="Sahasra", Percentage=91.0, ClassName="10th" };
var s3 = new Student { Id=2, Name="Priya", Percentage=91.0, ClassName="10th" };

Console.WriteLine(s1.Equals(s2)); // True - same Id+Name
Console.WriteLine(s1.Equals(s3)); // False - different Id+Name
Console.WriteLine(s1); // "Student[1]: Sahasra | 10th | 87.5%"

var set = new HashSet<Student> { s1, s2, s3 };
Console.WriteLine(set.Count); // 2 - s1 and s2 are "equal"

Common Mistakes

? Overriding Equals without GetHashCode (breaks hash collections):

public class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";

public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id && Name == s.Name;

// Missing - GetHashCode not overridden
}

var set = new HashSet<Student>();
set.Add(new Student { Id = 1, Name = "Sahasra" });
set.Add(new Student { Id = 1, Name = "Sahasra" });

Console.WriteLine(set.Count); // 2 - should be 1, but Equals says equal!
// Hash codes differ ? not found as duplicate

? Always override both Equals and GetHashCode together:

public class Student
{
public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id && Name == s.Name;

public override int GetHashCode()
=> HashCode.Combine(Id, Name); // ? Must match Equals logic
}

var set = new HashSet<Student>();
set.Add(new Student { Id = 1, Name = "Sahasra" });
set.Add(new Student { Id = 1, Name = "Sahasra" });

Console.WriteLine(set.Count); // 1 ? Correctly identified as duplicate

? Implementing GetHashCode with fields not in Equals (contract violation):

public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id;

public override int GetHashCode()
=> HashCode.Combine(Id, Name); // Name not in Equals! ? Violates contract

? Use same fields in both Equals and GetHashCode:

public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id && Name == s.Name;

public override int GetHashCode()
=> HashCode.Combine(Id, Name); // ? Same fields

? Returning constant GetHashCode (kills performance):

public override int GetHashCode()
=> 42; // ? All items go to same bucket, O(1) becomes O(n)

? Use HashCode.Combine with distinguishing fields:

public override int GetHashCode()
=> HashCode.Combine(Id, Name); // ? Distributes items across buckets

? Not implementing == operator when overriding Equals (inconsistent):

var s1 = new Student { Id = 1, Name = "Sahasra" };
var s2 = new Student { Id = 1, Name = "Sahasra" };

Console.WriteLine(s1.Equals(s2)); // True
Console.WriteLine(s1 == s2); // False ? Inconsistent!

Override == and != with Equals logic:

public static bool operator ==(Student? a, Student? b)
=> a?.Equals(b) ?? b is null;

public static bool operator !=(Student? a, Student? b)
=> !(a == b);

Console.WriteLine(s1.Equals(s2)); // True
Console.WriteLine(s1 == s2); // True ? Consistent

Best Practices

  1. Always override ToString() - Helps with logging, debugging, display
  2. Override Equals when comparing value - Not reference by default
  3. Always override GetHashCode with Equals - Never one without the other
  4. Use HashCode.Combine(fields) - Modern, handles collisions well
  5. Implement IEquatable<T> - Type-safe, avoids boxing
  6. Override == and != operators - Consistency with Equals
  7. Document equality semantics - What makes two objects equal?
  8. Use value equality for domain models - Student with same Id = equal
  9. Keep hash computation fast - GetHashCode called frequently
  10. Avoid mutable fields in GetHashCode - If field changes, hash changes (breaks collections)
  11. Test equality in collections - HashSet, Dictionary behavior with custom Equals
  12. Consider record types (C# 9+) - Auto-implements Equals, GetHashCode, ToString

🎯 Q1: Why must I override GetHashCode with Equals?

Contract: If a.Equals(b) == true, then a.GetHashCode() == b.GetHashCode().

Without this, hash collections (HashSet, Dictionary) break:

var s1 = new Student { Id = 1 };
var s2 = new Student { Id = 1 };

s1.Equals(s2) == true // Equals says equal

// But different GetHashCode
var set = new HashSet<Student> { s1, s2 };
Console.WriteLine(set.Count); // Should be 1, but is 2!
// HashSet uses GetHashCode to bucket items - if hashes differ, no duplicate found

Rule: Always override both together. Never Equals without GetHashCode.

public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id;

public override int GetHashCode()
=> HashCode.Combine(Id); // ? Uses same field
🎯 Q2: What's the difference between == and Equals()?

By default on classes:

  • ==: Reference equality (same object in memory)
  • .Equals(): Reference equality too (until overridden)

When you override Equals, you usually override == for consistency:

var s1 = new Student { Id = 1, Name = "Sahasra" };
var s2 = new Student { Id = 1, Name = "Sahasra" };

// Before override
Console.WriteLine(s1 == s2); // false (different objects)
Console.WriteLine(s1.Equals(s2)); // false (reference equality)

// After override
public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id && Name == s.Name;

public static bool operator ==(Student? a, Student? b)
=> a?.Equals(b) ?? b is null;

Console.WriteLine(s1 == s2); // true (value equality)
Console.WriteLine(s1.Equals(s2)); // true (consistent)

Rule: Override Equals ? override == for consistency.

🎯 Q3: When should I use IEquatable<T>?

IEquatable<T> provides type-safe equality without boxing/casting.

// Without IEquatable
public override bool Equals(object? obj)
{
if (obj is not Student other) return false; // Cast needed
return Id == other.Id;
}

// With IEquatable<T>
public bool Equals(Student? other) // Type-safe, no casting
=> other is not null && Id == other.Id;

public override bool Equals(object? obj) // Still needed for override
=> Equals(obj as Student);

Implement IEquatable<T>:

  • Type-safe version (Student? parameter)
  • Avoids boxing for value types
  • Cleaner API, better performance

Rule: Implement both IEquatable<T> and override Equals().

🎯 Q4: What fields should be in GetHashCode?

Rule: Same fields as in Equals().

public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id && Name == s.Name && ClassName == s.ClassName;

public override int GetHashCode()
=> HashCode.Combine(Id, Name, ClassName); // All three fields

If Equals checks A and B, GetHashCode must use A and B. Otherwise:

// ? Wrong - violates contract
public override bool Equals(object? obj)
=> obj is Student s && Id == s.Id;

public override int GetHashCode()
=> HashCode.Combine(Id, Name); // Name not in Equals!

Consistent fields = consistent hash distribution.

🎯 Q5: Why override ToString()?

Default ToString() prints type name only - not useful for debugging.

var student = new Student { Id = 1, Name = "Sahasra", Percentage = 87.5 };

// Default
Console.WriteLine(student); // "SchoolManagement.Student"

// With override
public override string ToString()
=> $"Student[{Id}]: {Name} | {Percentage:F1}%";

Console.WriteLine(student); // "Student[1]: Sahasra | 87.5%"

Helps with:

  • Console output for debugging
  • Logging
  • String interpolation
  • Debugger variable inspection

Override ToString for all public types you create.

🎯 Q6: Can I use record instead of class for equals?

C# 9+ records auto-implement Equals, GetHashCode, ToString correctly.

// Class - must implement manually
public class Student
{
public override bool Equals(object? obj) { }
public override int GetHashCode() { }
public override string ToString() { }
}

// Record - auto-generated
public record Student(int Id, string Name, double Percentage);

var s1 = new Student(1, "Sahasra", 87.5);
var s2 = new Student(1, "Sahasra", 87.5);

Console.WriteLine(s1 == s2); // true ? auto-implemented
Console.WriteLine(s1.GetHashCode() == s2.GetHashCode()); // true ?
Console.WriteLine(s1.ToString()); // "Student { Id = 1, Name = Sahasra, ... }"

Record = less boilerplate, value semantics by default. Prefer for domain models.


When You'll Use This in SMS

SMS overrides object methods:

public class Student
{
public override string ToString() => $"{Name} ({RollNumber})";
public override bool Equals(object obj) => obj is Student s && s.Id == Id;
public override int GetHashCode() => Id.GetHashCode();
}

var s1 = new Student { Id = 1, Name = "Sahasra" };
Console.WriteLine(s1); // Uses ToString()

ℹ️ Video Tutorial

Object methods explained: ToString, Equals, GetHashCode, GetType, virtual methods. Video coming soon. Subscribe to NexCoding YouTube for updates.


🤖Use AI to Learn Faster

Use ChatGPT, Claude, or Copilot to go deeper on C# object class methods ToString Equals GetHashCode. Try these prompts:

  • "Why must I override GetHashCode when I override Equals in C#?"
  • "What is the difference between == and Equals() in C# for classes?"
  • "What is the contract between Equals and GetHashCode in C#?"
  • "Show me the correct way to implement value equality for a C# class"

💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.

Use these links to continue the full Backend > C# topic from the top menu:

Target search terms for this lesson: c# tostring equals gethashcode, object methods c#, iequatable c#.


Next Article

Override vs Overload ->

nexcoding.in