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

时间:2021-05-14 16:10:58

Background

I have two objects which have bidirectional association between them in a C# project I am working on. I need to be able to check for value equality (vs reference equality) for a number of reasons (e.g to use them in collections) and so I am implementing IEquatable and the related functions.

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

Assumptions

  • I am using C# 3.0, .NET 3.5, and Visual Studio 2008 (although it shouldn't matter for equality comparison routine issue).
  • 我正在使用C#3.0,.NET 3.5和Visual Studio 2008(尽管它对于相等比较例程问题无关紧要)。

Constraints

Any solution must:

任何解决方案必须

  • Allow for bidirectional association to remain intact while permitting checking for value equality.
  • 允许双向关联保持不变,同时允许检查值相等。

  • Allow the external uses of the class to call Equals(Object obj) or Equals(T class) from IEquatable and receive the proper behavior (such as in System.Collections.Generic).
  • 允许类的外部用法从IEquatable调用Equals(Object obj)或Equals(T class)并接收正确的行为(例如在System.Collections.Generic中)。

Problem

When implementing IEquatable to provide checking for value equality on types with bidirectional association, infinite recursion occurs resulting in a stack overflow.

当实现IEquatable以提供具有双向关联的类型的值相等性检查时,会发生无限递归,从而导致堆栈溢出。

NOTE: Similarly, using all of the fields of a class in the GetHashCode calculation will result in a similar infinite recursion and resulting stack overflow issue.

注意:类似地,在GetHashCode计算中使用类的所有字段将导致类似的无限递归并导致堆栈溢出问题。


Question

How do you check for value equality between two objects which have bidirectional association without resulting in a stack overflow?

如何检查两个具有双向关联的对象之间的值相等而不会导致堆栈溢出?


Code

NOTE: This code is notional to display the issue, not demonstrate the actual class design I'm using which is running into this problem

注意:此代码用于显示问题,而不是演示我正在使用的实际类设计遇到此问题

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 个解决方案

#1


You are coupling classes too tightly and mixing values and references. You should either consider checking reference equality for one of the classes or make them aware of each other (by providing an internal specialized Equals method for the specific class or manually checking value equality of the other class). This shouldn't be a big deal since your requirements explicitly ask for this coupling so you are not introducing one by doing this.

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

#2


If redesigning the class structure to remove the bidirectional association is possible and reduces the number of problems associated with the implementation, then this is preferred solution.

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

If this redesign is not possible or introduces equal or greater implementation issues, then one possible solution is to use a specialized Equals method to be called by Equals methods of the classes involved in the bidirectional association. As Mehrdad stated, this shouldn't be too big of a deal since the requirements explicitly ask for this coupling, so you are not introducing one by doing this.

如果无法重新设计或引入相同或更大的实现问题,那么一种可能的解决方案是使用专门的Equals方法,由双向关联中涉及的类的Equals方法调用。正如Mehrdad所说,由于要求明确要求这种耦合,因此这不应该太大,所以你不要通过这样做来引入一个。


Code

Here is an implementation of this that keeps the specialized methods checking only their own fields. This reduces maintenance problems vs having each class do a per-property comparison of the other class.

这是一个实现,它使专门的方法只检查自己的字段。这减少了维护问题,而不是让每个类对另一个类进行每个属性的比较。

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

Output

The two addresses are equal.

这两个地址是相同的。

The two addresses are equal.

这两个地址是相同的。

#3


I think the best solution here is to break up the Address class into two parts

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

  1. Core Address Information (say Address)
  2. 核心地址信息(比如地址)

  3. 1 + Person Information (say OccupiedAddress)

  4. 1 +人信息(比如OccupiedAddress)

Then it would be fairly simple in the Person class to compare the core address information without creating a SO.

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

Yes this does create a bit of coupling in your code because Person will now have a bit of inner knowledge about how OccupiedAddress works. But these classes already have tight coupling so really you've made the problem no worse.

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

The ideal solution would be to completely decouple these classes.

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

#4


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
}

#5


I would say, don't call 'this.Resident.Equals(other.Resident));'

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

More than one person can live at an address so checking the resident is wrong. An address is an address regardless of who is living there!

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

Without knowing your domain, it's hard to confirm this, but defining equality between two parents based on their childrens relationship back to them seems a bit smelly!

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

Do your parents really have no way of identifying themselves without checking their children? Do your children really have a unique ID in, and of themselves, or are they really defined by their parent and its relationship to their siblings?

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

If you have some kind of unique hierarchy, that is unique only because of its relationships, I would suggest your equality tests should recurse to the root, and make an equality check based on the tree relationship itself.

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

#1


You are coupling classes too tightly and mixing values and references. You should either consider checking reference equality for one of the classes or make them aware of each other (by providing an internal specialized Equals method for the specific class or manually checking value equality of the other class). This shouldn't be a big deal since your requirements explicitly ask for this coupling so you are not introducing one by doing this.

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

#2


If redesigning the class structure to remove the bidirectional association is possible and reduces the number of problems associated with the implementation, then this is preferred solution.

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

If this redesign is not possible or introduces equal or greater implementation issues, then one possible solution is to use a specialized Equals method to be called by Equals methods of the classes involved in the bidirectional association. As Mehrdad stated, this shouldn't be too big of a deal since the requirements explicitly ask for this coupling, so you are not introducing one by doing this.

如果无法重新设计或引入相同或更大的实现问题,那么一种可能的解决方案是使用专门的Equals方法,由双向关联中涉及的类的Equals方法调用。正如Mehrdad所说,由于要求明确要求这种耦合,因此这不应该太大,所以你不要通过这样做来引入一个。


Code

Here is an implementation of this that keeps the specialized methods checking only their own fields. This reduces maintenance problems vs having each class do a per-property comparison of the other class.

这是一个实现,它使专门的方法只检查自己的字段。这减少了维护问题,而不是让每个类对另一个类进行每个属性的比较。

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

Output

The two addresses are equal.

这两个地址是相同的。

The two addresses are equal.

这两个地址是相同的。

#3


I think the best solution here is to break up the Address class into two parts

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

  1. Core Address Information (say Address)
  2. 核心地址信息(比如地址)

  3. 1 + Person Information (say OccupiedAddress)

  4. 1 +人信息(比如OccupiedAddress)

Then it would be fairly simple in the Person class to compare the core address information without creating a SO.

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

Yes this does create a bit of coupling in your code because Person will now have a bit of inner knowledge about how OccupiedAddress works. But these classes already have tight coupling so really you've made the problem no worse.

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

The ideal solution would be to completely decouple these classes.

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

#4


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
}

#5


I would say, don't call 'this.Resident.Equals(other.Resident));'

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

More than one person can live at an address so checking the resident is wrong. An address is an address regardless of who is living there!

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

Without knowing your domain, it's hard to confirm this, but defining equality between two parents based on their childrens relationship back to them seems a bit smelly!

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

Do your parents really have no way of identifying themselves without checking their children? Do your children really have a unique ID in, and of themselves, or are they really defined by their parent and its relationship to their siblings?

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

If you have some kind of unique hierarchy, that is unique only because of its relationships, I would suggest your equality tests should recurse to the root, and make an equality check based on the tree relationship itself.

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