2012-02-29 13 views
5

Tengo un diccionario de struct, donde un miembro es una lista que contiene diversos elementos aplicables a cada elemento del diccionario.Consulta de Linq para unirse a la lista en una estructura

Me gustaría unir estos elementos a cada elemento, para filtrarlos o agruparlos por elemento.

En SQL estoy familiarizado con unirme a tablas/consultas para obtener varias filas como desee, pero soy nuevo en C#/Linq. Como una "columna" puede ser un objeto/lista ya asociada con los elementos del diccionario, me pregunto cómo puedo usarlos para realizar una unión.

He aquí una muestra de la estructura:

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

me gustaría una consulta que da esta salida (count = 3)

name elements 
item1 elementA 
item2 elementA 
item2 elementB 

Por último término, les agrupar así:

Aquí está mi código para contar elementos del diccionario.

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 
    } 

Editar: Realmente no necesito la salida es un diccionario. Lo utilicé para almacenar mis datos porque funciona bien y evita los duplicados (tengo el único item.name que almaceno como clave). Sin embargo, con el fin de filtrar/agrupar, supongo que podría ser una lista o matriz sin problemas. Siempre puedo hacer .Diccionario donde key = item.Name después.

+0

@sinanakyazici la pregunta no especifica que la salida debe almacenarse en un diccionario (y, de hecho, como se observa correctamente, salir c no sea). – phoog

+0

@phoog tienes razón. Yo entiendo mal. Entonces borré mi comentario. – sinanakyazici

+0

@sinanakyazici por alguna razón no puedo eliminar (ni editar) mi comentario en el navegador de mi teléfono :( – phoog

Respuesta

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

El método aquí es Enumerable.SelectMany. Utilizando la sintaxis de método de extensión:

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

EDITAR

Tenga en cuenta que también se puede utilizar t.Value.name anterior, en lugar de t.Key, ya que estos valores son iguales.

Entonces, ¿qué está pasando aquí?

La sintaxis de consulta-comprensión es probablemente la más fácil de entender; puede escribir un bloque de iterador equivalente para ver qué está sucediendo. No podemos hacer eso, simplemente con un tipo anónimo, sin embargo, lo que vamos a declarar un tipo para volver:

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

¿Qué hay de la sintaxis método de extensión (o, lo que es realmente pasando aquí)?

(Esto se inspiró en parte por el post de Eric Lippert en https://stackoverflow.com/a/2704795/385844, tenía una explicación mucho más complicado, entonces leí eso, y se acercó con esto :)

Digamos que queremos evitar que declara la NameElement tipo. Podríamos usar un tipo anónimo al pasar una función.Nos gustaría cambiar la llamada de este:

var q = GetResults(dict); 

a esto:

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

La expresión lambda (string1, string2) => new { name = string1, element = string2 } representa una función que toma 2 cuerdas - definido por la lista de argumentos (string1, string2) - y devuelve una instancia del tipo anónimo inicializado con esas cadenas, definido por la expresión new { name = string1, element = string2 }.

La aplicación correspondiente es la siguiente:

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

La inferencia de tipos nos permite llamar a esta función sin especificar T por su nombre. Eso es útil, porque (hasta donde sabemos, como programadores de C#), el tipo que estamos utilizando no tiene nombre: es anónimo.

Nota que la variable t es ahora pair, para evitar confusión con el parámetro de tipo T, y v es ahora e, por "elemento". También hemos cambiado el tipo del primer parámetro a uno de sus tipos base, IEnumerable<KeyValuePair<string, MyStruct>>. Es más prolijo, pero hace que el método sea más útil, y será útil al final. Como el tipo ya no es un tipo de diccionario, también hemos cambiado el nombre del parámetro de dict a pairs.

Podríamos generalizar esto más. El segundo foreach tiene el efecto de proyectar un par de clave-valor a una secuencia de tipo T. Ese efecto completo podría ser encapsulado en una sola función; el tipo de delegado sería Func<KeyValuePair<string, MyStruct>, T>. El primer paso es refactorizar el método por lo que tenemos una sola declaración que convierte el elemento pair en una secuencia, utilizando el método de Select para invocar el resultSelector delegado:

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

Ahora podemos cambiar fácilmente 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; 
} 

El sitio de la llamada ahora se ve así; observe cómo la expresión lambda ahora incorpora la lógica que hemos eliminado del cuerpo del método cuando cambiamos su firma:

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

Para que el método más útil (y su aplicación menos detallado), vamos a sustituir el tipo KeyValuePair<string, MyStruct> con una tipo parámetro, TSource. Vamos a cambiar algunos otros nombres al mismo tiempo:

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

Y, sólo por diversión, vamos a hacer que sea un método de extensión:

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

Y ahí lo tienen: SelectMany! Bueno, la función todavía tiene un nombre incorrecto, y la implementación real incluye la validación de que la secuencia fuente y la función del selector no son nulas, pero esa es la lógica central.

De MSDN: SelectMany "proyecta cada elemento de una secuencia a un IEnumerable y aplana las secuencias resultantes en una secuencia."

+0

Con su primera respuesta, obtengo Una expresión de tipo 'MyStruct' no está permitida en una cláusula subsiguiente en una expresión de consulta con el tipo de fuente 'Dictionary '. La inferencia de tipo falló en la llamada a 'SelectMany'. – mtone

+0

Y la segunda, obtengo 'MyStruct' no contiene una definición para 'Seleccionar' y ningún método de extensión 'Select' aceptando un primer argumento de tipo 'MyStruct' podría ser encontrado – mtone

+0

@mtone Olvidé llamar a .elements! editing ... lo siento. – phoog

0

¿Qué sucede si utiliza otro diccionario para eso.

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

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

a continuación, puede consultar el nuevo diccionario para obtener el recuento, que tiene como elemento clave, por tanto, la de cada elemento que tiene los elementos que lo tenían. Por lo tanto, puede encontrar cuántos elementos tenían el elemento que desea

+0

Eso no funcionará porque las claves en el segundo diccionario no serían únicas. – phoog

1

Esto aplana las matrices en una sola matriz y luego cuenta valores únicos.

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

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

utilizando el siguiente diccionario:

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

consigo esta salida:

A: 3 
B: 2 
+0

No puede crear el segundo diccionario porque sus claves no serían únicas. – phoog

+0

Gracias por señalarlo, he actualizado mi respuesta para resolver el objetivo a largo plazo. – Despertar

+0

Gracias, esto de hecho proporciona el recuento agrupado adecuado. Por ahora, creo que prefiero hacerlo en 2 pasos (ampliar, luego agrupar), pero ciertamente lo tendré en cuenta. ¡Gracias de nuevo! – 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

¡Muchas gracias! Esto funciona, y es muy legible. Ciertamente, trabajaré más con eso. – mtone

0

es posible que desee comenzar a partir de una colección simple de estructuras, sino de su diccionario:

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

Esto devuelve:

Nombre Count
elementa 2
elementb 1

0

Si lo que buscas es agrupar/pivotante, esto se podría hacer más declarativa mediante el aprovechamiento de la agrupación de LINQ y evitar los diccionarios completo :

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

Por cierto, esta respuesta fue escrita en LINQPad.Las llamadas de volcado son la forma en que LINQPad muestra la salida. – devgeezer

Cuestiones relacionadas