Linq 查询以针对结构中的列表进行联接

本文关键字:列表 查询 结构 Linq | 更新日期: 2023-09-27 18:26:19

我有一个结构字典,其中一个成员是一个列表,其中包含适用于每个字典项的不同元素。

我想将这些元素

与每个项目连接起来,以便过滤它们和/或按元素对它们进行分组。

在SQL中,我熟悉如何连接表/查询以根据需要获取多行,但我是C#/Linq的新手。由于"列"可以是已经与正确的字典项目关联的对象/列表,我想知道如何使用它们来执行连接?

下面是结构的示例:

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

我想要一个给出此输出的查询(计数 = 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 我存储为密钥(。但是,出于过滤/分组的目的,我想它可能是一个没有问题的列表或数组。我总是可以做到的.到字典,其中键=项。之后的名字。

Linq 查询以针对结构中的列表进行联接

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

编辑

请注意,您也可以使用上面的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 });

lambda 表达式(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为"element"。 我们还将第一个参数的类型更改为其基类型之一 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;
}

调用站点现在如下所示;请注意 lambda 表达式现在如何合并我们在更改其签名时从方法主体中删除的逻辑:

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

这就是你所拥有的:选择许多! 好吧,该函数仍然具有错误的名称,实际实现包括验证源序列和选择器函数是否为非空,但这是核心逻辑。

来自 MSDN:SelectMany"将序列的每个元素投影到 IEnumerable 中,并将生成的序列平展为一个序列。

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

这会将数组平展为单个数组,然后计算唯一值。

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

如果您为此使用另一本字典怎么办。

Dictionary<String, string> dict2 = new Dictionary<string, string>();
 dict.foreach(item => item.elements.foreach(elem => dict2.Add(elem,item.name)));

然后,您可以查询新字典以获取计数,它以元素作为键,因此对于每个元素,它都有具有它的项目。因此,您可以找到有多少项目具有所需的元素

您可能希望从更简单的结构集合开始,但要从字典开始:

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

这将返回:

名称计数
   元素A 2
   ElementB 1

如果您追求的是分组/透视,则可以通过利用 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;
}