Сортировка иерархии с полями пути и глубины с помощью Linq

Я реализовал следующий интерфейс иерархии

interface ITree<T>
{
    // Incremential unique key
    int Id { get; set; }
    string Name { get; set; }
    // Hierarchy classical pattern
    T Parent { get; set; }
    ICollection<T> Children { get; set; }
    // Computed values
    int Depth { get; }
    // Hierarchy path with dotted notation (e.g.: 12.45.554.23,
    // where each path segment is an Id)
    string Path { get; set; }
}

class TreeItem : ITree<TreeItem>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public TreeItem Parent { get; set; }
    public ICollection<TreeItem> Children { get; set; }
    public string Path { get; set; }    
    public int Depth { get { return Path.Split('.').Length - 1; } }
}

Эти элементы хранятся и извлекаются через Entity Framework, поэтому мы можем предположить, что все поля отношения не являются нулевыми и непротиворечивыми:

  • Path и Depth всегда актуальны
  • Цепочки работают (например: item.Parent?.Parent?.Parent)
  • Обход поля Children также работает рекурсивно.
  • Использование Path и Depth является предпочтительным подходом, поскольку при этом не требуется вычислять поля отношения.

У меня есть следующая иерархия:

- A (Depth = 0)
-- B (Depth = 1)
-- C (Depth = 1)
- D  (Depth = 0)
-- E (Depth = 1)

Все мои элементы находятся в неупорядоченном плоском массиве, скажем, [D,C,B,E,A]. Я хочу использовать выражение Linq, чтобы отсортировать их следующим образом:

  • Первый уровень 0, согласно полю имени
  • Все дочерние элементы уровня 1 предыдущего, отсортированные по имени
  • Второй уровень 0 (еще по полю имени)
  • Все дети 1 уровня предыдущего...

Пример дан для двух уровней глубины, но я хотел бы, чтобы выражение проходило через иерархию независимо от ее глубины.

Обратите внимание, что для этого можно использовать поля уровня и пути моей структуры данных, так как все пути дерева перестраиваются всякий раз, когда элемент добавляется, перемещается или удаляется, а поле Глубина вычисляется с помощью простой функции Split('. ') на пути.

Проба :

var A = new TreeItem { Id = 1, Name = "A", Path = "1" };
var B = new TreeItem { Id = 2, Name = "B", Path = "1.2", Parent = A };
var C = new TreeItem { Id = 3, Name = "C", Path = "1.3", Parent = A };
var D = new TreeItem { Id = 4, Name = "D", Path = "4" };
var E = new TreeItem { Id = 5, Name = "E", Path = "4.5", Parent = D };

// populate children for the example.
// My actual code is automatic thanks to EF Inverse Relationship.
A.Children = new List<TreeItem> { B, C };
D.Children = new List<TreeItem> { E };

var listToSortHierarchically = new List<TreeItem> { D, C, B, E, A };
// I want the result of the hierarchical sort to be A B C D E

person kall2sollies    schedule 10.03.2016    source источник
comment
Что такое T? Не могли бы вы предоставить пример реализации. И разве это не должно быть, по крайней мере, ограничено where T : ITree<T>   -  person Ivan Stoev    schedule 10.03.2016
comment
Я добавил пример реализации. По сути, T — это объект POCO, и интерфейс Tree обеспечивает его фасады с полями Id, Name, Parent, Children, Depth и Path.   -  person kall2sollies    schedule 10.03.2016
comment
Спасибо. Это не компиляция, но тем не менее. Как насчет ограничения? Я имею в виду, если у меня есть ITree<TreeItem> node, могу ли я использовать node.Parent.Parent и т. д.? Без ограничения, мне нужно бросить?   -  person Ivan Stoev    schedule 10.03.2016
comment
Хорошо, быстро отредактируйте, чтобы получить модель, которая компилируется, и ответьте на ваши вопросы, но я предоставлю пример кода для настройки иерархии.   -  person kall2sollies    schedule 10.03.2016


Ответы (1)


Хорошо, сначала вы действительно должны добавить следующее ограничение

interface ITree<T>
    where T : class, ITree<T>
{
    // ...
}

поэтому мы можем безопасно перемещаться по иерархии, используя свойства Parent и Children без приведения.

Во-вторых, вместо того, чтобы заново изобретать колесо, я буду повторно использовать общее дерево для вспомогательного метода обхода порядка из моего ответа на Как сгладить дерево с помощью LINQ? (и пара других):

public static partial class TreeUtils
{
    public static IEnumerable<T> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
    {
        var stack = new Stack<IEnumerator<T>>();
        var e = source.GetEnumerator();
        try
        {
            while (true)
            {
                while (e.MoveNext())
                {
                    var item = e.Current;
                    yield return item;
                    var elements = elementSelector(item);
                    if (elements == null) continue;
                    stack.Push(e);
                    e = elements.GetEnumerator();
                }
                if (stack.Count == 0) break;
                e.Dispose();
                e = stack.Pop();
            }
        }
        finally
        {
            e.Dispose();
            while (stack.Count != 0) stack.Pop().Dispose();
        }
    }
}

С этим помощником в кармане рассматриваемый метод прост:

partial class TreeUtils
{
    public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
        where T : class, ITree<T>
    {
        if (order == null) order = items => items.OrderBy(item => item.Name);
        return order(source.Where(item => item.Parent == null))
            .Expand(item => item.Children != null && item.Children.Any() ? order(item.Children) : null);
    }
}

Пример использования:

List<TreeItem> flatList = ...;
var orderedList = flatList.Ordered().ToList();

ОБНОВЛЕНИЕ: то же самое, только с использованием свойств Path и Id:

public static partial class TreeUtils
{
    public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
        where T : class, ITree<T>
    {
        if (order == null) order = items => items != null && items.Any() ?  items.OrderBy(item => item.Name) : null;
        var chldrenByParentId = source
            .Select(item => new { item, path = item.Path.Split('.') })
            .ToLookup(e => e.path.Length >= 2 ? int.Parse(e.path[e.path.Length - 2]) : (int?)null, e => e.item);
        return (order(chldrenByParentId[null]) ?? Enumerable.Empty<T>())
            .Expand(item => order(chldrenByParentId[item.Id]));
    }
}
person Ivan Stoev    schedule 10.03.2016
comment
Спасибо за ваш точный ответ... Но на самом деле я ожидал ответа, касающегося только поля Пути (и Глубины, которая исходит из Пути). Ваш ответ, конечно, блестящий и общий при обходе и выравнивании или восстановлении деревьев, учитывая тот факт, что у нас фактически заполнены поля Родитель и Дети. Но я хотел бы избегать их использования, если это возможно, потому что из-за проблем с производительностью я могу в конечном итоге решить не заполнять их в моем плоском списке для сортировки. Надеюсь, я достаточно ясно! - person kall2sollies; 10.03.2016
comment
Как хочешь. Это выполнимо, но разделение ИМО и анализ пути и линейный поиск в плоском списке будут довольно неэффективными. - person Ivan Stoev; 10.03.2016
comment
Хорошо, поэтому я изучу, как настроить ваш подход в моем случае. Должен признаться, что у меня нет теоретических знаний об алгоритмах обхода дерева и поиска, и я должен улучшить это! - person kall2sollies; 10.03.2016
comment
В любом случае, для вашего удобства предоставлен альтернативный метод, не использующий Parent/Children. Наслаждаться :) - person Ivan Stoev; 10.03.2016