Background
I have two objects which have bidirectional association between them in a C# project I am working on. I need to be able to check for value equality (vs reference equality) for a number of reasons (e.g to use them in collections) and so I am implementing IEquatable and the related functions.
Assumptions
- I am using C# 3.0, .NET 3.5, and Visual Studio 2008 (although it shouldn't matter for equality comparison routine issue).
Constraints
Any solution must:
- Allow for bidirectional association to remain intact while permitting checking for value equality.
- Allow the external uses of the class to call Equals(Object obj) or Equals(T class) from IEquatable and receive the proper behavior (such as in System.Collections.Generic).
Problem
When implementing IEquatable to provide checking for value equality on types with bidirectional association, infinite recursion occurs resulting in a stack overflow.
NOTE: Similarly, using all of the fields of a class in the GetHashCode calculation will result in a similar infinite recursion and resulting stack overflow issue.
Question
How do you check for value equality between two objects which have bidirectional association without resulting in a stack overflow?
Code
NOTE: This code is notional to display the issue, not demonstrate the actual class design I'm using which is running into this problem
using System;
namespace EqualityWithBiDirectionalAssociation
{
public class Person : IEquatable<Person>
{
private string _firstName;
private string _lastName;
private Address _address;
public Person(string firstName, string lastName, Address address)
{
FirstName = firstName;
LastName = lastName;
Address = address;
}
public virtual Address Address
{
get { return _address; }
set { _address = value; }
}
public virtual string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
public virtual string LastName
{
get { return _lastName; }
set { _lastName = value; }
}
public override bool Equals(object obj)
{
// Use 'as' rather than a cast to get a null rather an exception
// if the object isn't convertible
Person person = obj as Person;
return this.Equals(person);
}
public override int GetHashCode()
{
string composite = FirstName + LastName;
return composite.GetHashCode();
}
#region IEquatable<Person> Members
public virtual bool Equals(Person other)
{
// Per MSDN documentation, x.Equals(null) should return false
if ((object)other == null)
{
return false;
}
return (this.Address.Equals(other.Address)
&& this.FirstName.Equals(other.FirstName)
&& this.LastName.Equals(other.LastName));
}
#endregion
}
public class Address : IEquatable<Address>
{
private string _streetName;
private string _city;
private string _state;
private Person _resident;
public Address(string city, string state, string streetName)
{
City = city;
State = state;
StreetName = streetName;
_resident = null;
}
public virtual string City
{
get { return _city; }
set { _city = value; }
}
public virtual Person Resident
{
get { return _resident; }
set { _resident = value; }
}
public virtual string State
{
get { return _state; }
set { _state = value; }
}
public virtual string StreetName
{
get { return _streetName; }
set { _streetName = value; }
}
public override bool Equals(object obj)
{
// Use 'as' rather than a cast to get a null rather an exception
// if the object isn't convertible
Address address = obj as Address;
return this.Equals(address);
}
public override int GetHashCode()
{
string composite = StreetName + City + State;
return composite.GetHashCode();
}
#region IEquatable<Address> Members
public virtual bool Equals(Address other)
{
// Per MSDN documentation, x.Equals(null) should return false
if ((object)other == null)
{
return false;
}
return (this.City.Equals(other.City)
&& this.State.Equals(other.State)
&& this.StreetName.Equals(other.StreetName)
&& this.Resident.Equals(other.Resident));
}
#endregion
}
public class Program
{
static void Main(string[] args)
{
Address address1 = new Address("seattle", "washington", "Awesome St");
Address address2 = new Address("seattle", "washington", "Awesome St");
Person person1 = new Person("John", "Doe", address1);
address1.Resident = person1;
address2.Resident = person1;
if (address1.Equals(address2)) // <-- Generates a stack overflow!
{
Console.WriteLine("The two addresses are equal");
}
Person person2 = new Person("John", "Doe", address2);
address2.Resident = person2;
if (address1.Equals(address2)) // <-- Generates a stack overflow!
{
Console.WriteLine("The two addresses are equal");
}
Console.Read();
}
}
}