This sounds like a case where you can finally apply that interview trivia question: find a cycle in a linked list using only O(1) memory.
In this case your "linked list" is the sequence of elements you enumerate. Use two enumerators, run one at half speed, and if the fast one ever runs into the slow one then you have a loop. This will also be O(n) time instead of the O(n^2) time required for checking a 'seen' list. The downside is you only find out about the loop after some of the nodes have been processed multiple times.
In the example I've replaced the 'half speed' method with the simpler-to-write 'drop markers' method.
class GenTreeNode {
...
///<summary>Wraps an the enumeration of linked data structures such as trees and linked lists with a check for cycles.</summary>
private static IEnumerable<T> CheckedEnumerable<T>(IEnumerable<T> sub_enumerable) {
long cur_track_count = 0;
long high_track_count = 1;
T post = default(T);
foreach (var e in sub_enumerable) {
yield return e;
if (++cur_track_count >= high_track_count) {
post = e;
high_track_count *= 2;
cur_track_count = 0;
} else if (object.ReferenceEquals(e, post)) {
throw new Exception("Infinite Loop");
}
}
}
...
///<summary>Enumerates the tree's nodes, assuming no cycles</summary>
private IEnumerable<GenTreeNode> tree_nodes_unchecked() {
yield return this;
foreach (var child in this.nodes)
foreach (var e in child.tree_nodes_unchecked())
yield return e;
}
///<summary>Enumerates the tree's nodes, checking for cycles</summary>
public IEnumerable<GenTreeNode> tree_nodes()
{
return CheckedEnumerable(tree_nodes_unchecked());
}
...
void ProcessTree() {
foreach (var node in tree_nodes())
proceess(node);
}
}