Daniel Dyson
Daniel Dyson

Reputation: 13230

How do I break out of recursive IEnumerable<T> loops using yield break?

I have the following method that works well, except the yield break statement only breaks out of the current enumerator. I understand why this is the case, but I am drawing a blank over how to propogate the yield break up through the recursive stack.

    private static IEnumerable<Node> FindChildrenById(IEnumerable nodes, string parentText) {
        var en = nodes.GetEnumerator();
        var targetFound = false;
        while (en.MoveNext())  {
            var node = en.Current as Node;
            if (node != null) 
            {
                if (node.Parent == null && string.IsNullOrEmpty(parentText))
                {
                    //Returns the top level nodes if an empty parentIdis entered
                    targetFound = true;
                    yield return node;
                }
                else if (node.Parent != null && node.Parent.Text == parentText)
                {
                    //returns the nodes belonging to the parent
                    yield return node;
                }
                else
                {
                    //Recurse into the children to see whether one of these is the node to find
                    foreach (var nd in FindChildrenById(node.Nodes, parentText))
                    {
                        yield return nd;
                    }
                }
            }
        }
        if (targetFound)
        {
            yield break;
        }
    }

So when I have the following nodes and pass "Top 2 a" as the parentText...

Top 1
    Top 1 a
    Top 1 b
Top 2
    Top 2 a
       Top 2 aa
       Top 2 ab
       Top 2 ac
    Top 2 b
Top 3
    Top 3 a
    Top 3 b
Top 4

... then I get the result:

Top 2 aa
Top 2 ab
Top 2 ac

This is the correct result, however, when I step through my code, the outer-most loop continues to process Top 3 and Top 4. How do I break out of this outer loop?

Upvotes: 6

Views: 3672

Answers (3)

Rune FS
Rune FS

Reputation: 21752

If I got your code right, I guess the code below will solve your problem

private static IEnumerable<Node> FindChildrenById(IEnumerable nodes, string parentText)
    {
        var result =
               (from node in nodes
                where (node.Parent == null && string.IsNullOrEmpty(parentText))
                      || (node.Parent != null && node.Parent.Text == parentText)
                select node).TakeWhile(node => !(node.Parent == null && string.IsNullOrEmpty(parentText)));
        return result;
    }

It's built on two extension methods (see below) and should only iterate until your target found criteria is met

public static class IEnumerablExtensions
        {
            //Will iterate the graph in depth first order
            public static IEnumerable<TResult> Select<TResult>(this IEnumerable collection, Func<Node, TResult> selector)
            {
                foreach (var obj in collection)
                {
                    var node = obj as Node;
                    if (node != null)
                    {
                        yield return selector(node);
                        foreach (var n in node.Nodes.Select(selector))
                        {
                           yield return n;
                        }
                    }
                }
            }

        public static IEnumerable<Node> Where(this IEnumerable collection, Predicate<Node> pred)
        {
            foreach (var node in collection.Select(x => x)) //iterate the list in graph first order
            {
                if (pred(node))
                    yield return node;
            }
        }
    }

EDIT: There was an error in the Select method in the original posting (it didn't iterate the children of children) that is now corrected

Upvotes: 3

leppie
leppie

Reputation: 117300

//Returns the top level nodes if an empty parentIdis entered
targetFound = true;
yield return node;
yield break;

Will that work for you?

Update:

I have given it some more thought. This might be tricky with recursion. You will need to keep some state variable to break out of all the loops.

If C# had tail-recursion, I would suggest converting the code to CPS.

You could always write it in MSIL :)

Upvotes: 1

corvuscorax
corvuscorax

Reputation: 5940

I'm assuming that the function is actually named FindChildrenById, otherwise I can't see any recursion going on.

Under this assumption you can either use exceptions (which I would strongly recommend against), or return a KeyValuePair<bool, IEnumerable<Node>> where the bool part will be used to signal an early out up the chain.

Then, on the API level, expose a wrapper method that simply returns the IEnumerable<Node> part and throws way the bool part.

Here's an example, given the class Node:

public class Node
{
    List<Node> children;
    public string Text { get; set; }
    public List<Node> Children { get { return children ?? (children = new List<Node>()); } }
}

You can traverse and shortcut like this:

public class NodeTraverser
{
    private static KeyValuePair<bool, IEnumerable<Node>> GetChildrenById(string text, Node node)
    {
        if(node.Text == text)
        {
            return new KeyValuePair<bool,IEnumerable<Node>>(true, node.Children);
        }
        foreach(var child in node.Children)
        {
            var result = GetChildrenById(text, child);
            if(result.Key)
            {
                return result; // early out
            }
        }
        return new KeyValuePair<bool,IEnumerable<Node>>(false, Enumerable.Empty<Node>());
    }

    public static IEnumerable<Node> FindChildrenbyId(string text, Node root)
    {
        return GetChildrenById(text, root).Value; 
    }
}

Upvotes: 1

Related Questions