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.
ToString()- human-readable string representationEquals()- value equality vs reference equalityGetHashCode()- contract with Equals, hash-based collectionsGetType()- 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
| Method | Purpose | Override | Use Case | Example |
|---|---|---|---|---|
| ToString() | String representation | ? Yes | Logging, debugging, display | "Student[1]: Sahasra" |
| Equals() | Value equality | ? Yes | Dictionary/HashSet, comparisons | s1.Equals(s2) |
| GetHashCode() | Hash for collections | ? With Equals | Dictionary/HashSet keys | HashCode.Combine(Id) |
| GetType() | Runtime type info | ? Rarely | Type checking, reflection | obj.GetType() == typeof(Student) |
| ReferenceEquals() | Reference equality | ? No | Check same object (rare) | ReferenceEquals(s1, s2) |
IEquatable<T> | Type-safe equality | ? Recommended | Avoid boxing, cleaner | Equals(Student other) |
| == operator | Equality operator | Override | Consistency with Equals | s1 == s2 |
| != operator | Inequality operator | Override | Opposite 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
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
- Always override ToString() - Helps with logging, debugging, display
- Override Equals when comparing value - Not reference by default
- Always override GetHashCode with Equals - Never one without the other
- Use HashCode.Combine(fields) - Modern, handles collisions well
- Implement
IEquatable<T>- Type-safe, avoids boxing - Override == and != operators - Consistency with Equals
- Document equality semantics - What makes two objects equal?
- Use value equality for domain models - Student with same Id = equal
- Keep hash computation fast - GetHashCode called frequently
- Avoid mutable fields in GetHashCode - If field changes, hash changes (breaks collections)
- Test equality in collections - HashSet, Dictionary behavior with custom Equals
- Consider record types (C# 9+) - Auto-implements Equals, GetHashCode, ToString
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
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.
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().
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.
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.
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()
Object methods explained: ToString, Equals, GetHashCode, GetType, virtual methods. Video coming soon. Subscribe to NexCoding YouTube for updates.
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.
C# Section Links
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#.