2012-02-29 14 views
5

Ho un dizionario di struct, in cui un membro è un elenco contenente vari elementi applicabili a ciascun elemento del dizionario.Query Linq da unire contro un elenco in una struct

Vorrei unire questi elementi a ciascun elemento, per filtrarli e/o raggrupparli per elemento.

In SQL ho familiarità con l'unione con tabelle/query per ottenere più righe come desiderato, ma sono nuovo di C#/Linq. Poiché una "colonna" può essere un oggetto/elenco già associato agli elementi del dizionario appropriati, mi chiedo come posso utilizzarli per eseguire un join?

Ecco un esempio della struttura:

name elements 
item1 list: elementA 
item2 list: elementA, elementB 

Vorrei una query che dà questa uscita (Numero = 3)

name elements 
item1 elementA 
item2 elementA 
item2 elementB 

Per ultima analisi, raggruppandoli in questo modo:

Ecco il mio codice inizia a contare gli elementi del dizionario.

public struct MyStruct 
    { 
     public string name; 
     public List<string> elements; 
    } 

    private void button1_Click(object sender, EventArgs e) 
    { 
     MyStruct myStruct = new MyStruct(); 
     Dictionary<String, MyStruct> dict = new Dictionary<string, MyStruct>(); 

     // Populate 2 items 
     myStruct.name = "item1"; 
     myStruct.elements = new List<string>(); 
     myStruct.elements.Add("elementA"); 
     dict.Add(myStruct.name, myStruct); 

     myStruct.name = "item2"; 
     myStruct.elements = new List<string>(); 
     myStruct.elements.Add("elementA"); 
     myStruct.elements.Add("elementB"); 
     dict.Add(myStruct.name, myStruct); 


     var q = from t in dict 
       select t; 

     MessageBox.Show(q.Count().ToString()); // Returns 2 
    } 

Edit: non ho davvero bisogno l'uscita è un dizionario. L'ho usato per archiviare i miei dati perché funziona bene e previene i duplicati (ho un item.name unico che memorizzo come chiave). Tuttavia, ai fini del filtro/raggruppamento, suppongo che potrebbe essere una lista o un array senza problemi. Posso sempre fare. ToDictionary dove key = item.Name in seguito.

+0

@sinanakyazici la domanda non specifica che l'output deve essere memorizzato in un dizionario (e, infatti, come si nota correttamente, fuori c annot be). – phoog

+0

@phoog hai ragione. Mi riferisco male. Così ho cancellato il mio commento. – sinanakyazici

+0

@sinanakyazici per qualche motivo non posso cancellare (né modificare) il mio commento sul browser del mio telefono :( – phoog

risposta

3
var q = from t in dict 
    from v in t.Value.elements 
    select new { name = t.Key, element = v }; 

Il metodo qui è Enumerable.SelectMany. Utilizzando la sintassi metodo di estensione:

var q = dict.SelectMany(t => t.Value.elements.Select(v => new { name = t.Key, element = v })); 

EDIT

noti che si potrebbe anche usare t.Value.name sopra, invece di t.Key, dal momento che questi valori sono uguali.

Quindi, cosa sta succedendo qui?

La sintassi di comprensione delle query è probabilmente la più semplice da comprendere; puoi scrivere un blocco iteratore equivalente per vedere cosa sta succedendo. Non possiamo farlo semplicemente con un tipo anonimo, però, quindi dovremo dichiarare un tipo di tornare:

class NameElement 
{ 
    public string name { get; set; } 
    public string element { get; set; } 
} 
IEnumerable<NameElement> GetResults(Dictionary<string, MyStruct> dict) 
{ 
    foreach (KeyValuePair<string, MyStruct> t in dict) 
     foreach (string v in t.Value.elements) 
      yield return new NameElement { name = t.Key, element = v }; 
} 

Come circa il metodo di sintassi di estensione (o, ciò che è veramente succedendo qui)?

(Questo è ispirato in parte dal post di Eric Lippert a https://stackoverflow.com/a/2704795/385844, ho avuto una spiegazione molto più complicato, poi ho letto che, e si avvicinò con questo :)

Diciamo che vogliamo evitare dichiarando la NameElement genere. Potremmo usare un tipo anonimo passando una funzione.Ci piacerebbe cambiare la chiamata da questo:

var q = GetResults(dict); 

a questo:

var q = GetResults(dict, (string1, string2) => new { name = string1, element = string2 }); 

L'espressione lambda (string1, string2) => new { name = string1, element = string2 } rappresenta una funzione che prende 2 stringhe - definito dalla lista degli argomenti (string1, string2) - e restituisce un istanza del tipo anonimo inizializzato con quelle stringhe - definito dall'espressione new { name = string1, element = string2 }.

L'attuazione corrispondente è questo:

IEnumerable<T> GetResults<T>(
    IEnumerable<KeyValuePair<string, MyStruct>> pairs, 
    Func<string, string, T> resultSelector) 
{ 
    foreach (KeyValuePair<string, MyStruct> pair in pairs) 
     foreach (string e in pair.Value.elements) 
      yield return resultSelector.Invoke(t.Key, v); 
} 

Tipo inferenza ci permette di richiamare questa funzione senza specificare T per nome. Questo è utile, perché (per quanto ne sappiamo come programmatori C#), il tipo che stiamo usando non ha un nome: è anonimo.

nota che la variabile t è ora pair, per evitare confusione con il parametro di tipo T, e v è ora e, per "elemento". Abbiamo anche cambiato il tipo del primo parametro in uno dei suoi tipi di base, IEnumerable<KeyValuePair<string, MyStruct>>. È più prolisso, ma rende il metodo più utile e alla fine sarà utile. Poiché il tipo non è più un tipo di dizionario, abbiamo anche modificato il nome del parametro da dict a pairs.

Potremmo generalizzare ulteriormente. Il secondo foreach ha l'effetto di proiettare una coppia chiave-valore in una sequenza di tipo T. L'intero effetto potrebbe essere incapsulato in una singola funzione; il tipo delegato sarà Func<KeyValuePair<string, MyStruct>, T>. Il primo passo è quello di refactoring il metodo quindi abbiamo una singola istruzione che converte l'elemento pair in una sequenza, utilizzando il metodo Select per richiamare il resultSelector delegato:

IEnumerable<T> GetResults<T>(
    IEnumerable<KeyValuePair<string, MyStruct>> pairs, 
    Func<string, string, T> resultSelector) 
{ 
    foreach (KeyValuePair<string, MyStruct> pair in pairs) 
     foreach (T result in pair.Value.elements.Select(e => resultSelector.Invoke(pair.Key, e)) 
      yield return result; 
} 

Ora si può facilmente cambiare la firma:

IEnumerable<T> GetResults<T>(
    IEnumerable<KeyValuePair<string, MyStruct>> pairs, 
    Func<KeyValuePair<string, MyStruct>, IEnumerable<T>> resultSelector) 
{ 
    foreach (KeyValuePair<string, MyStruct> pair in pairs) 
     foreach (T result in resultSelector.Invoke(pair)) 
      yield return result; 
} 

Il sito di chiamata ora si presenta così; notare come l'espressione lambda ora incorpora la logica che abbiamo rimosso dal corpo del metodo, quando abbiamo cambiato la sua firma:

var q = GetResults(dict, pair => pair.Value.elements.Select(e => new { name = pair.Key, element = e })); 

Per rendere il metodo più utile (e la sua attuazione meno prolissa), cerchiamo di sostituire il tipo KeyValuePair<string, MyStruct> con un digitare parametro, TSource. Cambieremo alcuni altri nomi al tempo stesso:

T  -> TResult 
pairs -> sourceSequence 
pair -> sourceElement 

E, solo per calci, ce la faremo un metodo di estensione:

static IEnumerable<TResult> GetResults<TSource, TResult>(
    this IEnumerable<TSource> sourceSequence, 
    Func<TSource, IEnumerable<TResult>> resultSelector) 
{ 
    foreach (TSource sourceElement in sourceSequence) 
     foreach (T result in resultSelector.Invoke(pair)) 
      yield return result; 
} 

E il gioco è fatto: SelectMany! Bene, la funzione ha ancora il nome sbagliato e l'implementazione effettiva include la convalida che la sequenza sorgente e la funzione selector non siano nulle, ma questa è la logica principale.

Da MSDN: SelectMany "proietta ogni elemento di una sequenza su un oggetto IEnumerable e appiattisce le sequenze risultanti in una sequenza."

+0

Con la tua prima risposta, ottengo Un'espressione di tipo 'MyStruct' non è consentita in una clausola successiva da un'espressione di query con tipo di origine "Dictionary ". Tipo di inferenza non riuscita nella chiamata a "SelectMany". – mtone

+0

E il secondo, "MyStruct" non contiene una definizione per "Seleziona" e nessun metodo di estensione "Seleziona" che accetta un primo argomento di tipo 'MyStruct' potrebbe essere trovato – mtone

+0

@mtone ho dimenticato di chiamare .elementi! modifica ... mi spiace, – phoog

0

Cosa succede se si utilizza un altro dizionario per questo.

Dictionary<String, string> dict2 = new Dictionary<string, string>(); 

dict.foreach(item => item.elements.foreach(elem => dict2.Add(elem,item.name))); 

allora è possibile interrogare il nuovo dizionario per ottenere il conteggio, ha come elemento chiave quindi per ogni elemento ha le voci che aveva. Così puoi trovare quanti elementi avevano l'elemento che desideri

+0

Questo non funzionerà perché le chiavi nel secondo dizionario non sarebbero uniche. – phoog

1

Questo appiattisce gli array in un singolo array, quindi conteggia valori univoci.

var groups = dictionary 
    .SelectMany(o => o.Value) 
    .GroupBy(o => o); 

foreach (var g in groups) 
    Console.WriteLine(g.Key + ": " + g.Count()); 

Utilizzando il seguente dizionario:

Dictionary<string, string[]> dictionary = new Dictionary<string, string[]>(); 
dictionary.Add("One", new string[] { "A" }); 
dictionary.Add("Two", new string[] {"A", "B" }); 
dictionary.Add("Three", new string[] { "A", "B" }); 

ottengo questo output:

A: 3 
B: 2 
+0

Non è possibile creare il secondo dizionario perché le sue chiavi non sarebbero univoche. – phoog

+0

Grazie per avermelo ricordato, ho aggiornato la mia risposta per risolvere l'obiettivo a lungo termine. – Despertar

+0

Grazie, questo fornisce effettivamente il conteggio corretto. Per ora, penso che preferisco farlo in 2 passaggi (espandere, quindi raggruppare), ma certamente terrò a mente questo. Grazie ancora! – mtone

1
/* Will return 
name elements 
item1 elementA 
item2 elementA 
item2 elementB 
*/ 
var res = dict 
    .Values 
    .SelectMany(m => m.elements.Select(e => new {m.name, element= e})) 
    .ToArray(); 

/* Will return 
element count 
ElementA 2 
ElementB 1 
*/ 
var res2 = res 
    .GroupBy(r => r.element) 
    .Select(g => new {element = g.Key, count = g.Count()}) 
    .ToArray(); 
+0

Grazie mille! Questo funziona ed è molto leggibile. Sicuramente lavorerò ulteriormente. – mtone

0

si consiglia di partire da una collezione semplice di struct, ma dal vostro dizionario:

var q = from t in dict.Values 
      from el in t.Elements 
      group el by el into eNameGroup 
      select new { Name = eNameGroup.Key, Count = eNameGroup.Count() }; 

Ciò restituisce:

Conte Nome
Elementa 2
ElementB 1

0

Se quello che cercate è il raggruppamento/rotazione, questo potrebbe essere fatto di più in modo dichiarativo sfruttando raggruppamento di LINQ ed evitando dizionari tutto :

void Main() 
{ 
    var items = new MyStruct[] { 
     new MyStruct { name = "item1", elements = new List<string> { "elementA" }}, 
     new MyStruct { name = "item2", elements = new List<string> { "elementA", "elementB" }}}; 

    var groupedByElement = 
     from item in items 
     from element in item.elements 
     group item by element; 

    groupedByElement.Dump(); // items grouped by element value, (pivoted) 

    var elementsWithCount = 
     from gj in groupedByElement 
     select new { element = gj.Key, count = gj.Count() }; 

    elementsWithCount.Dump(); 
    // element, count 
    // elementA, 2 
    // elementB, 1 
} 

public struct MyStruct 
{ 
    public string name; 
    public List<string> elements; 
} 
+0

BTW, questa risposta è stata scritta in LINQPad.Le chiamate Dump sono il modo in LINQPad di visualizzare l'output. – devgeezer

Problemi correlati