2009-12-17 11 views
14

Uświadomiłem sobie, że nie przekazałem wystarczającej ilości informacji dla większości ludzi, aby przeczytać mój umysł i zrozumieć wszystkie moje potrzeby, więc zmieniłem to nieco z oryginału.Elegancki sposób tworzenia zagnieżdżonego słownika w C#

Powiedzmy mam listę elementów danej klasy tak:

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

I chcę kategoryzować ciąg Baz opartą na wartościach Foo, potem poprzeczkę. Nie będzie co najwyżej jednej rzeczy dla każdej możliwej kombinacji wartości Foo i Bar, ale nie mam gwarancji, że wartość dla każdego z nich. Może to pomóc w konceptualizacji go jako informacji o komórce dla tabeli: Foo to numer wiersza, Bar to numer kolumny, a Baz to wartość, którą można tam znaleźć, ale niekoniecznie musi być obecna wartość dla każdej komórki.

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

Jaki jest łatwy, elegancki sposób generowania zagnieżdżonego słownika? Używam C# wystarczająco długo, że jestem przyzwyczajony do znalezienia prostego, jednoliniowego rozwiązania dla wszystkich typowych rzeczy takich jak ten, ale ten ma mnie zakłopotany.

+0

Co to jest punkt wyjścia? Lista obiektów Rzecz? –

+0

Dzięki za odpowiedzi do tej pory. Zaktualizowałem pytanie, aby było bardziej zrozumiałe. Wygląda na to, że niektórzy z was mieli już dobry pomysł. Kiedy przetestuję twoje odpowiedzi, zacznę awansować i przypisać zwycięzcę. – StriplingWarrior

Odpowiedz

25

Oto rozwiązanie przy użyciu 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

Użyj "var dict =" Możesz użyć LINQ do złożenia wielu instrukcji foreach: var bazs = dict.SelectMany (topPair => topPair.Value.Values); foreach (baz ciąg w bazach) { } } –

+0

To wydaje się być krótkie, eleganckie rozwiązanie, którego szukałem. Kombinacja GroupBy/ToDictionary była tym, z czym miałem problem wymyślić sam. Dziękuję Ci. – StriplingWarrior

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

Nie bardzo rozumiałeś sedno mojego pytania. Szukam instrukcji linq lub czegoś, co pozwoli mi wypełnić słownik z istniejącej listy, a nie po prostu go utworzyć. – StriplingWarrior

4

Zdefiniuj swój własny generic NestedDictionary klasa

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

następnie w kodzie piszesz

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

jeśli używasz int, int, ciąg jednej partii, zdefiniować niestandardową klasa też za to ..

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

, a następnie napisać:

NestedIntStringDictionary dict = 
      new NestedIntStringDictionary(); 

EDIT: Aby dodać możliwość skonstruowania konkretną instancję z podanej listy przedmiotów:

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

a następnie napisać:

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

Jak wyglądałby akcesor? –

+0

@Scott W .: 'Tuple '. – jason

+0

W jaki sposób pomaga mi to elegancko budować zagnieżdżony słownik z danych, które otrzymałem? – StriplingWarrior

2

Możesz być w stanie użyj numeru KeyedCollection, w którym zdefiniujesz:

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

Nie jest dla mnie oczywiste, jak to rozwiązałoby mój problem. Proszę opracuj. – StriplingWarrior

16

elegancki sposób byłoby nie tworzenia słowników siebie, ale używać LINQ GroupBy i ToDictionary wygenerować go dla Ciebie.

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

Zakładam, że kwalifikując Baz użyciu Foo i Bar to znaczy, że jeśli dwie rzeczy mają zarówno Foo i Bar równa się wówczas ich wartość Baz być taka sama, jak dobrze. Proszę popraw mnie jeżeli się mylę.

Jesteś w zasadzie grupą według właściwości Foo ...
następnie dla każdej grupy wynikowej grupujesz na własność Bar ...
, a następnie dla każdej wynikowej grupy przyjmujesz pierwszą wartość Baz jako wartość słownika.

Jeśli zauważyłeś, nazwy metod odpowiadają dokładnie temu, co próbujesz zrobić. :-)


EDIT: Oto kolejny sposób korzystania listowych zapytań, są dłużej, ale są ciche łatwiejsze do odczytania i 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); 

Niestety, nie ma odpowiednika zrozumienie kwerendy dla ToDictionary więc nie jest tak elegancka jak wyrażenia lambda.

...

Mam nadzieję, że to pomoże.

+1

Doskonała odpowiedź. – Tinister

+3

* westchnienie * Nie mogę się doczekać, aby wydostać się z .NET 2.0, umierając, aby użyć LINQ. –

+1

Moje sympatie ... = \ –

3

Innym podejściem byłoby wpisanie słownika przy użyciu anonimowego typu na podstawie wartości Foo i 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}]; 

To skutecznie spłaszcza hierarchię w jeden słownik. Pamiętaj, że tego słownika nie można ujawnić zewnętrznie, ponieważ jest on oparty na typie anonimowym.

Jeśli kombinacja wartości Foo i Bar nie jest unikatowa w oryginalnej kolekcji, należy najpierw je pogrupować.

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

Doceniam twoje poświęcenie czasu na wymyślenie tak kompletnej odpowiedzi i mogę zrozumieć, dlaczego wymyśliłeś to rozwiązanie w oparciu o to, jak pierwotnie sformułowałem to pytanie, ale tak naprawdę nie robi tego, czego potrzebuję. – StriplingWarrior

+0

robi to, co jest mi potrzebne do zrobienia :) –

1

Myślę, że najprostszym podejściem byłoby użycie metod rozszerzenia LINQ. Oczywiście nie przetestowałem tego kodu dla performace.

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

Wyjście: dwa klucz klasa Mapa

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

Skorzystaj BeanMap użytkownika. Dostępna jest również mapa z trzema kluczami i można ją łatwo rozszerzyć, jeśli potrzebujesz n kluczy.

http://beanmap.codeplex.com/

Twoje rozwiązanie będzie wtedy wyglądać tak:

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)); 
} 
Powiązane problemy