Possiedo alcuni dati con vari attributi e desidero raggruppare gerarchicamente tali dati. Per esempio:Come posso raggruppare gerarchicamente i dati utilizzando LINQ?
public class Data
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
vorrei questo raggruppati come:
A1
- B1
- C1
- C2
- C3
- ...
- B2
- ...
A2
- B1
- ...
...
Attualmente, sono stato in grado di raggruppare questo utilizzando LINQ in modo tale che il gruppo di testa divide i dati di A, allora ogni divide sottogruppi da B, allora ogni sottogruppo B contiene sottogruppi di C, ecc LINQ assomiglia a questo (assumendo una sequenza IEnumerable<Data>
chiamato 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
};
Come puoi vedere, questo diventa un po 'disordinato più è necessario il sottogruppo. C'è un modo migliore per eseguire questo tipo di raggruppamento? Sembra che ci dovrebbe essere e io non lo vedo.
Aggiornamento
Finora, ho trovato che esprime questo raggruppamento gerarchico utilizzando le API LINQ fluenti, piuttosto che linguaggio di query migliora senza dubbio la leggibilità, ma non si sente molto secca.
Ci sono stati due modi per farlo: uno con GroupBy
con un selettore dei risultati, l'altro con GroupBy
seguito da una chiamata Select
. Entrambi possono essere formattati in modo da essere più leggibili rispetto all'utilizzo del linguaggio di query, ma non si adattano ancora bene.
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)
})
})
});
Quello che mi piacerebbe ...
posso immaginare un paio di modi in cui questo potrebbe essere espressa (supponendo che la lingua e il quadro sostenuta). La prima sarebbe un'estensione GroupBy
che accetta una serie di coppie di funzioni per la selezione delle chiavi e la selezione dei risultati, Func<TElement, TKey>
e Func<TElement, TResult>
. Ogni coppia descrive il prossimo sottogruppo. Questa opzione cade perché ogni coppia potrebbe potenzialmente richiedere TKey
e TResult
per essere diversi dagli altri, il che significherebbe che GroupBy
avrebbe bisogno di parametri finiti e di una dichiarazione complessa.
La seconda opzione sarebbe un metodo di estensione SubGroupBy
che potrebbe essere concatenato per produrre sottogruppi. SubGroupBy
sarebbe lo stesso di GroupBy
ma il risultato sarebbe il precedente raggruppamento ulteriormente partizionato. Per esempio:
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 { ... })
La difficoltà con questo è come implementare i metodi più efficiente con la mia attuale comprensione, ogni livello sarebbe ricreare nuovi oggetti al fine di estendere gli oggetti precedenti. La prima iterazione creerebbe raggruppamenti di A, la seconda creerebbe oggetti che hanno una chiave di A e raggruppamenti di B, la terza ripeterà tutto ciò e aggiungerà i raggruppamenti di C. Questo sembra terribilmente inefficiente (anche se sospetto le mie attuali opzioni Effettivamente farlo comunque). Sarebbe bello se le chiamate passassero attorno a una meta-descrizione di ciò che era necessario e le istanze fossero state create solo nell'ultimo passaggio, ma anche questo sembra difficile.Si noti che il suo è simile a quello che può essere fatto con GroupBy
ma senza le chiamate di metodo nidificate.
Speriamo che tutto ciò abbia un senso. Mi aspetto che stia cacciando gli arcobaleni qui, ma forse no.
Update - un'altra opzione
Un'altra possibilità che penso sia più elegante di miei suggerimenti precedenti si affida a ciascun gruppo di appartenenza essere solo una chiave e una sequenza di elementi secondari (come negli esempi), molto simile IGrouping
fornisce adesso. Ciò significa che un'opzione per la costruzione di questo raggruppamento sarebbe una serie di selettori chiave e un singolo selettore di risultati.
Se le chiavi erano tutte limitate a un tipo di set, il che non è irragionevole, questo potrebbe essere generato come una sequenza di selettori di chiavi e un selettore di risultati, un selettore di risultati e uno params
di selettori di chiave. Naturalmente, se le chiavi dovessero essere di tipi e livelli diversi, questo diventa di nuovo difficile, tranne che per una profondità finita della gerarchia dovuta al modo in cui la parametrizzazione dei generici funziona.
Ecco alcuni esempi illustrativi di quello che voglio dire:
Ad esempio:
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*/ })
Oppure:
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)
Questo non risolve le inefficienze di implementazione, ma dovrebbe risolvere il complesso nidificazione. Tuttavia, quale sarebbe il tipo di ritorno di questo raggruppamento? Avrei bisogno della mia interfaccia o posso usare lo IGrouping
in qualche modo. Quanto devo definire o la profondità variabile della gerarchia lo rende ancora impossibile?
La mia ipotesi è che questo dovrebbe essere lo stesso del tipo di ritorno da qualsiasi chiamata IGrouping
ma come fa il tipo di sistema ad inferire quel tipo se non è coinvolto in nessuno dei parametri che vengono passati?
Questo problema sta allungando la mia comprensione, che è grande, ma il mio cervello fa male.
@Jeff: potresti postare il tipo di codice che vuoi * scrivere * (presumibilmente invocando una sorta di aiuto) e quindi possiamo vedere cosa possiamo fare? Sospetto che sia una di quelle cose che richiedono un sovraccarico diverso per ogni livello di gerarchia (ad esempio una per 2 livelli, una per 3 ecc.) Ma potrebbe comunque essere utile. –
@jon skeet: sicuro. Fornirò un aggiornamento a breve. Sento che c'è una soluzione più elegante ma non riesco a vederla. Ho fatto un tentativo di speculare la mia chiamata ieri, ma cade in fallo di regole generiche poiché ogni utilizzo di Func richiedeva diversi tipi generici. –
@Jon Skeet: ho fornito alcuni dettagli sulle opzioni che ho considerato (al di fuori delle restrizioni della lingua o del framework) e il mio pensiero generale. –