2009-12-17 2 views
14

Я понял, что не дал достаточной информации для большинства людей, чтобы читать мои мысли и понимать все мои потребности, поэтому я несколько изменил это из оригинала.Элегантный способ создания вложенного словаря в C#

Скажем, у меня есть список элементов класса, как это:

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

И я хочу, чтобы классифицировать строку Баз на основе значений Foo, затем Bar. Для каждой возможной комбинации значений Foo и Bar будет не более одной Thing, но я не гарантированно получаю ценность для каждого из них. Это может помочь концептуализировать его как информацию о ячейке для таблицы: Foo - номер строки, Bar - номер столбца, а Baz - это значение, которое можно найти там, но не обязательно будет значение, присутствующее для каждой ячейки.

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

Что такое простой, элегантный способ создания вложенного словаря? Я слишком долго использовал C#, чтобы привыкнуть к поиску простых однострочных решений для всех подобных вещей, но это меня настораживает.

+0

Что отправной точкой? Список объектов Thing? –

+0

Спасибо за ответы. Я обновил вопрос, чтобы сделать его более понятным. Похоже, у некоторых из вас была правильная идея. После того, как я проведу ваши ответы, я начну выигрывать и назначать победителя. – StriplingWarrior

ответ

25

Вот решение с помощью 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

Используйте «var dict =» И вы можете использовать LINQ для свертывания нескольких операторов foreach: var bazs = dict.SelectMany (topPair => topPair.Value.Values); foreach (string baz in bazs) { // ... } –

+0

Это, кажется, короткое, элегантное решение, которое я искал. Комбинация GroupBy/ToDictionary - это то, с чем я столкнулся самостоятельно. Спасибо. – StriplingWarrior

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

Вы не совсем поняли суть моего вопроса. Я ищу инструкцию linq или что-то, что позволило бы мне заполнить словарь из существующего списка, а не просто создавать его. – StriplingWarrior

4

Определите свой собственный общий NestedDictionary класс

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

затем в своем коде вы пишете

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

если использовать Int, Int, строковую много, определить пользовательские класс для этого тоже ..

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

, а затем написать:

NestedIntStringDictionary dict = 
      new NestedIntStringDictionary(); 

EDIT: Для того, чтобы добавить возможность построить конкретный экземпляр из предоставленного списка элементов:

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

, а затем написать:

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

Что бы выглядел аксессор? –

+0

@Scott W .: 'Tuple '. – jason

+0

Как это помогает мне элегантно построить вложенный словарь из данных, которые мне дали? – StriplingWarrior

2

Вы можете быть в состоянии используйте KeyedCollection, где вы определили:

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

Мне не сразу становится ясно, как это решит мою проблему. Пожалуйста, дополните. – StriplingWarrior

16

Элегантный способ будет не создавать словари самостоятельно, но использовать LINQ GroupBy и ToDictionary, чтобы создать его для вас.

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

Я полагаю, что по классификации Baz использованием Foo и Bar вы имеете в виду, что если две вещи имеют как Foo и Bar равно, то их значение Baz быть такой же, как хорошо. Пожалуйста, поправьте меня, если я ошибаюсь.

В первую очередь вы являетесь группой по недвижимости Foo ...
, то для каждой результирующей группы вы группируете по свойству Bar ...
затем для каждой результирующей группы вы берете первое значение как значение словаря.

Если вы заметили, имена методов совпадают именно с тем, что вы пытаетесь сделать. :-)


EDIT: Вот еще один способ использования постижений запроса, они больше, но тихо легче читать и 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); 

К сожалению, нет понимания запроса аналог для ToDictionary поэтому он не такой элегантный, как лямбда-выражения.

...

Надеюсь, это поможет.

+1

Отличный ответ. – Tinister

+3

* sigh * Я не могу дождаться выхода из .NET 2.0, умеющего использовать LINQ. –

+1

Мои симпатии ... = \ –

3

Другой подход заключается в том, чтобы закрепить свой словарь с помощью анонимного типа, основанного на значениях Foo и 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}]; 

Это эффективно сглаживает вашу иерархию в один словарь. Обратите внимание, что этот словарь не может быть открыт извне, поскольку он основан на анонимном типе.

Если комбинация значений Foo и Bar не уникальна в вашей первоначальной коллекции, вам необходимо сначала сгруппировать их.

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

Я ценю ваше время, чтобы найти такой полный ответ, и я могу понять, почему вы пришли к решению, основанному на том, как я изначально сформулировал этот вопрос, но на самом деле он не делает то, что мне нужно. – StriplingWarrior

+0

он делает то, что мне нужно это делать :) –

1

Я думаю, что самым простым подходом было бы использовать методы расширения LINQ. Очевидно, я не тестировал этот код для 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(); 

Выход: два ключ класса Map

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

Используйте BeanMap в. Существует также 3 ключевых карты, и она достаточно расширяема, если вам нужны n ключей.

http://beanmap.codeplex.com/

Ваше решение будет выглядеть так:

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)); 
} 
Смежные вопросы