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