c# - Linq 查询以加入结构中的列表

标签 c# linq linq-to-objects

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

我想针对每个项目加入这些元素,以便过滤它们和/或按元素对它们进行分组。

在 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 where key = item.Name。

最佳答案

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 ,因为这些值是相等的。

那么,这里发生了什么?

查询理解语法可能是最容易理解的;您可以编写等效的迭代器 block 来查看发生了什么。但是,我们不能简单地使用匿名类型来做到这一点,因此我们将声明一个类型以返回:

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

扩展方法语法如何(或者,这里真正发生了什么)?

(这在一定程度上受到 Eric Lippert 在 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 ,对于“元素”。我们还将第一个参数的类型更改为其基本类型之一,IEnumerable<KeyValuePair<string, MyStruct>> .它比较冗长,但它使方法更有用,最终会有所帮助。由于该类型不再是字典类型,我们还将参数名称从 dict 更改为至 pairs .

我们可以进一步概括这一点。第二个foreach具有将键值对投影到类型 T 的序列的效果。整个效果可以封装在一个函数中;委托(delegate)类型将是 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;
}

就这样:SelectMany!好吧,这个函数仍然有一个错误的名字,实际的实现包括验证源序列和选择器函数是非空的,但这是核心逻辑。

来自 MSDN : SelectMany “将序列的每个元素投影到 IEnumerable 并将结果序列展平为一个序列。”

关于c# - Linq 查询以加入结构中的列表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9494362/

相关文章:

C# - Linq - XPathSelectElement 每次都返回相同的结果

c# - 通过 linq 从 XML 中获取值

c# - 使用 Visual Studio 设计器 - "Object reference not set to an instance of an object"(Visual Studio 2008)

c# - ASP.NET Web API CORS 不适用于 AngularJS

c# - 一元 : Why unary's behavior in c# varies with c/c++

c# - 如何使用 LINQ 和 C# 找到最接近 0,0 点的点

linq - LINQ to Object和LINQ to SQL查询之间的差异

c# - LINQ 和内存分配

Linq 将 IEnumerable(Of Struct) 连接到 IEnumerable(of object)

c# - react 性扩展内存使用