4

背景

在我正在处理的 C# 项目中,我有两个对象之间具有双向关联。由于多种原因(例如,在集合中使用它们),我需要能够检查值相等(与引用相等),因此我正在实现 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();
        }
    }
}
4

5 回答 5

2

您将类耦合得太紧并且混合了值和引用。您应该考虑检查其中一个类的引用相等性或使它们彼此了解(通过为特定类提供internal专门的Equals方法或手动检查另一个类的值相等性)。这应该没什么大不了的,因为您的要求明确要求这种耦合,因此您不会通过这样做来引入一个。

于 2009-04-08T18:22:17.857 回答
1

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

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

那么在 Person 类中比较核心地址信息而不创建 SO 将是相当简单的。

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

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

于 2009-04-08T18:52:58.533 回答
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();
        }
    }
}

输出

两个地址相等。

两个地址相等。

于 2009-04-08T19:32:32.710 回答
-1

我会说,不要调用 'this.resident.Equals(other.Resident));'

一个地址可以住不止一个人,因此检查居民是错误的。一个地址就是一个地址,不管谁住在那里!

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

如果不检查孩子,您的父母真的无法识别自己吗?您的孩子是否真的拥有自己的唯一 ID,或者他们真的由父母及其与兄弟姐妹的关系定义?

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

于 2009-04-08T18:25:37.943 回答
-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
}
于 2009-04-08T18:26:13.010 回答