如何查找具有给定名称的最高级别后代 [英] How to find highest level descendants with a given name
问题描述
我正在寻找一种使用linq在XML树中查找第一个结果级别的方法.我拥有的XML如下:
I am looking for a way to find the first result level in a XML tree using linq. The XML I have is like the following:
<column>
<row>
<object/>
<column column-id="1" column-name="abc">
<row>
<column>
<row>
<column column-id="2" column-name="abc"/>
</row>
</column>
</row>
</column>
</row>
<row>
<column column-id="3" column-name="abc">
<row>
<column/>
</row>
</column>
</row>
</column>
现在,我想获取所有第一级columns
,其中列名称为abc
.所以结果应该是:
Now I want to get all the first level columns
where the column-name is abc
. So the result should be:
<column column-id="1" column-name="abc">...</column>
<column column-id="3" column-name="abc">...</column>
我已经尝试了以下代码:
I have tried already the following code:
layout.Descendants("column")
.Where(x => x.Attribute("column-name").Value.Equals("abc") && !x.Ancestors("column").Any());
当正在搜索的XElement layout
未命名为"column"
并且未嵌套在任何名为"column"
的容器元素内时,此方法很好用.但实际上,我的XElement
确实属于其根元素名为"column"
的文档内,因此x.Ancestors("column").Any()
表达式错误地过滤了所有匹配项. IE.可以通过如下初始化layout
来使用上面的XML字符串重现该问题:
This works fine when the XElement layout
being searched is not named "column"
and is not nested inside any container elements named "column"
. But my XElement
does, in fact, belong inside a document whose root element is named "column"
, so the x.Ancestors("column").Any()
expression wrongly filters out all matches. I.e. the problem can be reproduced using the XML string above by initializing layout
as follows:
var layout = XElement.Parse(xmlString);
由于稍后需要进行更改,因此我希望将关系保留在变量中.
I want to keep the relation in the variable because of changes I have to make later on.
也许有办法限制祖先选择器吗?
Is there maybe a way limit the ancestors selector?
推荐答案
假设您事先不知道要查询的元素的精确深度,那么您想要做的就是将元素层次结构下移到指定的元素,并返回与给定条件匹配的 top 个元素,在这种情况下,其名称为"column"
.
Assuming you don't know in advance the precise depth of the elements for which you are querying, what you want to do is to descend the element hierarchy underneath a specified element, and return the topmost elements that match a given condition, in this case having the name "column"
.
As a quick-and-dirty way to do this, you can only check for ancestors of the candidate matched column that are still descendants of layout
by using TakeWhile()
var matches = layout
.Descendants("column")
.Where(x => (string)x.Attribute("column-name") == "abc" && !x.Ancestors().TakeWhile(a => a != layout).Any(a => a.Name == "column"));
一种性能更高的通用解决方案是在XElement
上引入一个扩展方法,该方法枚举给定元素的所有后代,并返回与给定谓词匹配的最顶层元素.这通常是有用的,例如如果要查询要在深层XML层次结构顶部附近的后代,则可以避免不必要地下降到匹配的节点中:
A more performant, general solution would be to introduce an extension method on XElement
that enumerates through all descendants of the given element, returning the topmost elements that match a given predicate. This would be generally useful e.g. in cases where one wants to query for descendants that are going to be near the top of a deep XML hierarchy, as it avoids descending unnecessarily into matched nodes:
public static partial class XElementExtensions
{
/// <summary>
/// Enumerates through all descendants of the given element, returning the topmost elements that match the given predicate
/// </summary>
/// <param name="root"></param>
/// <param name="filter"></param>
/// <returns></returns>
public static IEnumerable<XElement> DescendantsUntil(this XElement root, Func<XElement, bool> predicate, bool includeSelf = false)
{
if (predicate == null)
throw new ArgumentNullException();
return GetDescendantsUntil(root, predicate, includeSelf);
}
static IEnumerable<XElement> GetDescendantsUntil(XElement root, Func<XElement, bool> predicate, bool includeSelf)
{
if (root == null)
yield break;
if (includeSelf && predicate(root))
{
yield return root;
yield break;
}
var current = root.FirstChild<XElement>();
while (current != null)
{
var isMatch = predicate(current);
if (isMatch)
yield return current;
// If not a match, get the first child of the current element.
XElement next = (isMatch ? null : current.FirstChild<XElement>());
if (next == null)
// If no first child, get the next sibling of the current element.
next = current.NextSibling<XElement>();
// If no more siblings, crawl up the list of parents until hitting the root, getting the next sibling of the lowest parent that has more siblings.
if (next == null)
{
for (var parent = current.Parent as XElement; parent != null && parent != root && next == null; parent = parent.Parent as XElement)
{
next = parent.NextSibling<XElement>();
}
}
current = next;
}
}
public static TNode FirstChild<TNode>(this XNode node) where TNode : XNode
{
var container = node as XContainer;
if (container == null)
return null;
return container.FirstNode.NextSibling<TNode>(true);
}
public static TNode NextSibling<TNode>(this XNode node) where TNode : XNode
{
return node.NextSibling<TNode>(false);
}
public static TNode NextSibling<TNode>(this XNode node, bool includeSelf) where TNode : XNode
{
if (node == null)
return null;
for (node = (includeSelf ? node : node.NextNode); node != null; node = node.NextNode)
{
var nextTNode = node as TNode;
if (nextTNode != null)
return nextTNode;
}
return null;
}
}
然后像这样使用它:
var matches = layout
.DescendantsUntil(x => x.Name == "column")
.Where(x => (string)x.Attribute("column-name") == "abc");
扩展方法应合理执行,因为它避免了递归和复杂的嵌套linq查询.
The extension method should be reasonably performant as it avoids recursion and complex nested linq queries.
示例 .Net小提琴显示两个选项.
这篇关于如何查找具有给定名称的最高级别后代的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!