我想构建一个这样的树结构:
root Id 1
child id 2
grandChild id 3
下面的代码示例。如果我使用GetChildrenNodesCorrect(),
,我会得到正确的结果。但是当使用GetChildrenNodesWrong()
时,它会返回以下内容:
root Id 1
child id 2
Null
我知道ToList()
不是延迟执行,并立即返回结果。有人能解释一下吗?
public class ToListTest
{
public static void Entry()
{
var toListTest = new ToListTest();
toListTest.Test();
}
public void Test()
{
List<Node> newsList = new List<Node>
{
new Node{Id = 1, ParentId = 0},
new Node{Id = 2, ParentId = 1},
new Node{Id = 3, ParentId = 2}
};
var root = BuildUpTree(newsList);
}
private TreeNode BuildUpTree(List<Node> newsList)
{
var root = new TreeNode { currentNode = newsList.First(n => n.ParentId == 0) };
BuildUpTreeChildrenNodes(newsList, root);
return root;
}
private void BuildUpTreeChildrenNodes(List<Node> newsList, TreeNode currentTreeNode)
{
currentTreeNode.Children = GetChildrenNodesWrong(newsList, currentTreeNode);
foreach (var node in currentTreeNode.Children)
{
BuildUpTreeChildrenNodes(newsList, node);
}
}
private IEnumerable<TreeNode> GetChildrenNodesWrong(List<Node> newsList, TreeNode cuurentNode)
{
return newsList.Where(n => n.ParentId == cuurentNode.currentNode.Id)
.Select(n => new TreeNode
{
currentNode = n
});
}
private IEnumerable<TreeNode> GetChildrenNodesCorrect(List<Node> newsList, TreeNode cuurentNode)
{
return GetChildrenNodesWrong(newsList, cuurentNode).ToList();
}
public class TreeNode
{
public Node currentNode { get; set; }
public IEnumerable<TreeNode> Children { get; set; }
}
public class Node
{
public int Id { get; set; }
public int ParentId { get; set; }
}
}
更新
在调试中,当使用GetChildrenNodesWrong(),
root时,在方法返回之前同时拥有子孙。方法返回后,root只有child,而grandchild为null。
更新2
IMO,问题可能与清洁代码无关。但欢迎任何人展示更直观的代码。
答案 0 :(得分:1)
每次评估IEnumerable
时,都会重新执行Linq查询。因此,当您计算树时,它会为节点分配空间,但不会将它们分配给任何永久变量。这意味着在foreach
中的BuildUpTreeChildrenNodes
循环中,您没有在所需节点的实例上调用递归函数。相反,您在由foreach
循环(枚举IEnumerable
)创建的节点的重新实例化版本上调用它。当您在ToList
上调用IEnumerable
时,foreach
循环将返回列表中的元素,这些元素位于内存中。
如果你使root
公共静态,然后调试你的代码,你会看到当你调用BuildUpTreeChildrenNodes
时,node
参数不是你的节点的实例想。即使它具有相同的ID并且表示图中的相同节点,它实际上也不以任何方式连接到根节点。检查:
root.Children.Any(n => n.Id == node.Id) //true
root.Children.Contains(node) //false
查看问题的最简单方法是:
//Create a singleton Node list:
var nodeSingleton= Enumerable.Range(0, 1).Select(x => new Node { Id = x });
Console.Write(nodeSingleton.Single() == nodeSingleton.Single());
您可能希望这返回true
,但实际上它将是false
- 两次调用Single
Linq方法时,单例变量的延迟执行是已评估,并返回Node
类的不同实例。
但是,如果您在单身人士上拨打ToList
,那么您将获得内存中的列表,而Single
方法将返回Node
的相同实例。
更广泛地说,我认为这段代码的问题在于它过多地混淆了命令式和功能性代码。很奇怪,很多方法都是void
,然后GetChildrenNodesWrong
方法不是。我认为你应该选择一种风格并坚持下去,因为切换范式可能会令人困惑。
答案 1 :(得分:0)
我不完全确定你在问什么。所有LINQ查询都延迟执行,当您调用ToList()
时,您只是强制查询执行。我认为主要问题在于你的where子句。只有两个对象满足条件,因此LINQ查询返回的IEnumerable应该只有2个对象。
由于GetChildrenNodesWrong
中的LINQ查询产生“off by one”错误,因此它没有达到预期效果。这基本上是发生了什么;
1)我们为root用户提供n = root没有任何反应。我们移动到下一个节点。
2)n.Id = 1,节点2满足where条件,因为它的parentId是1.我们分配一个新节点,将节点电流指向节点2
3)我们现在到达第三个节点。 n.ParentId = 2和current.Id = 2.我们有一个匹配,所以我们分配了另一个节点并将电流指向节点3.
4)我们在列表的末尾。大孩子永远不会被分配,因为我们已经离开了。
基本上你迭代x时间,其中x是列表的长度但是因为第一次迭代时current = n你没有分配一个节点,所以当你期望x时,你最终得到x -1个节点。