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.
