C# object comparisons basics
Introduction
When working with various types in code comparisons are performed often ranging from simple string comparisons to various containers e.g. project class instances will be covered along with working with comparing objects with Interfaces to assist with common operations were duplicate data is undesirable.
This article will not dive too deep into all aspects for topics covered as the Microsoft documentation provides this information, instead practical samples for real life usage are shown with caveats.
Comparing strings
When comparing two or more string variables the comparison is case sensitive. In the following example the result is false.
if ("karen" == "Karen")
{
Console.WriteLine("Yes");
}
else
{
Console.WriteLine("No");
}
When this is encountered developer tend to use code as shown below to do a case insensitive compare.
if ("karen".ToLower() == "Karen".ToLower())
{
Console.WriteLine("Yes");
}
else
{
Console.WriteLine("No");
}
This time the result is true as they are both the same casing. Another option
if (string.Equals("Karen", "karen", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Yes");
}
else
{
Console.WriteLine("No");
}
In the code above String.Equals provides an enumeration parameter StringComparison where StringComparison.IgnoreCase in an overloaded method for Equals. If this compare operation is done frequently a language extension can make the comparing method based, written once, used in all projects which reference the class.
public static class Extensions
{
/// <summary>
/// Perform case insensitive equal on two strings
/// </summary>
/// <param name="sender"></param>
/// <param name="item"></param>
/// <returns>true if both strings are a match, false if not a match</returns>
public static bool AreEqual(this string sender, string item) =>
string.Equals(sender, item, StringComparison.OrdinalIgnoreCase);
}
Revised code sample
if ("Karen".AreEqual("karen"))
{
Console.WriteLine("Yes");
}
else
{
Console.WriteLine("No");
}
The same issue for comparing string is casing with String.Contains as in the following example where the same chars are in both strings but one has a uppercase A while the other a lowercase a. The result is false back from the Contains method.
if ("Karen".Contains("Aren"))
{
Console.WriteLine("Yes");
}
else
{
Console.WriteLine("No");
}
Rather use Contains use IndexOf.
if ("Karen".IndexOf("Aren", StringComparison.OrdinalIgnoreCase) >=0)
{
Console.WriteLine("Yes");
}
else
{
Console.WriteLine("No");
}
As with Equals this code is best suited in a language extension.
public static class StringExtensions
{
/// <summary>
/// Determines if a string is within another string with Comparison options
/// </summary>
/// <param name="source"></param>
/// <param name="compareToken">string to see if it exists in source string</param>
/// <param name="comparer">StringComparison</param>
/// <returns></returns>
public static bool Contains(this string source, string compareToken, StringComparison comparer) =>
source?.IndexOf(compareToken, comparer) >= 0;
}
If ignore case is what's need most set the comparison to default to StringComparison.OrdinalIgnoreCase which allows other options to be used and eliminates the need to pass OrdinalIgnoreCase to be passed on each call to the method.
public static bool Contains(this string source,
string compareToken, StringComparison comparer = StringComparison.OrdinalIgnoreCase) =>
source?.IndexOf(compareToken, comparer) >= 0;
There is an issue with the above, using a default for the last parameter will mean the standard Contains method will be called. We could rename Contains as follows which is a better choice,
public static bool ContainsWithOptions(this string source,
string compareToken, StringComparison comparer = StringComparison.OrdinalIgnoreCase) =>
source?.IndexOf(compareToken, comparer) >= 0;
Important
Although this should be obvious, it does not matter where these compares happen e.g. in a class, model, web or windows forms container.
Comparing objects
When comparing object equality there several options.
IEquatable<T> Interface
IEquatable<T> Interface Interface requires the Equals method and GetHashCode property to be overridden. Looking at where this would be useful, in this case data is read from an external source such as a Text file were for some reason data in the incoming file if added to a internal data source would violate current data.
- One option might be to write database table constraints and then run incoming data to the internal data source, on error record the issue.
- Another option is to write assertive statements to test if an object exists presently in the internal data source directly in the code reading in external data,
- Using logic in bullet two implement IEquatable<T> Interface.
Here Employee data is read in from an external data source were duplicates are checked on primary key, first and last name and department. The class below represents a container for employee data which is prior to implementing IEquatable<T> Interface
public class Employee
{
public int EmployeeIdentifier { get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public string DepartmentName {get;set;}
}
Implementing IEquatable<T> Interface
using System;
namespace Operations.Classes
{
public class Employee : IEquatable<Employee>
{
public int EmployeeIdentifier { get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public string DepartmentName {get;set;}
public bool Equals(Employee other)
{
return other.EmployeeIdentifier == EmployeeIdentifier &&
other.FirstName == FirstName &&
LastName == other.LastName &&
other.DepartmentName == DepartmentName;
}
public override int GetHashCode() =>
new { EmployeeIdentifier, FirstName, LastName, DepartmentName }.GetHashCode();
}
}
Using mocked data to test if the class above will not allow duplicates.
protected HashSet<Employee> EmployeeHashSet()
{
return new HashSet<Employee>
{
{new Employee {EmployeeIdentifier = 1, FirstName = "Karen", LastName = "Payne", DepartmentName = "Finance"}},
{new Employee {EmployeeIdentifier = 3, FirstName = "Mary", LastName = "Jones", DepartmentName = "IT"}},
{new Employee {EmployeeIdentifier = 1, FirstName = "Karen", LastName = "Payne", DepartmentName = "Finance"}},
{new Employee {EmployeeIdentifier = 4, FirstName = "Frank", LastName = "Anderson", DepartmentName = "IT"}}
};
}
Test method which should return three employee records (a HashSet is returned, tag on .ToList for a list of employee).
[TestMethod]
public void HashSetIEquatableTest()
{
var employees = EmployeeHashSet();
Assert.IsTrue(employees.Count == 3);
}
Going back to comparing strings, in the above example if the first instance has Payne for last name and the third last name is payne the compare fails. To correct this the GetHashCode must use StringComparer.GetHashCode. Revised class which will now perform proper comparisons.
using System;
namespace Operations.Classes
{
public class Employee : IEquatable<Employee>
{
public int EmployeeIdentifier { get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public string DepartmentName {get;set;}
public bool Equals(Employee other)
{
return other.EmployeeIdentifier == EmployeeIdentifier &&
other.FirstName == FirstName &&
other.DepartmentName == DepartmentName;
}
public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(FirstName) ^
StringComparer.OrdinalIgnoreCase.GetHashCode(LastName) ^
StringComparer.OrdinalIgnoreCase.GetHashCode(DepartmentName);
}
}
}
Note: In some cases (using the above example) there may be no primary key, if not simply exclude it from the overridden Equals method and GetHashCode property.
IEqualityComparer Interface
IEqualityComparer Interface can be used to return distinct items in a collection. Working against string, numerics are straightforward along with dates. In the following example there are duplicates.
protected List<DateTime> DateTimeList()
{
return new List<DateTime>()
{
new DateTime(2019,9,12),
new DateTime(2019,9,13),
new DateTime(2019,9,12),
new DateTime(2019,9,11)
};
}
To get distinct dates.
var datesTimes = DateTimeList();
var test = datesTimes.Distinct();
Suppose there is an empty DateTime list and the developer wants only unique dates the following language extension will work (and work for any ICollection).
/// <summary>
/// Adds a value uniquely to to a collection and returns
/// a value whether the value was added or not.
/// </summary>
/// <typeparam name = "T">The generic collection value type</typeparam>
/// <param name = "sender">The collection.</param>
/// <param name = "pValue">The value to be added.</param>
/// <returns>Indicates whether the value was added or not</returns>
/// <remarks>Naming done to not conflict with extension method above</remarks>
public static bool AddUniqueNoInterface<T>(this ICollection<T> sender, T pValue)
{
var alreadyHasValue = sender.Contains(pValue);
if (!alreadyHasValue)
{
sender.Add(pValue);
}
return alreadyHasValue;
}
Working with the predefined list above and the language extension.
[TestMethod]
public void AddUniqueDatesWithExtensionMethod()
{
var datesTimes = DateTimeList();
var dateTimesDistinct = new List<DateTime>();
foreach (var dateTime in datesTimes)
{
dateTimesDistinct.AddUniqueNoInterface(dateTime);
}
Assert.IsTrue(dateTimesDistinct.Count == 3,
"Expected three unique dates");
}
For complex collections this is where IEqualityComparer Interface would be an options. Consider receiving records for the following class.
public class Suppliers
{
public int SupplierIdentifier { get; set; }
public string CompanyName { get; set; }
public string ContactName { get; set; }
}
Once a list is populated which may have duplicate records the overload for Distinct accepts a IEqualityComparer<T> to compare values in the collection. For the Supplier class duplicates may come from CompanyName, ContactName, SupplierIdentifier or a combination of these properties. In this example two properties will define the equality.
Note that optionally include modifications for string comparing on string properties if needed as explained above.
using System.Collections.Generic;
namespace Operations.Classes
{
public class SuppliersComparer : IEqualityComparer<Suppliers>
{
public bool Equals(Suppliers supplier1, Suppliers supplier2)
{
return supplier1.SupplierIdentifier == supplier2.SupplierIdentifier &&
supplier1.CompanyName == supplier2.CompanyName;
}
public int GetHashCode(Suppliers obj)
{
return new {obj.SupplierIdentifier, ComanyName = obj.CompanyName}.GetHashCode();
}
}
}
In the following usage there may be only one or more duplicates so in the first example the first duplicate is used using .First() extension while in the second example .Last is used to get the last instance for a duplicate. There are many twist here, this is simply a base example to start with.
[TestMethod]
public void DictionaryTest()
{
var dictionaryFirst = SupplierList().Distinct(new SuppliersComparer()).ToList()
.GroupBy(supplier => supplier.SupplierIdentifier)
.ToDictionary(grouping => grouping.Key, grouping => grouping.First());
Assert.IsTrue(dictionaryFirst.Count == 2,
"Expected two for dictionary first");
var dictionaryLast = SupplierList().Distinct(new SuppliersComparer()).ToList()
.GroupBy(supplier => supplier.SupplierIdentifier)
.ToDictionary(grouping => grouping.Key, grouping => grouping.Last());
Assert.IsTrue(dictionaryLast.Count == 2,
"Expected two for dictionary last");
}
Advantages, no extra code needed in the class to check for duplicates and that other implementations can be used. In the following code there are two Comparers for a Customer class.
using System;
using Operations.Interfaces;
namespace Operations.Classes
{
public class Customer : IBase
{
public int Id { get; }
public int CustomerIdentifier { get; set; }
public string CompanyName { get; set; }
public int? ContactIdentifier { get; set; }
public int? ContactTypeIdentifier { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
public int? CountryIdentfier { get; set; }
public string Phone { get; set; }
public DateTime? ModifiedDate { get; set; }
public bool? InUse { get; set; }
}
}
Equality on company name, street, city and country.
using System.Collections.Generic;
namespace Operations.Classes
{
public class CustomerNameStreetCityCountryComparer : IEqualityComparer<Customer>
{
public bool Equals(Customer customer1, Customer customer2)
{
return customer1.CompanyName == customer2.CompanyName &&
customer1.Street == customer2.Street &&
customer1.City == customer2.City &&
customer1.CountryIdentfier == customer2.CountryIdentfier;
}
public int GetHashCode(Customer obj)
{
return new { obj.CompanyName, obj.CountryIdentfier }.GetHashCode();
}
}
}
While this one is on name and country.
using System.Collections.Generic;
namespace Operations.Classes
{
public class CustomerNameCountryComparer : IEqualityComparer<Customer>
{
public bool Equals(Customer customer1, Customer customer2)
{
return customer1.CompanyName == customer2.CompanyName &&
customer1.CountryIdentfier == customer2.CountryIdentfier;
}
public int GetHashCode(Customer obj)
{
return new { obj.CompanyName, obj.CountryIdentfier }.GetHashCode();
}
}
}
Usage for both in a unit test method.
[TestMethod]
public void CustomersComparerTest()
{
var customers = CustomerList().Distinct(new CustomerNameStreetCityCountryComparer()).ToList();
Assert.IsTrue(customers.Count == 2,
"Expected two customers");
var secondAttempt = CustomerList();
secondAttempt[0].CountryIdentfier = 1;
customers = secondAttempt.Distinct(new CustomerNameStreetCityCountryComparer()).ToList();
Assert.IsTrue(customers.Count == 3,
"Expected three customers");
}
Equality by primary key
To compare objects for duplication by primary key keeping code open to work againsts more than one class. Create an Interface to implement with one property, Id.
namespace Operations.Interfaces
{
public interface IBase
{
int Id { get; }
}
}
Implement in a class and assign Id to the current property for uniquely identifying an instance.
using Operations.Interfaces;
namespace Operations.Classes
{
public class Person : IBase
{
public int Identifier { get; set; }
public int Id => Identifier;
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
Create an extension method that has a constraint that the type must implement IBase.
public static void AddUnique<TType>(this ICollection<TType> self, TType item) where TType : IBase
{
if (self.FirstOrDefault(data => data.Id == item.Id) == null)
{
self.Add(item);
}
}
Here a list is iterated were there are duplicates by Identifier property.
[TestMethod]
public void AddUniquePersonByPrimaryTest()
{
var peopleList = new List<Person>();
foreach (var person in PeopleList())
{
peopleList.AddUnique(person);
}
Assert.IsTrue(peopleList.Count == 3,
"People count incorrect for AddUnique");
}
Moving to extension method that is fluid, accepts a predicate.
public static void AddRangeUnique<TType>(this ICollection<TType> self, Func<TType, TType, bool> predicate, IEnumerable<TType> items)
{
foreach (TType item in items)
{
if (!self.Any(data => predicate(data, item)))
{
self.Add(item);
}
}
}
A AddRange could had been done to compliment the AddUnique extension but options are good, define the condition in the caller rather than hard code by Interface.
public void AddUniquePersonAfterTest()
{
var peopleList = new List<Person>();
peopleList.AddRangeUnique((person1, person2) =>
person1.Id == person2.Id, PeopleList());
Assert.IsTrue(peopleList.Count == 3,
"People count incorrect for AddUniqueBy");
}
Overriding == != operators
If == is needed to override != must be also and the reverse. The pattern is similar to both interfaces explained prior.
using System;
namespace Samples.Classes
{
public class Person
{
public int Identifier { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public override bool Equals(object sender)
{
if (sender == null)
return false;
if (!(sender is Person person))
return false;
/*
* See abbreviated / extension AreEqual below
*/
return Identifier == person.Identifier &&
string.Equals(FirstName, person.FirstName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(LastName, person.LastName, StringComparison.OrdinalIgnoreCase);
}
public bool Equals(Person person)
{
if (person == null)
return false;
return Identifier == person.Identifier &&
FirstName.AreEqual(person.FirstName) &&
LastName.AreEqual(person.LastName);
}
public override int GetHashCode()
{
return Identifier.GetHashCode() ^ FirstName.GetHashCode() ^ LastName.GetHashCode();
}
public static bool operator ==(Person person1, Person person2)
{
if (ReferenceEquals(person1, person2))
return true;
if (person1 as object == null || person2 as object == null)
return false;
return person1.Identifier == person2.Identifier &&
person1.FirstName.AreEqual(person2.FirstName) &&
person1.LastName.AreEqual(person2.LastName);
}
public static bool operator !=(Person person1, Person person2)
{
return !(person1 == person2);
}
}
}
See also
.NET: Understanding Equality
.NET: Best Approach Implementing equality comparison
Summary
This article has presented basics for comparing common types and collections rather than write code that does similar result yet many times ends up fragile and only works in one method. The code samples are easy to follow and are easy to adapt to classes in various solutions ranging from Console projects to Web projects along with Windows Forms projects.
Source code
See the following GitHub repository where all code presented above and more can be downloaded to learn from.