Part of the problem is that field.GetValue(other)
returns an object
. You'll likely find that using recursive reflection to compare untyped objects will frustrate you.
In order to make this work you'd have to determine whether the field is a collection, and if it is, compare the contents of the collection instead. Then what if one of collections contains a collection? This is doable, but it's a headache.
Whatever this object is that you're trying to compare, it's much simpler if you implement IEquatable<T>
or define an IEqualityComparer<T>
for the types you want to compare. Your Equals
method then checks equality explicitly for the various members. If those members are reference types, you define equality for those as well. Then, if those types have more nested properties you don't have drill down into all of those nested properties and compare their properties.
What this ultimately requires is that your method "knows" what types it's comparing so that it knows exactly how to compare them. If you're using reflection to figure out what the properties are and then trying to check them for equality, it's possible, but it's difficult and potentially broken by future changes.
Here's an example using a few classes with nested properties.
public class Foo : IEquatable<Foo>
{
public int FooValue { get; set; }
public List<Bar> Bars { get; set; }
public bool Equals(Foo other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
if (FooValue != other.FooValue) return false;
if (Bars == null ^ other.Bars == null) return false;
return Bars == null || Bars.SequenceEqual(other.Bars);
}
}
public class Bar : IEquatable<Bar>
{
public int BarValue { get; set; }
public List<FooBar> FooBars { get; set; }
public bool Equals(Bar other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
if( BarValue != other.BarValue) return false;
if (FooBars == null ^ other.FooBars == null) return false;
return FooBars==null || FooBars.SequenceEqual(other.FooBars);
}
}
public class FooBar : IEquatable<FooBar>
{
public int FooBarValue { get; set; }
public bool Equals(FooBar other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return FooBarValue == other.FooBarValue;
}
}
Each class with a nested collection property uses SequenceEqual
to determine whether the collections match. But how does it figure out whether individual items in those collections are equal to each other? It doesn't. It assumes that those types will be able to to determine equality.
Now if the conditions that make two instances of a class equatable change, the details can be isolated to that class.
Suppose I can't modify one of these classes to support IEquatable<T>
, or for some reason it doesn't make sense to. Perhaps equality is determined differently in different contexts. Maybe a class has an ID
property and in some context all I care about is that if two objects have the same ID
, they're equal.
In that case I could move that equality comparison into its own class and only use it when I want to. It's no longer "built in" to the class.
public class FooBar
{
public int FooBarValue { get; set; }
}
public class FooBarComparer : IEqualityComparer<FooBar>
{
public bool Equals(FooBar x, FooBar y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.FooBarValue == y.FooBarValue;
}
public int GetHashCode(FooBar obj)
{
return obj.GetHashCode();
}
}
Now when I use SequenceEqual
on two collections of FooBar
I would do this:
FooBars.SequenceEqual(other.FooBars, new FooBarComparer())
Another benefit of this is that you can re-use equality comparisons. Suppose you have two different classes with a list of Bar
. Now both classes don't have to inspect Bar
and its nested properties to determine equality, which would duplicate code. That equality check is located elsewhere and can be shared by both when that's desirable.