Как я могу иерархически сгруппировать данные с помощью LINQ? - PullRequest
14 голосов
/ 09 февраля 2010

У меня есть некоторые данные, которые имеют различные атрибуты, и я хочу иерархически сгруппировать эти данные. Например:

public class Data
{
   public string A { get; set; }
   public string B { get; set; }
   public string C { get; set; }
}

Я бы хотел, чтобы это было сгруппировано как:

A1
 - B1
    - C1
    - C2
    - C3
    - ...
 - B2
    - ...
A2
 - B1
    - ...
...

В настоящее время я смог сгруппировать это с помощью LINQ так, что верхняя группа делит данные на A, затем каждая подгруппа делится на B, затем каждая подгруппа B содержит подгруппы на C и т. Д. LINQ выглядит следующим образом (при условии, что IEnumerable<Data> последовательность называется data):

var hierarchicalGrouping =
            from x in data
            group x by x.A
                into byA
                let subgroupB = from x in byA
                                group x by x.B
                                    into byB
                                    let subgroupC = from x in byB
                                                    group x by x.C
                                    select new
                                    {
                                        B = byB.Key,
                                        SubgroupC = subgroupC
                                    }
                select new
                {
                    A = byA.Key,
                    SubgroupB = subgroupB
                };

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

Обновление
До сих пор я обнаружил, что выражение этой иерархической группировки с использованием плавных API-интерфейсов LINQ, а не языка запросов, возможно, улучшает читабельность, но это не выглядит ОЧЕНЬ СУХОЙ.

Было два способа сделать это: один с помощью GroupBy с селектором результатов, другой с использованием GroupBy с последующим вызовом Select. Оба могут быть отформатированы так, чтобы их было удобнее читать, чем при использовании языка запросов, но они по-прежнему плохо масштабируются.

var withResultSelector =
    data.GroupBy(a => a.A, (aKey, aData) =>
        new
        {
            A = aKey,
            SubgroupB = aData.GroupBy(b => b.B, (bKey, bData) =>
                new
                {
                    B = bKey,
                    SubgroupC = bData.GroupBy(c => c.C, (cKey, cData) =>
                    new
                    {
                        C = cKey,
                        SubgroupD = cData.GroupBy(d => d.D)
                    })
                })
        });

var withSelectCall =
    data.GroupBy(a => a.A)
        .Select(aG =>
        new
        {
            A = aG.Key,
            SubgroupB = aG
                .GroupBy(b => b.B)
                .Select(bG =>
            new
            {
                B = bG.Key,
                SubgroupC = bG
                    .GroupBy(c => c.C)
                    .Select(cG =>
                new
                {
                    C = cG.Key,
                    SubgroupD = cG.GroupBy(d => d.D)
                })
            })
        });

Что бы я хотел ...
Я могу предусмотреть несколько способов, которыми это может быть выражено (при условии, что язык и среда его поддерживают). Первым будет расширение GroupBy, которое принимает ряд пар функций для выбора ключа и выбора результата, Func<TElement, TKey> и Func<TElement, TResult>. Каждая пара описывает следующую подгруппу. Эта опция падает, потому что каждая пара может потребовать, чтобы TKey и TResult отличались от других, что означало бы, что GroupBy потребует конечных параметров и сложного объявления.

Второй вариант - это метод расширения SubGroupBy, который можно объединить в цепочку для создания подгрупп. SubGroupBy будет таким же, как GroupBy, но в результате будет разбита предыдущая группа. Например:

var groupings = data
    .GroupBy(x=>x.A)
    .SubGroupBy(y=>y.B)
    .SubGroupBy(z=>z.C)

// This version has a custom result type that would be the grouping data.
// The element data at each stage would be the custom data at this point
// as the original data would be lost when projected to the results type.
var groupingsWithCustomResultType = data
    .GroupBy(a=>a.A, x=>new { ... })
    .SubGroupBy(b=>b.B, y=>new { ... })
    .SubGroupBy(c=>c.C, c=>new { ... })

Сложность в этом заключается в том, как эффективно реализовать методы, так как при моем текущем понимании каждый уровень будет заново создавать новые объекты для расширения предыдущих объектов. Первая итерация создаст группы A, вторая создаст объекты, которые имеют ключ A и группы B, третья сделает все это заново и добавит группы C. Это кажется ужасно неэффективным (хотя я подозреваю, что мои текущие параметры на самом деле делать это в любом случае). Было бы хорошо, если бы вызовы передавали мета-описание того, что требовалось, и экземпляры создавались только на последнем проходе, но это тоже звучит сложно. Обратите внимание, что его аналогично тому, что можно сделать с GroupBy, но без вложенных вызовов методов.

Надеюсь, все это имеет смысл. Я ожидаю, что гонюсь за радугой здесь, но, возможно, нет.

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

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

Вот несколько иллюстративных примеров того, что я имею в виду:

Например:

public static /*<grouping type>*/ SubgroupBy(
    IEnumerable<Func<TElement, TKey>> keySelectors,
    this IEnumerable<TElement> sequence,
    Func<TElement, TResult> resultSelector)
{
    ...
}

var hierarchy = data.SubgroupBy(
                    new [] {
                        x => x.A,
                        y => y.B,
                        z => z.C },
                    a => new { /*custom projection here for leaf items*/ })

Или:

public static /*<grouping type>*/ SubgroupBy(
    this IEnumerable<TElement> sequence,
    Func<TElement, TResult> resultSelector,
    params Func<TElement, TKey>[] keySelectors)
{
    ...
}

var hierarchy = data.SubgroupBy(
                    a => new { /*custom projection here for leaf items*/ },
                    x => x.A,
                    y => y.B,
                    z => z.C)

Это не решает неэффективности реализации, но должно решать сложное вложение. Тем не менее, каким будет тип возврата этой группировки? Нужен ли мне собственный интерфейс или я могу использовать IGrouping как-нибудь. Сколько мне нужно определить или переменная глубина иерархии все еще делает это невозможным?

Я предполагаю, что это должно совпадать с типом возврата из любого вызова IGrouping, но как система типов выводит этот тип, если он не участвует ни в одном из передаваемых параметров?

Эта проблема растягивает мое понимание, и это здорово, но мой мозг болит.

Ответы [ 2 ]

9 голосов
/ 28 февраля 2010

Вот описание , как можно реализовать механизм иерархической группировки.

Из этого описания:

Класс результата:

public class GroupResult
{
    public object Key { get; set; }
    public int Count { get; set; }
    public IEnumerable Items { get; set; }
    public IEnumerable<GroupResult> SubGroups { get; set; }
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); }
}

Метод расширения:

public static class MyEnumerableExtensions
{
    public static IEnumerable<GroupResult> GroupByMany<TElement>(
        this IEnumerable<TElement> elements,
        params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            var selector = groupSelectors.First();

            //reduce the list recursively until zero
            var nextSelectors = groupSelectors.Skip(1).ToArray();
            return
                elements.GroupBy(selector).Select(
                    g => new GroupResult
                    {
                        Key = g.Key,
                        Count = g.Count(),
                        Items = g,
                        SubGroups = g.GroupByMany(nextSelectors)
                    });
        }
        else
            return null;
    }
}

Использование:

var result = customers.GroupByMany(c => c.Country, c => c.City);

Edit:

Вот улучшенная и правильно напечатанная версия кода.

public class GroupResult<TItem>
{
    public object Key { get; set; }
    public int Count { get; set; }
    public IEnumerable<TItem> Items { get; set; }
    public IEnumerable<GroupResult<TItem>> SubGroups { get; set; }
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); }
}

public static class MyEnumerableExtensions
{
    public static IEnumerable<GroupResult<TElement>> GroupByMany<TElement>(
        this IEnumerable<TElement> elements,
        params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            var selector = groupSelectors.First();

            //reduce the list recursively until zero
            var nextSelectors = groupSelectors.Skip(1).ToArray();
            return
                elements.GroupBy(selector).Select(
                    g => new GroupResult<TElement> {
                        Key = g.Key,
                        Count = g.Count(),
                        Items = g,
                        SubGroups = g.GroupByMany(nextSelectors)
                    });
        } else {
            return null;
        }
    }
}
4 голосов
/ 09 февраля 2010

Вам нужна рекурсивная функция. Рекурсивная функция вызывает себя для каждого узла в дереве.

Чтобы сделать это в Linq, вы можете использовать Y-комбинатор .

...