【问题标题】:Function which will return particular node from tree structure从树结构返回特定节点的函数
【发布时间】:2015-08-18 02:08:52
【问题描述】:

我正在编写将从树结构返回特定节点的函数。但是当我使用 LINQ 在树中搜索时,它会在第一个分支中搜索,最后当它到达叶子时,它会抛出空引用异常,因为叶子没有任何子节点。

这是我的课,

public class Node
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Content { get; set; }
        public IEnumerable<Node> Children { get; set; }
        public IEnumerable<Node> GetNodeAndDescendants() // Note that this method is lazy
        {
            return new[] { this }
                   .Concat(Children.SelectMany(child => child.GetNodeAndDescendants()));
        }
    }

这就是我调用这个函数的方式,

 var foundNode = Location.GetNodeAndDescendants().FirstOrDefault(node => node.Name.Contains("string to search"));

var foundNode = Location.GetNodeAndDescendants().FirstOrDefault(node => node.Id==123)

这样做的正确方法是什么?任何示例代码都将不胜感激

【问题讨论】:

  • 为什么一开始就让代码遇到 NullReferenceException?叶子不应该返回一个包含零元素而不是 null 的 Children 集合,从而避免 NullReferenceException 吗?无需代码示例 - 只需查看 NullReferenceException 的堆栈跟踪以了解导致它的原因,然后从那里开始研究如何纠正代码的行为...
  • 哦,按照@elgonzo 的建议工作。

标签: c# linq recursion tree


【解决方案1】:

编写自己的函数没有错,但是基于 LINQ 或递归迭代器的实现不是一个好主意(性能!)。但是为什么要依赖外部库呢?很多你不需要的代码,实现接口,修改你的类等。编写一个通用函数来遍历预序树并将其用于任何树结构并不难。下面是我参与How to flatten tree via LINQ?的修改版(没什么特别的,普通的迭代实现):

public static class TreeHelper
{
    public static IEnumerable<T> PreOrderTraversal<T>(T node, Func<T, IEnumerable<T>> childrenSelector)
    {
        var stack = new Stack<IEnumerator<T>>();
        var e = Enumerable.Repeat(node, 1).GetEnumerator();
        try
        {
            while (true)
            {
                while (e.MoveNext())
                {
                    var item = e.Current;
                    yield return item;
                    var children = childrenSelector(item);
                    if (children == null) continue;
                    stack.Push(e);
                    e = children.GetEnumerator();
                }
                if (stack.Count == 0) break;
                e.Dispose();
                e = stack.Pop();
            }
        }
        finally
        {
            e.Dispose();
            while (stack.Count != 0) stack.Pop().Dispose();
        }
    }
}

你在class Node里面的函数变成了:

public IEnumerable<Node> GetNodeAndDescendants() // Note that this method is lazy
{
    return TreeHelper.PreOrderTraversal(this, node => node.Children);
}

其他一切都保持您所做的方式,并且应该可以正常工作。

编辑:看起来你需要这样的东西:

public interface IContainer
{
    // ...
}

public class CustomerNodeInstance : IContainer
{
    // ...
}

public class ProductNodeInstance : IContainer
{
    // ...
}

public class Node : IContainer
{
    // ...
    public IEnumerable<IContainer> Children { get; set; }
    public IEnumerable<IContainer> GetNodeAndDescendants() // Note that this method is lazy
    {
        return TreeHelper.PreOrderTraversal<IContainer>(this, item => { var node = item as Node; return node != null ? node.Children : null; });
    }
}

【讨论】:

  • 我现在的问题是树的叶节点将不是类型 Node ,它可以是类型 CustomerNodeInstance 或 ProductNodeInstance 所有类型(CustomerNodeInstance,Node,ProductNodeInstance )都实现 IContainer 接口。所以任何一个孩子都可以是实例或节点。有没有更好的方法。
  • CustomerNodeInstance 和 ProductNodeInstance 是否继承自 Node? IContainer 接口是什么样的?
  • 不,CustomerNodeInstance 和 ProductNodeInstance 不继承自节点,但它们正在实现接口 IContainer。因此,Node、CustomerNodeInstance 和 ProductNodeInstance 实现了 IContainer。并且 IContainer 有 ContainerType 枚举,它说明了它的类型,无论它是 Node 或 CustomerNodeInstance 还是 ProductNodeInstance 类型。
  • 如果我理解正确,您希望这些叶节点包含在函数结果中。我很确定这是可能的,但是 Node.Children 属性不应该是 IEnumerable&lt;IContainer&gt; 类型吗?
  • 是的,它是正确的。 Node 的子级为 IEnumerable,并且 Node 可以有 Node、CustomerNodeInstance 和 ProductNodeInstance 作为子级。因此,Node 可以有多个节点或 Nodes+CustomerNodeInstance 或 Nodes+CustomerNodeInstance+ProductNodeInstance 或 CustomerNodeInstance+ProductNodeInstance 等作为子节点。不幸的是,我没有得到任何适当的解决方案。目前我只是在使用 stack(push/pop) ,这会影响性能。
【解决方案2】:

如果您不介意依赖第三方解决方案,我有一个我一直在研究的轻量级库,它可以用几乎任何树完成这个和许多其他事情。它被称为Treenumerable。你可以在 GitHub 上找到它:https://github.com/jasonmcboyd/Treenumerable;以及 NuGet 上的最新版本(此时为 1.2.0):http://www.nuget.org/packages/Treenumerable。它具有良好的测试覆盖率,并且看起来很稳定。

它确实需要您创建一个辅助类,该类使用两种方法实现ITreeWalker 接口:TryGetParentGetChildren。正如您可能猜到的那样,TryGetParent 获取节点的父节点,因此您的 Node 类必须以一种知道其父节点的方式进行修改。我想你可以在TryGetParent 中抛出一个NotSupported 异常,因为该方法对于任何遍历操作都不是必需的。无论如何,不​​管你走哪条路,下面的代码都会做你想做的事:

ITreeWaler<Node> walker;
// Don't forget to instantiate 'walker'.

var foundNode =
    walker
    .PreOrderTraversal(Location)
    .FirstOrdefault(node => node.Name.Contains("string to search"));

我的实现和你的实现之间值得一提的区别是我的实现不依赖于递归。这意味着您不必担心深树会抛出StackOverflowException

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-03
    • 2018-10-31
    • 1970-01-01
    • 1970-01-01
    • 2021-07-04
    • 2021-10-26
    • 2015-02-14
    相关资源
    最近更新 更多