2009-12-17 100 views
14

Me di cuenta de que no daba suficiente información para que la mayoría de las personas leyeran mi mente y entendieran todas mis necesidades, así que cambié esto un poco del original.Manera elegante de crear un diccionario anidado en C#

Di Tengo una lista de elementos de una clase como esta:

public class Thing 
{ 
    int Foo; 
    int Bar; 
    string Baz; 
} 

y quiero categorizar la cadena Baz basado en los valores de Foo, luego de barras. Habrá como máximo una cosa para cada posible combinación de valores de Foo y Bar, pero no estoy seguro de que tenga un valor para cada uno. Puede ayudar a conceptualizarlo como información de celda para una tabla: Foo es el número de fila, Bar es el número de columna y Baz es el valor que se encuentra allí, pero no necesariamente habrá un valor presente para cada celda.

IEnumerable<Thing> things = GetThings(); 
List<int> foos = GetAllFoos(); 
List<int> bars = GetAllBars(); 
Dictionary<int, Dictionary<int, string>> dict = // what do I put here? 
foreach(int foo in foos) 
{ 
    // I may have code here to do something for each foo... 
    foreach(int bar in bars) 
    { 
     // I may have code here to do something for each bar... 
     if (dict.ContainsKey(foo) && dict[foo].ContainsKey(bar)) 
     { 
      // I want to have O(1) lookups 
      string baz = dict[foo][bar]; 
      // I may have code here to do something with the baz. 
     } 
    } 
} 

¿Cuál es una manera fácil y elegante de generar el diccionario anidado? He estado usando C# el tiempo suficiente para acostumbrarme a encontrar soluciones sencillas de una sola línea para todas las cosas comunes como esta, pero esta me tiene perplejo.

+0

Cuál es el punto de partida? Una lista de objetos Thing? –

+0

Gracias por las respuestas hasta el momento. He actualizado la pregunta para que quede más clara. Parece que algunos de ustedes ya tuvieron la idea correcta. Una vez que pruebe tus respuestas, comenzaré a votar y asignaré un ganador. – StriplingWarrior

Respuesta

25

Aquí es una solución utilizando LINQ:

Dictionary<int, Dictionary<int, string>> dict = things 
    .GroupBy(thing => thing.Foo) 
    .ToDictionary(fooGroup => fooGroup.Key, 
        fooGroup => fooGroup.ToDictionary(thing => thing.Bar, 
                thing => thing.Baz)); 
+3

Use "var dict =" Y puede usar LINQ para contraer sus declaraciones múltiples foreach: var bazs = dict.SelectMany (topPair => topPair.Value.Values); foreach (string baz in bazs) { // ... } –

+0

Parece ser la solución corta y elegante que estaba buscando. El combo GroupBy/ToDictionary era lo que me costaba crear por mi cuenta. Gracias. – StriplingWarrior

-2
Dictionary<int, Dictionary<string, int>> nestedDictionary = 
      new Dictionary<int, Dictionary<string, int>>(); 
+0

No entendiste del todo la esencia de mi pregunta. Estoy buscando una declaración linq o algo que me permita completar el diccionario de una lista existente, no simplemente instanciarlo. – StriplingWarrior

4

definir su propio encargo genérico NestedDictionary clase

public class NestedDictionary<K1, K2, V>: 
    Dictionary<K1, Dictionary<K2, V>> {} 

entonces en su código que escriba

NestedDictionary<int, int, string> dict = 
     new NestedDictionary<int, int, string>(); 

si se utiliza el int, int, string uno mucho, definen una costumbre clase para eso también ..

public class NestedIntStringDictionary: 
     NestedDictionary<int, int, string> {} 

y luego escribir:

NestedIntStringDictionary dict = 
      new NestedIntStringDictionary(); 

EDIT: Para agregar la capacidad de construir instancia específica del proporcionado Lista de artículos:

public class NestedIntStringDictionary: 
     NestedDictionary<int, int, string> 
    { 
     public NestedIntStringDictionary(IEnumerable<> items) 
     { 
      foreach(Thing t in items) 
      { 
       Dictionary<int, string> innrDict = 
         ContainsKey(t.Foo)? this[t.Foo]: 
          new Dictionary<int, string>(); 
       if (innrDict.ContainsKey(t.Bar)) 
        throw new ArgumentException(
         string.Format(
          "key value: {0} is already in dictionary", t.Bar)); 
       else innrDict.Add(t.Bar, t.Baz); 
      } 
     } 
    } 

y luego escribir:

NestedIntStringDictionary dict = 
     new NestedIntStringDictionary(GetThings()); 
+0

¿Cómo se vería el accesorio? –

+0

@Scott W .: 'Tuple '. – jason

+0

¿Cómo me ayuda esto a construir elegantemente un diccionario anidado a partir de los datos que me han dado? – StriplingWarrior

2

Usted puede ser capaz de utilice un KeyedCollection donde defina:

class ThingCollection 
    : KeyedCollection<Dictionary<int,int>,Employee> 
{ 
    ... 
} 
+0

No me resulta inmediatamente obvio cómo esto solucionaría mi problema. Por favor elabora. – StriplingWarrior

16

Una forma elegante sería no crear los diccionarios mismo pero el uso de LINQ GroupBy y ToDictionary para generarlo para usted.

var things = new[] { 
    new Thing { Foo = 1, Bar = 2, Baz = "ONETWO!" }, 
    new Thing { Foo = 1, Bar = 3, Baz = "ONETHREE!" }, 
    new Thing { Foo = 1, Bar = 2, Baz = "ONETWO!" } 
}.ToList(); 

var bazGroups = things 
    .GroupBy(t => t.Foo) 
    .ToDictionary(gFoo => gFoo.Key, gFoo => gFoo 
     .GroupBy(t => t.Bar) 
     .ToDictionary(gBar => gBar.Key, gBar => gBar.First().Baz)); 

Debug.Fail("Inspect the bazGroups variable."); 

que suponer que mediante la categorización Baz usando Foo y Bar quiere decir que si dos cosas tienen tanto Foo y Bar es igual a su valor luego Baz también ser el mismo también. Por favor corrígeme si estoy equivocado.

Básicamente está agrupado por la propiedad Foo primero ...
luego para cada grupo resultante, se agrupa en la propiedad ...
y luego para cada grupo resultante toma el primer valor Baz como valor del diccionario.

Si se dio cuenta, los nombres de los métodos coincidían exactamente con lo que está tratando de hacer. :-)


EDIT: Aquí hay otra manera usando comprensiones de consulta, que son más largos, pero son tranquilas fácil de leer y grok:

var bazGroups = 
    (from t1 in things 
    group t1 by t1.Foo into gFoo 
    select new 
    { 
     Key = gFoo.Key, 
     Value = (from t2 in gFoo 
        group t2 by t2.Bar into gBar 
        select gBar) 
        .ToDictionary(g => g.Key, g => g.First().Baz) 
    }) 
    .ToDictionary(g => g.Key, g => g.Value); 

Por desgracia, no hay contraparte comprensión de consultas para ToDictionary así que no es tan elegante como las expresiones lambda.

...

Espero que esto ayude.

+1

Excelente respuesta. – Tinister

+3

* suspiro * No puedo esperar para salir de .NET 2.0, muriendo por utilizar LINQ. –

+1

Mis simpatías ... = \ –

3

Otro enfoque sería convertir su diccionario en clave de tipo anónimo basado en los valores Foo y Bar.

var things = new List<Thing> 
       { 
        new Thing {Foo = 3, Bar = 4, Baz = "quick"}, 
        new Thing {Foo = 3, Bar = 8, Baz = "brown"}, 
        new Thing {Foo = 6, Bar = 4, Baz = "fox"}, 
        new Thing {Foo = 6, Bar = 8, Baz = "jumps"} 
       }; 
var dict = things.ToDictionary(thing => new {thing.Foo, thing.Bar}, 
           thing => thing.Baz); 
var baz = dict[new {Foo = 3, Bar = 4}]; 

Esto alisa de manera efectiva su jerarquía en un solo diccionario. Tenga en cuenta que este diccionario no se puede exponer externamente ya que se basa en un tipo anónimo.

Si la combinación de valores Foo y Bar no es única en su colección original, entonces deberá agruparlos primero.

var dict = things 
    .GroupBy(thing => new {thing.Foo, thing.Bar}) 
    .ToDictionary(group => group.Key, 
        group => group.Select(thing => thing.Baz)); 
var bazes = dict[new {Foo = 3, Bar = 4}]; 
foreach (var baz in bazes) 
{ 
    //... 
} 
+0

Le agradezco que se haya tomado su tiempo para obtener una respuesta tan completa, y puedo entender por qué se le ocurrió esta solución en función de cómo redacté originalmente la pregunta, pero en realidad no hace lo que necesito. – StriplingWarrior

+0

hace lo que necesito hacer :) –

1

Creo que el enfoque más simple sería utilizar los métodos de extensión LINQ. Obviamente no he probado este código para el rendimiento.

var items = new[] { 
    new Thing { Foo = 1, Bar = 3, Baz = "a" }, 
    new Thing { Foo = 1, Bar = 3, Baz = "b" }, 
    new Thing { Foo = 1, Bar = 4, Baz = "c" }, 
    new Thing { Foo = 2, Bar = 4, Baz = "d" }, 
    new Thing { Foo = 2, Bar = 5, Baz = "e" }, 
    new Thing { Foo = 2, Bar = 5, Baz = "f" } 
}; 

var q = items 
    .ToLookup(i => i.Foo) // first key 
    .ToDictionary(
    i => i.Key, 
    i => i.ToLookup(
     j => j.Bar,  // second key 
     j => j.Baz));  // value 

foreach (var foo in q) { 
    Console.WriteLine("{0}: ", foo.Key); 
    foreach (var bar in foo.Value) { 
    Console.WriteLine(" {0}: ", bar.Key); 
    foreach (var baz in bar) { 
     Console.WriteLine(" {0}", baz.ToUpper()); 
    } 
    } 
} 

Console.ReadLine(); 

de salida: dos clases Mapa clave de

1: 
    3: 
    A 
    B 
    4: 
    C 
2: 
    4: 
    D 
    5: 
    E 
    F 
2

Uso BeanMap. También hay un mapa de 3 teclas, y es bastante extensible en caso de que necesite n teclas.

http://beanmap.codeplex.com/

Su solución sería el siguiente aspecto:

class Thing 
{ 
    public int Foo { get; set; } 
    public int Bar { get; set; } 
    public string Baz { get; set; } 
} 

[TestMethod] 
public void ListToMapTest() 
{ 
    var things = new List<Thing> 
      { 
       new Thing {Foo = 3, Bar = 3, Baz = "quick"}, 
       new Thing {Foo = 3, Bar = 4, Baz = "brown"}, 
       new Thing {Foo = 6, Bar = 3, Baz = "fox"}, 
       new Thing {Foo = 6, Bar = 4, Baz = "jumps"} 
      }; 

    var thingMap = Map<int, int, string>.From(things, t => t.Foo, t => t.Bar, t => t.Baz); 

    Assert.IsTrue(thingMap.ContainsKey(3, 4)); 
    Assert.AreEqual("brown", thingMap[3, 4]); 

    thingMap.DefaultValue = string.Empty; 
    Assert.AreEqual("brown", thingMap[3, 4]); 
    Assert.AreEqual(string.Empty, thingMap[3, 6]); 

    thingMap.DefaultGeneration = (k1, k2) => (k1.ToString() + k2.ToString()); 

    Assert.IsFalse(thingMap.ContainsKey(3, 6)); 
    Assert.AreEqual("36", thingMap[3, 6]); 
    Assert.IsTrue(thingMap.ContainsKey(3, 6)); 
} 
Cuestiones relacionadas