Соотношение значений с двунаправленной ассоциацией в C#
Фон
У меня есть два объекта, которые имеют двунаправленную связь между ними в проекте 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();
}
}
}
5 ответов
Если изменение структуры класса для удаления двунаправленной связи возможно и уменьшает количество проблем, связанных с реализацией, то это предпочтительное решение.
Если этот редизайн невозможен или представляет проблемы с реализацией равных или более серьезных проблем, то одним из возможных решений является использование специализированного метода Equals, который вызывается методами Equals классов, участвующих в двунаправленной ассоциации. Как сказал Mehrdad, это не должно быть слишком сложным делом, так как требования явно требуют такой связи, так что вы не вводите ее, делая это.
Код
Вот реализация этого, которая сохраняет специализированные методы, проверяя только свои собственные поля. Это уменьшает проблемы с обслуживанием по сравнению с тем, что каждый класс выполняет сравнение для каждого класса для каждого свойства.
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();
}
internal virtual bool EqualsIgnoringAddress(Person other)
{
// Per MSDN documentation, x.Equals(null) should return false
if ((object)other == null)
{
return false;
}
return ( this.FirstName.Equals(other.FirstName)
&& this.LastName.Equals(other.LastName));
}
#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.EqualsIgnoringPerson(other.Address) // Don't have Address check it's person
&& 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();
}
internal virtual bool EqualsIgnoringPerson(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));
}
#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.EqualsIgnoringAddress(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)) // <-- No stack overflow!
{
Console.WriteLine("The two addresses are equal");
}
Person person2 = new Person("John", "Doe", address2);
address2.Resident = person2;
if (address1.Equals(address2)) // <-- No a stack overflow!
{
Console.WriteLine("The two addresses are equal");
}
Console.Read();
}
}
}
Выход
Два адреса равны.
Два адреса равны.
Вы слишком тесно связываете классы и смешиваете значения и ссылки. Вы должны рассмотреть возможность проверки ссылочного равенства для одного из классов или сделать так, чтобы они знали друг друга (предоставив internal
специализированный Equals
метод для определенного класса или проверка вручную значения равенства другого класса). Это не должно иметь большого значения, так как ваши требования явно запрашивают эту связь, поэтому вы не вводите ее, выполняя это.
Я думаю, что лучшее решение здесь - это разделить класс Address на две части.
- Основная информация об адресе (скажем, адрес)
1 + Персональная информация (скажем, OccupiedAddress)
Тогда было бы довольно просто в классе Person сравнить информацию об адресе ядра без создания SO.
Да, это создает некоторую связь в вашем коде, потому что Person теперь будет иметь немного внутренних знаний о том, как работает OccupiedAddress. Но у этих классов уже есть тесная связь, так что на самом деле вы сделали проблему не хуже.
Идеальным решением было бы полностью развязать эти классы.
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); // wrong
this.FirstName.Equals(person.FirstName)
this.LastName.Equals(person.LastName)
// and so on
}
Я бы сказал, не называйте 'this.Resident.Equals(other.Resident));'
По одному адресу может проживать более одного человека, поэтому проверка жильца неверна. Адрес - это адрес независимо от того, кто там живет!
Не зная вашего домена, это трудно подтвердить, но определение равенства между двумя родителями на основе их отношений с детьми кажется им вонючим!
Ваши родители действительно не могут идентифицировать себя, не проверяя своих детей? У ваших детей действительно есть уникальное удостоверение личности, или они сами, или они действительно определены их родителями и их отношениями с их братьями и сестрами?
Если у вас есть какая-то уникальная иерархия, которая уникальна только благодаря своим отношениям, я бы предложил, чтобы ваши тесты на равенство вернулись к корню и сделали проверку на равенство на основе самого отношения дерева.