XElement: поиск потомков одного уровня

Serg046

Ну т.е. найти всех потомков по имени Саша, но только внуков. Используя Descendants, во-первых результат включает лишнюю информацию, во-вторых затраты на поиск тех самых ненужных элементов. Пока что проверяю свойство Parent, чтобы одинаковое было (переделаю). В голову приходит только бегать по уровням дерева, т.е, все дети -> все внуки -> и т.д.Есть ли красивое решение?
14 ответов

Serg046

Serg046, Elements - возвращает только непосредственных потомков.


Serg046

Psilon, в курсе, но не известно на каком уровне нужно искать. Т.е. нужно найти всех Саш, но одного порядка (что первое найдено) если внуки, то только внуки, если правнуки, то только правнуки, а не внуки и правнуки и т.д.


Serg046

Serg046, ищем первого Сашу, берем его отца, вызываем для него Elements..


Serg046

Psilon, супер) А как искать? Если через Descendants, то оно ж все равно сначало все элементы вернет, а потом только я первый возьму (не выгодно). Как-то красиво можно, или только самому по одному элементу перебирать?


Serg046

Serg046, он так или иначе строит полное дерево, поэтому не думаю, что Descendants много геморроя вызовет. Сделайте как предложено, если будет неудовлетворительно по скорости, тогда можно думать Преждевременные оптимизации - зло


Serg046

В общем все равно не верно вышло
<1>
  <2>
    <3>
      <Search attr="1">
      </Search>
    </3>
    <Search attr="2">
    </Search>
  </2>
</1>
Descendants.First вернет Search с аттрибутом "1", а надо бы с "2" ибо оно выше.Накидал такой костыль
public static XElement FindElement(this XElement el, string name)
        {
            if (el.Name.ToString().Equals(name))
                return el;
            var searchList = el.Elements();
            while (searchList.Count() > 0)
            {
                var newSearchList = new List<XElement>();
                foreach (var elem in searchList)
                {
                    if (elem.Name.ToString().Equals(name))
                        return elem;
                    newSearchList.AddRange(elem.Elements());
                }
                searchList = newSearchList;
            }
            return null;
        }


Serg046

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
 
namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            string xml = @"<a>
  <b>
    <c>
      <Search attr=""1"">
      </Search>
    </c>
    <Search attr=""2"">
    </Search>
  </b>
</a>";
            using (var sr = new StringReader(xml))
            {
                var xdoc = XDocument.Load(sr);
                var searchresults = SearchResults(xdoc.Root, "Search");
                foreach (var element in searchresults)
                {
                    Console.WriteLine(element.Attribute("attr"));
                }
            }
        }
 
        private static IEnumerable<XElement> SearchResults(XElement element, string name)
        {
            bool levelFound = false;
            foreach (var elm in element.Elements())
            {
                if (elm.Name == name)
                {
                    levelFound = true;
                    yield return elm;
                }
            }
            if (levelFound) 
                yield break;
            foreach (var elm in element.Elements())
            {
                var res = SearchResults(elm, name).ToArray();
                if (res.Length == 0) 
                    continue;
                foreach (var xElement in res)
                {
                    yield return xElement;
                }
                break;
            }
        }
    }
}
Чуть длиннее, зато без костылей С костылями покороче, но помедленнее по идее.например так:
        private static XElement[] SearchResults(XElement element, string name)
        {
            var elms = element.Elements(name).ToArray();
            if (elms.Length != 0)
                return elms;
            var searchResults = element.Elements().Select(x => SearchResults(x, name));
            return searchResults.FirstOrDefault(x => x != null) ?? new XElement[0];
        }
Компромисс между скоростью и читабельностью (а возможно и самый быстрый и читабельный ):
        private static XElement[] SearchResults(XContainer container, string name)
        {
            var elms = container.Elements(name).ToArray();
            if (elms.Length != 0)
                return elms;
            foreach (var xElement in container.Elements())
            {
                var results = SearchResults(xElement, name);
                if (results.Length > 0)
                    return results;
            }
            return new XElement[0];
        }
Serg046, у вашего метода много проблем. Во-первых Count() очень жестко сажает производительность, во-вторых нужно собирать результаты в какую-то коллекцию, потому что иначе будет possible multiple enumeration (см. stackoverflow).


Serg046

Psilon, не не, ваш вариант неправильно работает, пробуйте такой исходник
string xml = @"<a>
  <b>
    <c>
      <s>
        <Search attr=""1"">
        </Search>
      </s>
    </c>
    <d>
      <Search attr=""2"">
      </Search>
    </d>
  </b>
</a>";
А еще не выгодно, ибо сначала будет смотреть ветку "" до конца (а если она огромна?), а только потом "". Ну и сейчас это не предусмотрено.
Во-первых Count() очень жестко сажает производительность
Догадывался, но неужели больше чем container.Elements(name).ToArray(); Сначала и сам к массиву приводил, но потом это оказалось избыточно.
во-вторых нужно собирать результаты в какую-то коллекцию
Это я не понял, я все это дело использую пока так
public static XElement FindElement(this XElement el, string name)
        {
            if (el.Name.ToString().Equals(name))
                return el;
            var searchList = el.Elements();
            while (searchList.Count() > 0)
            {
                var newSearchList = new List<XElement>();
                foreach (var elem in searchList)
                {
                    if (elem.Name.ToString().Equals(name))
                        return elem;
                    newSearchList.AddRange(elem.Elements());
                }
                searchList = newSearchList;
            }
            return null;
        }
 
        public static IEnumerable<XElement> FindElements(this XElement el, string name)
        {
            var temp = el.FindElement(name);
            if (temp != null)
            {
                if (temp.Parent != null)
                    return temp.Parent.Elements(name);
                else
                    return new XElement[] { temp };
            }                
            return Enumerable.Empty<XElement>();
        }
Вместо вашего варианта можно бы было просто сделать Descendants.First (по вашему совету про преждевременную оптимизацию)Кстати, свой вариант я бы тоже с радостью ускорил бы, но пока не вижу за счет чего.


Serg046

Serg046, тогда так
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
 
namespace ConsoleApplication1
{
    internal class Program
    {
        private static void Main()
        {
            string xml = @"<a>
  <b>
    <c>
      <s>
        <Search attr=""1"">
        </Search>
      </s>
    </c>
    <d>
      <Search attr=""2"">
      </Search>
    </d>
  </b>
</a>";
            using (var sr = new StringReader(xml))
            {
                var xdoc = XDocument.Load(sr);
                var searchresults = xdoc.SearchResults("Search");
                foreach (var element in searchresults)
                {
                    Console.WriteLine(element.Attribute("attr"));
                }
            }
        }
    }
 
    public static class Searcher
    {
        public static IEnumerable<XElement> SearchResults(this XContainer element, string name)
        {
            return SearchResults(element, name, 0).Value;
        }
 
        private static ************<int, XElement[]> SearchResults(XContainer element, string name, int currentLevel)
        {
            var elms = element.Elements(name).ToArray();
            if (elms.Length > 0)
                return new ************<int, XElement[]>(currentLevel, elms);
            var searchResults = element.Elements().Select(x => SearchResults(x, name, currentLevel + 1));
            if (!searchResults.Any())
                return new ************<int, XElement[]>(int.MaxValue, new XElement[0]);
            var min = searchResults.Min(x => x.Key);
            return searchResults.First(x => x.Key == min);
        }
    }
}


Serg046

А еще не выгодно, ибо сначала будет смотреть ветку "" до конца (а если она огромна?)
в вашем коде эта проблема осталась, а еще Вы ищите все элементы, а только потом берете ближний. Да и вообще 435 рекурсивных вызовов (итераций) для такого маленького исходника. Похоже больше на антиоптимизацию или я чего-то не заметил?


Serg046

Serg046, так в любом случае придется так искать, потому что вы заранее не знаете, выше расположен элемент или ниже. Еще так можно:
    public static class Searcher
    {
        private static ************<int, XElement> currentParent;
        private static int calls;
        public static IEnumerable<XElement> GetOuterTag(this XContainer element, string name)
        {
            calls = 0;
            currentParent = new ************<int, XElement>(int.MaxValue, null);
            FindParent(element, name, 0);
            return currentParent.Value.Elements(name);
        }
 
        private static void FindParent(XContainer element, string name, int currentLevel)
        {
            if (currentLevel >= currentParent.Key)
                return;
            calls++;
            var elm = element.Element(name);
            if (elm != null)
                currentParent = new ************<int, XElement>(currentLevel, elm.Parent);
            foreach (var xElement in element.Elements())
                FindParent(xElement, name, currentLevel + 1);
        }
    }
calls - число вызовов, отладочная переменная.Упростить этот алгоритм вряд ли можно, т.к. я уже сказал, вид дерева изначально неизвестен, а если мы найдем в самом начале подходящее решение, то все последующие циклы быстренько пройдут проверку if (currentLevel >= minLevelFound.Key) return;Кстати, неясно что делать, если будет на одном уровне иерархии это значение:
            string xml = @"<a>
  <b>
    <c>
      <s>
        <Search attr=""1"">
        </Search>
      </s>
    </c>
    <d>
      <e>
         <Search attr=""2"">
         </Search>
      </e>
    </d>
  </b>
</a>";


Serg046

так в любом случае придется так искать, потому что вы заранее не знаете, выше расположен элемент или ниже
Посмотрите мой код, он ходит как раз таки по левелам, а не детям.
Кстати, неясно что делать, если будет на одном уровне иерархии это значение:
В этом первый попадется он и верный
то все последующие циклы быстренько пройдут проверку
так ведь там рекурсивный вызов, а не цикл, за вызов же заплатим или будет инлайн после компиляции?И вообще, кстати, спасибо, все это носит скорее игровой характер, чем рабочий.


Serg046

Serg046, а можно те данные, на котором 500 рекурсивных вызовов?
так ведь там рекурсивный вызов, а не цикл, за вызов же заплатим или будет инлайн после компиляции?
не факт, по ситуации. Циклы конечно дешевле, но вы каждый раз коллекцию создаете, а я нет. Все неоднозначно. Дайте данные, на котором у вас получилось 500 рекурсивных вызовов, интересно сравнить.


Serg046

<a>
  <b>
    <c>
      <s>
        <Search attr=""1"">
        </Search>
      </s>
    </c>
    <d>
      <Search attr=""2"">
      </Search>
    </d>
  </b>
</a>
435