2010-02-09 10 views
14

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.

+0

@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. –

+0

@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. –

+0

@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. –

risposta

8

Here is a description come è possibile implementare un meccanismo di raggruppamento gerarchico.

Da questa descrizione: classe

Risultato: metodo

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); } 
} 

Estensione:

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; 
    } 
} 

Usage:

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

Edit:

Ecco una versione migliorata e correttamente digitato il codice.

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; 
     } 
    } 
} 
+0

Questo non viene creato per me in "Articoli = g" ... e "Elementi IEnumerabili" se si imposta "Elementi IEnumerable " –

+0

@Prigioniero ZERO: Gli articoli sono di TElement e non di GroupResult. Ho aggiunto una versione scritta correttamente al post. – AxelEckenberger

+1

La versione migliorata non viene compilata. (GroupResult richiede 1 argomento di tipo.) –

4

È necessaria una funzione ricorsiva. La funzione ricorsiva si chiama per ogni nodo nell'albero.

Per fare ciò in Linq, è possibile use a Y-combinator.

+0

Come funzionerebbe quando la proprietà che sto raggruppando cambia a ogni livello? –

+0

Non funziona. È meglio impostare un'associazione autoreferenziale aggiungendo un ParentID a ciascun nodo (in modo tale che ci si riferisca sempre a ParentID ad ogni livello), a meno che, naturalmente, il numero di livelli dell'albero (profondità nidificata) sia limitato dal design dell'applicazione. –

Problemi correlati