Searching a Tree of Objects with Linq
I've finally had legitimate use for LINQ to Objects, not just to make the syntax cleaner, but also to simplify the underlying code and provide me a lot of flexibility without significant work.The scenario
I have a tree of objects that have both a type and a name. The name is unique, the Type is not. The interface is this:
public interface INode { int Id { get; } string Name { get; set; } string Type { get; set; } List<INode> Children { get; } }
I want to be able to find a single named Node in the tree and I want to be able to retrieve a collection of all nodes for a particular type. The searchable interface could be expressed as this:
public interface ISearchableNode : INode { INode FindByName(string name); IEnumerable<INode> FindByType(string name); }
The signature for the evaluation delegate looks like this:
delegate bool FindDelegate(INode node);
FindDelegate, I can simply use Func<INode,bool>:
// Instead of this: private void Find(INode node, List<INode> resultSet, FindDelegate findDelegate); // called like this for a Name search: Find(this, resultSet, delegate(INode node) { return node.Name == name; }); // I can use this: private void Find(INode node, List<INode> resultSet, Func<INode, bool> f) // called like this: Find(this, resultSet, node => node.Name == name);
Thus giving me the following implementation for Problem solved, move on... Well, except there is significant room for improvement. Here are the two main issues that ought to be resolved:
In order to use LINQ to objects, I need to either create a custom query provider or implement IEnumerable. The latter is significantly simpler and could be expressed using the following interface:
Searching for all Nodes of a type becomes
As it turns out, using yield for the recursive iteration also solved the second issue. As yield returns values as it encounters them during iteration, the search doesn't happen until the query is executed. And one of the side effects of LINQ syntax is that creating a query does not execute it until the result set is iterated over. Therefore, ISearchableNode:
public INode FindByName(string name)
{
List<INode> resultSet = new List<INode>();
Find(this, resultSet, x => x.Name == name);
return resultSet.FirstOrDefault();
}
public IEnumerable<INode> FindByType(string type)
{
List<INode> resultSet = new List<INode>();
Find(this, resultSet, x => x.Type == type);
return (IEnumerable<INode>)resultSet;
}
private void Find(INode node, List<INode> resultSet, Func<INode, bool> f)
{
if (f(node))
{
resultSet.Add(node);
}
foreach (INode child in node.Children)
{
Find(child, resultSet, f);
}
}
LINQ to Hierarchical Data
public interface IQueryableNode : IEnumerable<INode>
{
}
#region IEnumerable<Node> Members
public IEnumerator<INode> GetEnumerator()
{
yield return this;
foreach (Node child in Children)
{
foreach (Node subchild in child)
{
yield return subchild;
}
}
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
var allBar = from n in x
where n.Type == "bar"
select n;
foreach (Node n in allBar)
{
// do something with that node
}
INode node = (from n in x
where n.Name == "i"
select n).FirstOrDefault();
Deferred execution
FirstOrDefault() actually short-circuits the query as soon as the first match (and in case of Name, it's going to be the only match) is hit.

5 Comments:
Great stuff !!!! Exactly what I looked for.
This looks like exactly what I need. But when I use it I notice that LINQ never seems to call the iterator. For example, I have a List of objects that each have a Children property, which is also a List. When I use LINQ to query the main List, it only looks at the parent level of ther objects. It never iterates through the children. Any ideas?
Thanks,
Mike
LINQ won't traverse the hierarchy automatically. I.e. if you have have your children as regular lists, then it will only stay at that level. That's where the custom IEnumerable<INode> comes in and the GetEnumerator does the hierarchical dive.
So you're node can still have it's children as a list, but needs to implement IEnumerable<INode> and provide a custom enumerator that can dive into the children recursively.
Thanks ether.
I may have misunderstood, but I'm suppose to have my base object ("node") implement IEnumerable right?
Here is an example of what I am trying (please excuse my use of []. I am just trying to get blogger to accept the comment):
MyObject mo =
(from item in items
where (item.ID == 22)
select item).FirstOrDefault();
Where items is a list of nested MyObjects.
Assuming this as my class definition:
public class MyObject : IEnumerable[MyObject]
{
int id = 0;
List[MyObject] children = new List[MyObject]();
int ID
{
get { return id; }
set { id = value; }
}
List[MyObject] Children
{
get { return children; }
set { children = value; }
}
public IEnumerator[MyObject]GetEnumerator()
{
yield return this;
foreach (MyObject child in Children)
{
foreach (MyObject subchild in child)
{
yield return subchild;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
ether,
Thanks again for your help. I solved my problem using something similar. I just used an extension method that took the list and the "property" to descend by and recursed the tree. Very similar to:
http://mutable.net/blog/archive/2008/05/23/using-linq-to-objects-for-recursion.aspx
Thanks again for the informative post.
Post a Comment
<< Home