Соотношение значений с двунаправленной ассоциацией в C # - PullRequest
4 голосов
/ 08 апреля 2009

Фон

У меня есть два объекта, которые имеют двунаправленную связь между ними в проекте 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 ]

2 голосов
/ 08 апреля 2009

Вы слишком тесно связываете классы и смешиваете значения и ссылки. Вам следует либо рассмотреть проверку равенства ссылок для одного из классов, либо информировать их друг о друге (предоставив internal специализированный метод Equals для конкретного класса или вручную проверив равенство значений другого класса). Это не должно иметь большого значения, так как ваши требования явно запрашивают эту связь, поэтому вы не вводите ее, делая это.

1 голос
/ 08 апреля 2009

Если возможна перестройка структуры классов для удаления двунаправленной ассоциации и уменьшения количества проблем, связанных с реализацией, то это предпочтительное решение.

Если этот редизайн невозможен или создает проблемы с реализацией равных или более серьезных проблем, то одним из возможных решений является использование специализированного метода Equals, который вызывается методами Equals классов, участвующих в двунаправленной ассоциации. Как сказал Мердад, это не должно быть слишком сложным делом, так как требования явно запрашивают эту связь, поэтому вы не вводите ее, делая это.


код

Вот реализация этого, которая сохраняет специализированные методы, проверяя только свои собственные поля. Это уменьшает проблемы с обслуживанием по сравнению с тем, что каждый класс выполняет сравнение для каждого класса для каждого свойства.

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();
        }
    }
}

Выход

Два адреса равны.

Два адреса равны.

0 голосов
/ 08 апреля 2009

Я думаю, что лучшее решение здесь - это разделить класс Address на две части

  1. Основная информация об адресе (скажем, адрес)
  2. 1 + Персональная информация (скажем, OccupiedAddress)

Тогда в классе Person было бы довольно просто сравнить информацию об основном адресе без создания SO.

Да, это создает некоторую связь в вашем коде, потому что Person теперь будет иметь немного внутренних знаний о том, как работает OccupiedAddress. Но у этих классов уже есть тесная связь, так что на самом деле вы сделали проблему не хуже.

Идеальным решением было бы полное разделение этих классов.

0 голосов
/ 08 апреля 2009
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
}
0 голосов
/ 08 апреля 2009

Я бы сказал, не называйте 'this.Resident.Equals (other.Resident));'

Более одного человека могут проживать по одному адресу, поэтому проверка жильца неверна. Адрес - это адрес независимо от того, кто там живет!

Не зная вашего домена, трудно это подтвердить, но определение равенства между двумя родителями на основе их отношений с детьми кажется им немного вонючим!

Ваши родители действительно не могут идентифицировать себя без проверки своих детей? У ваших детей действительно есть уникальное удостоверение личности, или они сами, или они действительно определены их родителями и их отношениями с их братьями и сестрами?

Если у вас есть какая-то уникальная иерархия, которая уникальна только благодаря своим отношениям, я бы предложил, чтобы ваши тесты на равенство вернулись к корню и сделали проверку на равенство на основе самого отношения дерева.

...