У меня есть некоторые данные, которые имеют различные атрибуты, и я хочу иерархически сгруппировать эти данные. Например:
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
, но как система типов выводит этот тип, если он не участвует ни в одном из передаваемых параметров?
Эта проблема растягивает мое понимание, и это здорово, но мой мозг болит.