Linq-запрос для объединения со списком в структуре - PullRequest
5 голосов
/ 29 февраля 2012

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

Я хотел бы объединить эти элементы для каждого элемента, чтобы отфильтровать их и / или сгруппировать по элементам.

В SQL я знаком с объединением таблиц / запросов для получения нескольких строк по желанию, но я новичок в C # / Linq. Поскольку «столбец» может быть объектом / списком, уже связанным с соответствующими элементами словаря, мне интересно, как я могу использовать их для выполнения объединения?

Вот пример структуры:

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

Я хотел бы запрос, который дает этот вывод (count = 3)

name   elements
item1  elementA
item2  elementA
item2  elementB

В конечном итоге, сгруппировать их следующим образом:

   element    count
   ElementA   2
   ElementB   1

Вот мой код начала подсчета словарных элементов.

    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
    }

Редактировать: мне не нужен выходной словарь. Я использовал его для хранения своих данных, потому что он работает хорошо и предотвращает дублирование (у меня есть уникальный item.name, который я храню в качестве ключа). Тем не менее, для целей фильтрации / группировки, я думаю, это может быть список или массив без проблем. Я всегда могу сделать .ToDictionary, где ключ = item.Name потом.

Ответы [ 6 ]

3 голосов
/ 29 февраля 2012
var q = from t in dict
    from v in t.Value.elements
    select new { name = t.Key, element = v };

Метод здесь Enumerable.SelectMany.Использование синтаксиса метода расширения:

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

EDIT

Обратите внимание, что вы также можете использовать t.Value.name выше вместо t.Key, так как эти значения равны.

Итак, что здесь происходит?

Синтаксис понимания запросов, вероятно, проще всего понять;Вы можете написать эквивалентный блок итератора, чтобы увидеть, что происходит.Однако мы не можем сделать это просто с анонимным типом, поэтому мы объявим тип для возврата:

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

Как насчет синтаксиса метода расширения (или, что действительно )что происходит здесь??

(Это частично вдохновлено постом Эрика Липперта в https://stackoverflow.com/a/2704795/385844; У меня было гораздо более сложное объяснение, потом я его прочитал и придумал:)

Допустим, мы хотим избежать объявления типа NameElement.Мы могли бы использовать анонимный тип, передав функцию.Мы изменили бы вызов с этого:

var q = GetResults(dict);

на этот:

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

Лямбда-выражение (string1, string2) => new { name = string1, element = string2 } представляет функцию, которая принимает 2 строки - определенные списком аргументов(string1, string2) - и возвращает экземпляр анонимного типа, инициализированный этими строками - определяется выражением new { name = string1, element = string2 }.

Соответствующая реализация такова:

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

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

Обратите внимание, что переменная t теперь pair, чтобы избежать путаницы с параметром типа T, а v теперь e, для "элемента".Мы также изменили тип первого параметра на один из его базовых типов, IEnumerable<KeyValuePair<string, MyStruct>>.Это более многословно, но делает метод более полезным и в конечном итоге будет полезным.Поскольку тип больше не является типом словаря, мы также изменили имя параметра с dict на pairs.

Мы могли бы обобщить это далее.Второй foreach имеет эффект проецирования пары ключ-значение на последовательность типа T. Весь этот эффект может быть заключен в одну функцию;тип делегата будет Func<KeyValuePair<string, MyStruct>, T>.Первым шагом является рефакторинг метода, поэтому у нас есть один оператор, который преобразует элемент pair в последовательность, используя метод Select для вызова делегата resultSelector:

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

Теперь мыможно легко изменить подпись:

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

Сайт вызова теперь выглядит так;обратите внимание, что лямбда-выражение теперь включает логику, которую мы удалили из тела метода, когда мы изменили его сигнатуру:

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

Чтобы сделать метод более полезным (и его реализация менее многословной), давайте заменим тип KeyValuePair<string, MyStruct> с параметром типа, TSource.В то же время мы изменим некоторые другие имена:

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

И, просто для удовольствия, мы сделаем это методом расширения:

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

И вот оно у вас есть: Выберите много!Ну, у функции все еще есть неправильное имя, и фактическая реализация включает в себя проверку того, что исходная последовательность и функция селектора не равны NULL, но это основная логика.

С MSDN :SelectMany "проецирует каждый элемент последовательности в IEnumerable и объединяет результирующие последовательности в одну последовательность."

1 голос
/ 29 февраля 2012
/* 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();
1 голос
/ 29 февраля 2012

Это объединяет массивы в один массив, а затем подсчитывает уникальные значения.

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

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

Используя следующий словарь:

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

Я получаю этот вывод:

 A: 3
 B: 2
0 голосов
/ 13 марта 2012

Если то, что вам нужно, это группировка / поворот, это можно сделать более декларативно, используя группирование LINQ и вообще избегая словарей:

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 голосов
/ 29 февраля 2012

Вы можете начать с более простой коллекции структур, но из своего словаря:

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

Возвращает:

Имя Количество
ЭлементА 2
ЭлементB 1

0 голосов
/ 29 февраля 2012

Что делать, если вы используете другой словарь для этого.

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

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

тогда вы можете запросить новый словарь, чтобы получить счетчик, у него есть элемент в качестве ключа, поэтому для каждого элемента у него есть элементы, которые его имели. Таким образом, вы можете найти, сколько предметов имеет элемент, который вы хотите

...