简体   繁体   English

C#:遍历对象图时避免无限递归

[英]C#: Avoid infinite recursion when traversing object graph

I have an object graph wherein each child object contains a property that refers back to its parent.我有一个对象图,其中每个子对象都包含一个指向其父对象的属性。 Are there any good strategies for ignoring the parent references in order to avoid infinite recursion?是否有忽略父引用以避免无限递归的好策略? I have considered adding a special [Parent] attribute to these properties or using a special naming convention, but perhaps there is a better way.我曾考虑为这些属性添加一个特殊的 [Parent] 属性或使用特殊的命名约定,但也许有更好的方法。

If the loops can be generalised (you can have any number of elements making up the loop), you can keep track of objects you've seen already in a HashSet and stop if the object is already in the set when you visit it.如果循环可以泛化(你可以有任意数量的元素组成循环),你可以跟踪你已经在HashSet看到的对象,并在你访问它时如果对象已经在集合中就停止。 Or add a flag to the objects which you set when you visit it (but you then have to go back & unset all the flags when you're done, and the graph can only be traversed by a single thread at a time).或者为您访问它时设置的对象添加一个标志(但是您必须在完成后返回并取消设置所有标志,并且图形一次只能由一个线程遍历)。

Alternatively, if the loops will only be back to the parent, you can keep a reference to the parent and not loop on properties that refer back to it.或者,如果循环只会返回到父级,您可以保留对父级的引用,而不是在引用它的属性上循环。

For simplicity, if you know the parent reference will have a certain name, you could just not loop on that property :)为简单起见,如果您知道父引用将具有特定名称,则不能在该属性上循环:)

What a coincidence;多么巧合; this is the topic of my blog this coming Monday.这是下周一我博客的主题。 See it for more details.有关更多详细信息,请参阅它。 Until then, here's some code to give you an idea of how to do this:在此之前,这里有一些代码可以让您了解如何执行此操作:

static IEnumerable<T> Traversal<T>(
    T item,
    Func<T, IEnumerable<T>> children)
{
    var seen = new HashSet<T>();
    var stack = new Stack<T>();
    seen.Add(item);
    stack.Push(item); 
    yield return item;
    while(stack.Count > 0)
    {
        T current = stack.Pop();
        foreach(T newItem in children(current))
        {
            if (!seen.Contains(newItem))
            {
                seen.Add(newItem);
                stack.Push(newItem);
                yield return newItem;
            }
        }
    } 
}

The method takes two things: an item, and a relation that produces the set of everything that is adjacent to the item.该方法需要两个东西:一个项目,以及一个产生与该项目相邻的所有东西的集合的关系。 It produces a depth-first traversal of the transitive and reflexive closure of the adjacency relation on the item .它对item 上的邻接关系的传递和自反闭包进行深度优先遍历 Let the number of items in the graph be n, and the maximum depth be 1 <= d <= n, assuming the branching factor is not bounded.假设图中的项数为 n,最大深度为 1 <= d <= n,假设分支因子无界。 This algorithm uses an explicit stack rather than recursion because (1) recursion in this case turns what should be an O(n) algorithm into O(nd), which is then something between O(n) and O(n^2), and (2) excessive recursion can blow the stack if the d is more than a few hundred nodes.该算法使用显式堆栈而不是递归,因为 (1) 在这种情况下,递归将应该是 O(n) 的算法变成了 O(nd),然后是 O(n) 和 O(n^2) 之间的某个东西, (2) 如果 d 超过几百个节点,过度的递归会炸毁堆栈。

Note that the peak memory usage of this algorithm is of course O(n + d) = O(n).请注意,该算法的峰值内存使用量当然是 O(n + d) = O(n)。

So, for example:因此,例如:

foreach(Node node in Traversal(myGraph.Root, n => n.Children))
  Console.WriteLine(node.Name);

Make sense?有道理?

If you're doing a graph traversal, you can have a "visited" flag on each node.如果您正在进行图形遍历,则可以在每个节点上都有一个“已访问”标志。 This ensures that you don't revisit a node and possibly get stuck in an infinite loop.这可确保您不会重新访问节点并可能陷入无限循环。 I believe this is the standard way of performing a graph traversal.我相信这是执行图遍历的标准方法。

This is a common problem, but the best approach depends on the scenario.这是一个常见问题,但最佳方法取决于场景。 An additional problem is that in many cases it isn't a problem visiting the same object twice - that doesn't imply recursion - for example, consider the tree:另一个问题是,在许多情况下,两次访问同一个对象不是问题——这并不意味着递归——例如,考虑树:

A
=> B
   => C
=> D
   => C

This may be valid (think XmlSerializer , which would simply write the C instance out twice), so it is often necessary to push/pop objects on a stack to check for true recursion.这可能是有效的(想想XmlSerializer ,它会简单地将C实例写出两次),因此通常需要在堆栈上推送/弹出对象以检查真正的递归。 The last time I implemented a "visitor", I kept a "depth" counter, and only enabled the stack checking beyond a certain threshold - that means that most trees simply end up doing some ++ / -- , but nothing more expensive.上次我实现了“访问者”时,我保留了一个“深度”计数器,并且只启用了超过某个阈值的堆栈检查——这意味着大多数树最终只会做一些++ / -- ,但没有什么比这更昂贵的了。 You can see the approach I took here .你可以看到我在这里采取的方法。

我不确定您在这里要做什么,但是当您进行深度优先搜索的广度优先搜索时,您可以只维护一个包含所有先前访问过的节点的哈希表。

I published a post explaining in detail with code examples how to do object traversal by recursive reflection and also detect and avoid recursive references to prevent a stack over flow exception: https://doguarslan.wordpress.com/2016/10/03/object-graph-traversal-by-recursive-reflection/我发表了一篇文章,用代码示例详细解释了如何通过递归反射进行对象遍历,以及如何检测和避免递归引用以防止堆栈溢出异常: https : //doguarslan.wordpress.com/2016/10/03/object -graph-traversal-by-recursive-reflection/

In that example I did a depth first traversal using recursive reflection and I maintained a HashSet of visited nodes for reference types.在那个例子中,我使用递归反射进行了深度优先遍历,并为引用类型维护了一个访问节点的 HashSet。 One thing to be careful is to initialize your HashSet with your custom equality comparer which uses the object reference for hash calculation, basically the GetHashCode() method implemented by the base object class itself and not any overloaded versions of GetHashCode() because if the types of properties you traverse overload GetHashCode method, you may detect false hash collisions and think that you detected a recursive reference which in reality could be that the overloaded version of GetHashCode producing the same hash value via some heuristics and confusing the HashSet, all you need to detect is to check if there is any parent child in anywhere in the object tree pointing to the same location in memory.需要注意的一件事是使用自定义相等比较器初始化您的 HashSet,该比较器使用对象引用进行哈希计算,基本上是由基对象类本身实现的 GetHashCode() 方法,而不是 GetHashCode() 的任何重载版本,因为如果类型您遍历的属性重载 GetHashCode 方法,您可能会检测到错误的哈希冲突并认为您检测到了递归引用,这实际上可能是 GetHashCode 的重载版本通过一些启发式方法产生相同的哈希值并混淆 HashSet,所有您需要检测是检查对象树中的任何位置是否有任何父子节点指向内存中的相同位置。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM