Como agrupar hierarquicamente dados usando o LINQ?
Eu tenho alguns dados que possuem vários atributos e quero agrupar hierarquicamente esses dados. Por exemplo:
public class Data
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
Eu gostaria que isso agrupasse como:
A1
- B1
- C1
- C2
- C3
- ...
- B2
- ...
A2
- B1
- ...
...
Atualmente, eu pude agrupar isso usando o LINQ, de modo que o grupo superior divida os dados por A, então cada subgrupo divide por B, então cada subgrupo B contém subgrupos por C, etc. O LINQ se parece com isso (assumindo umaIEnumerable<Data>
sequência chamadadata
):
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
};
Como você pode ver, isso fica um pouco confuso quanto mais subgrupos forem necessários. Existe uma maneira melhor de executar esse tipo de agrupamento? Parece que deveria haver e eu simplesmente não estou vendo.
Atualizar
Até agora, descobri que expressar esse agrupamento hierárquico usando as APIs LINQ fluentes, em vez da linguagem de consulta, sem dúvida melhora a legibilidade, mas não parece muito SECO.
Havia duas maneiras de fazer isso: uma usandoGroupBy
com um seletor de resultados, o outro usandoGroupBy
seguido por umSelect
ligar. Ambos podem ser formatados para serem mais legíveis do que usar a linguagem de consulta, mas ainda não são dimensionados bem.
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)
})
})
});
O que eu gostaria ...
Eu posso imaginar algumas maneiras pelas quais isso pode ser expresso (assumindo que a linguagem e a estrutura o suportem). O primeiro seria umGroupBy
extensão que utiliza uma série de pares de funções para seleção de teclas e seleção de resultados,Func<TElement, TKey>
eFunc<TElement, TResult>
. Cada par descreve o próximo subgrupo. Essa opção cai porque cada par exigiria potencialmenteTKey
eTResult
ser diferente dos outros, o que significariaGroupBy
precisaria de parâmetros finitos e uma declaração complexa.
A segunda opção seria umaSubGroupBy
método de extensão que poderia ser encadeado para produzir subgrupos.SubGroupBy
seria o mesmo queGroupBy
mas o resultado seria o agrupamento anterior ainda particionado. Por exemplo:
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 dificuldade disso é como implementar os métodos com eficiência, como no meu entendimento atual, cada nível recriaria novos objetos para estender os objetos anteriores. A primeira iteração criaria agrupamentos de A, a segunda criaria objetos que possuem uma chave de A e agrupamentos de B, a terceira refizeria tudo isso e acrescentaria os agrupamentos de C. Isso parece terrivelmente ineficiente (embora eu suspeite de minhas opções atuais faça isso mesmo). Seria bom se as chamadas passassem por uma meta-descrição do que era necessário e as instâncias fossem criadas apenas na última passagem, mas isso também parece difícil. Observe que o dele é semelhante ao que pode ser feito comGroupBy
mas sem as chamadas de método aninhado.
Espero que tudo isso faça sentido. Espero estar perseguindo arco-íris aqui, mas talvez não.
Atualização - outra opção
Outra possibilidade que eu acho mais elegante do que minhas sugestões anteriores depende de cada grupo de pais, sendo apenas uma chave e uma sequência de itens filhos (como nos exemplos), bem comoIGrouping
fornece agora. Isso significa que uma opção para construir esse agrupamento seria uma série de seletores de chave e um único seletor de resultados.
Se todas as chaves estiverem limitadas a um tipo de conjunto, o que não é razoável, isso poderá ser gerado como uma sequência de seletores de chave e um seletor de resultados, ou um seletor de resultados e umparams
seletores de teclas. Obviamente, se as chaves precisassem ser de tipos e níveis diferentes, isso se tornaria difícil novamente, exceto por uma profundidade hierárquica finita devido à maneira como a parametrização genérica funciona.
Aqui estão alguns exemplos ilustrativos do que quero dizer:
Por exemplo:
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*/ })
Ou:
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)
Isso não resolve as ineficiências de implementação, mas deve resolver o aninhamento complexo. No entanto, qual seria o tipo de retorno desse agrupamento? Eu precisaria de minha própria interface ou posso usarIGrouping
de alguma forma. Quanto eu preciso definir ou a profundidade variável da hierarquia ainda torna isso impossível?
Meu palpite é que esse deve ser o mesmo que o tipo de retorno de qualquerIGrouping
chamar, mas como o sistema de tipos infere esse tipo se não estiver envolvido em nenhum dos parâmetros que são passados?
Esse problema está ampliando minha compreensão, o que é ótimo, mas meu cérebro dói.