等于和循环引用:如何解决无限递归?

我有一些包含几个字段的类。 我需要通过值来比较它们,例如,如果一个类的两个实例的字段包含相同的数据,则它们是相等的。 我已经重写了GetHashCodeEquals方法。

可能会发生这些类包含循环引用。

例如:我们想建立机构模型(如政府,体育俱乐部等)。 一个机构有一个名字。 Club是一个有名字和成员名单的机构。 每个成员都是一个Person ,有一个名字和一个喜欢的机构。 如果某个俱乐部的成员将该俱乐部作为他最喜欢的机构,我们有一个循环参考。

但循环引用与值相等一起导致无限递归。 这是一个代码示例:

interface IInstitution { string Name { get; } }

class Club : IInstitution
{
    public string Name { get; set; }
    public HashSet<Person> Members { get; set; }

    public override int GetHashCode() { return Name.GetHashCode() + Members.Count; }

    public override bool Equals(object obj)
    {
        Club other = obj as Club;
        if (other == null)
            return false;

        return Name.Equals(other.Name) && Members.SetEquals(other.Members);
    }
}

class Person
{
    public string Name { get; set; }
    public IInstitution FavouriteInstitution { get; set; }

    public override int GetHashCode() { return Name.GetHashCode(); }

    public override bool Equals(object obj)
    {
        Person other = obj as Person;
        if (other == null)
            return false;

        return Name.Equals(other.Name)
            && FavouriteInstitution.Equals(other.FavouriteInstitution);
    }
}

class Program
{
    public static void Main()
    {
        Club c1 = new Club { Name = "myClub", Members = new HashSet<Person>() };
        Person p1 = new Person { Name = "Johnny", FavouriteInstitution = c1 }
        c1.Members.Add(p1);

        Club c2 = new Club { Name = "myClub", Members = new HashSet<Person>() };
        Person p2 = new Person { Name = "Johnny", FavouriteInstitution = c2 }
        c2.Members.Add(p2);

        bool c1_and_c2_equal = c1.Equals(c2); // StackOverflowException!
            // c1.Equals(c2) calls Members.SetEquals(other.Members)
            // Members.SetEquals(other.Members) calls p1.Equals(p2)
            // p1.Equals(p2) calls c1.Equals(c2) 
    }
}

c1_and_c2_equal应该返回true ,实际上我们(人类)可以通过一点思考看出它们是否值相等,而不会遇到无限递归。 但是,我无法真正说出我们如何解释这一点。 但是,既然有可能,我希望有一种方法可以在代码中解决这个问题!

所以问题是: 如何检查值相等而不会遇到无限递归?

请注意,我需要通常解决循环引用,而不仅仅是上面的情况。 我将它称为2圈,因为c1引用了p1 ,而p1引用了c1 。 可以有其他的n圈,例如,如果一个俱乐部A有一个会员M其最喜欢的俱乐部B具有会员N其最喜欢的俱乐部是A 那将是一个4圈。 其他对象模型也可能允许奇数为n的n圈。 我正在寻找一种解决所有这些问题的方法,因为我不会提前知道n可以拥有哪些价值。


一个简单的解决方法(在RDBMS中使用)是使用一个唯一的Id来标识一个Person (任何类型)。 那么你不需要比较每一个其他的财产,你永远不会遇到这样的cuircular参考。

另一种方法是在Equals进行不同的比较,因此只需对Equals类型进行深度检查,而不是对引用类型进行深度检查。 您可以使用自定义比较器:

public class PersonNameComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null && y == null) return true;
        if (x == null || y == null) return false;
        if(object.ReferenceEquals(x, y)) return true;
        return x.Name == y.Name;
    }

    public int GetHashCode(Person obj)
    {
        return obj?.Name?.GetHashCode() ?? int.MinValue;
    }
}

现在您可以更改ClubEquals实施方式,以避免Members (人员)使用他们的深度支票,其中包括该机构,但仅限其Name

public override bool Equals(object obj)
{
    if (Object.ReferenceEquals(this, obj))
        return true;

    Club other = obj as Club;
    if (other == null)
        return false;

    var personNameComparer = new PersonNameComparer();
    return Name.Equals(other.Name) 
        && Members.Count == other.Members.Count 
        && !Members.Except(other.Members, personNameComparer).Any();
}

您注意到我无法使用SetEquals因为我的自定义比较器没有超载。


根据Dryadwoods的建议,我改变了Equals方法,以便我可以跟踪已经比较的项目。

首先,我们需要一个相等比较器来检查相应元素对的引用相等性:

public class ValuePairRefEqualityComparer<T> : IEqualityComparer<(T,T)> where T : class
{
    public static ValuePairRefEqualityComparer<T> Instance
        = new ValuePairRefEqualityComparer<T>();
    private ValuePairRefEqualityComparer() { }

    public bool Equals((T,T) x, (T,T) y)
    {
        return ReferenceEquals(x.Item1, y.Item1)
            && ReferenceEquals(x.Item2, y.Item2);
    }

    public int GetHashCode((T,T) obj)
    {
        return RuntimeHelpers.GetHashCode(obj.Item1)
            + 2 * RuntimeHelpers.GetHashCode(obj.Item2);
    }
}

这里是Club的修改Equals方法:

static HashSet<(Club,Club)> checkedPairs
    = new HashSet<(Club,Club)>(ValuePairRefEqualityComparer<Club>.Instance);

public override bool Equals(object obj)
{
    Club other = obj as Club;
    if (other == null)
        return false;

    if (!Name.Equals(other.Name))
        return;

    if (checkedPairs.Contains((this,other)) || checkedPairs.Contains((other,this)))
        return true;

    checkedPairs.Add((this,other));

    bool membersEqual = Members.SetEquals(other.Members);
    checkedPairs.Clear();
    return membersEqual;
}

Person的版本是类似的。 请注意,我将(this,other)添加到checkedPairs并检查是否包含(this,other)(other,this) ,因为在第一次调用c1.Equals(c2)后可能会发生这种情况,调用c2.Equals(c1)而不是c1.Equals(c2) 。 我不确定这是否真的发生,但由于我看不到SetEquals的实现,我相信这是一种可能性。

由于我对已经检查过的对使用静态字段并不满意(如果程序并发,它将不起作用!),我问了另一个问题:为调用堆栈创建最后一个变量。


对于我感兴趣的一般情况

- 我们有C1 ,..., Cn类,其中每个类都可以有任意数量的VALUES(如intstring ,...)以及任何其他类C1的引用。 。, Cn (例如,通过具有用于每种类型的CiICollection<Ci> ) -

问题“两个对象AB相等?” 在我这里描述的平等意义上,

似乎等同于

问题“对于两个有限的,定向的,连通的彩色图GH ,是否存在从GH的同构?

这是等值:

  • 图顶点对应于object s(类实例)
  • 图边对应于object的引用
  • 颜色对应于值的集合和类型本身(即,如果两个顶点的对应object具有相同类型和相同值,则两个顶点的颜色相同)
  • 这是一个NP难题,所以我认为我会放弃实施该计划的计划,而采用无循环参考的方法。

    链接地址: http://www.djcxy.com/p/40589.html

    上一篇: equals and circular references: how to resolve infinite recursion?

    下一篇: Transaction on rest api