C#中双向关联的价值平等

时间:2009-04-08 18:20:55

标签: c# .net equals iequatable

背景

我正在处理的C#项目中有两个在它们之间具有双向关联的对象。我需要能够检查值相等(vs引用相等)有多种原因(例如在集合中使用它们),因此我实现了IEquatable和相关函数。

假设

  • 我正在使用C#3.0,.NET 3.5和Visual Studio 2008(尽管对于相等比较例程问题无关紧要。)

约束

任何解决方案都必须:

  • 允许双向关联保持不变,同时允许检查值相等。
  • 允许类的外部用法从IEquatable调用Equals(Object obj)或Equals(T class)并接收正确的行为(例如在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 个答案:

答案 0 :(得分:2)

您将类紧密耦合并混合值和引用。您应该考虑检查其中一个类的引用相等性,或者让它们相互了解(通过为特定类提供internal专用Equals方法或手动检查另一个类的值相等性。这应该不是什么大问题,因为你的要求明确要求这种耦合,所以你不要这样做。

答案 1 :(得分:1)

如果可以重新设计类结构以删除双向关联,并减少与实现相关的问题数量,那么这是首选解决方案。

如果无法重新设计或引入相同或更大的实现问题,那么一种可能的解决方案是使用专门的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();
        }
    }
}

输出

  

两个地址相同。

     

两个地址相同。

答案 2 :(得分:0)

我认为这里最好的解决方案是将Address类拆分为两部分

  1. 核心地址信息(比如地址)
  2. 1 +人员信息(比如OccupiedAddress)

  3. 然后在Person类中比较核心地址信息而不创建SO会相当简单。

    是的,这确实会在代码中产生一些耦合,因为Person现在对于OccupiedAddress的工作方式有一些内在的了解。但是这些类已经紧密耦合,所以你真的让问题变得更糟。

    理想的解决方案是完全解耦这些类。

答案 3 :(得分:-1)

我会说,不要叫'this.Resident.Equals(other.Resident));'

不止一个人可以住在一个地址,所以检查居民是错的。地址是一个地址,无论谁住在那里!

在不了解您的域名的情况下,很难确认这一点,但根据孩子与孩子的关系来定义两个父母之间的平等似乎有点臭!

你的父母在没有检查孩子的情况下真的无法识别自己吗?您的孩子是否真的拥有一个独特的身份证明,或者他们是否真的由父母及其与兄弟姐妹的关系来定义?

如果你有某种独特的层次结构,这只是因为它的关系而是唯一的,我建议你的相等测试应该递归到根,并根据树关系本身进行相等检查。

答案 4 :(得分:-1)

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
}