Фон
У меня есть два объекта, которые имеют двунаправленную связь между ними в проекте C #, над которым я работаю. Мне нужно иметь возможность проверять равенство значений (по сравнению с ссылочным равенством) по ряду причин (например, использовать их в коллекциях), и поэтому я реализую IEquatable и связанные с ним функции.
Предположения
- Я использую C # 3.0, .NET 3.5 и Visual Studio 2008 (хотя это не должно иметь значения для проблемы рутинного сравнения).
Ограничения
Любое решение должно:
- Оставить двунаправленной ассоциацию без изменений, разрешив проверку на равенство значений.
- Разрешить внешнему использованию класса вызывать Equals (Object obj) или Equals (T class) из IEquatable и получать правильное поведение (например, в System.Collections.Generic).
Проблема
При реализации IEquatable для обеспечения проверки на равенство значений для типов с двунаправленной ассоциацией происходит бесконечная рекурсия, приводящая к переполнению стека.
ПРИМЕЧАНИЕ: Аналогичным образом, использование всех полей класса в вычислении GetHashCode приведет к аналогичной бесконечной рекурсии и возникнет проблема переполнения стека.
Вопрос
Как проверить равенство значений между двумя объектами, имеющими двунаправленную ассоциацию, не приводя к переполнению стека?
Код
ПРИМЕЧАНИЕ: Этот код предназначен для отображения проблемы, а не демонстрации фактического дизайна класса, который я использую и который сталкивается с этой проблемой
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();
}
}
}