Linq查询加入结构中的列表

mto*_*one 5 c# linq linq-to-objects

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

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

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

以下是结构示例:

name   elements
item1  list: elementA
item2  list: elementA, elementB
Run Code Online (Sandbox Code Playgroud)

我想要一个提供此输出的查询(count = 3)

name   elements
item1  elementA
item2  elementA
item2  elementB
Run Code Online (Sandbox Code Playgroud)

最终,将它们分组为:

   element    count
   ElementA   2
   ElementB   1
Run Code Online (Sandbox Code Playgroud)

这是我的代码开始计算字典项目.

    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
    }
Run Code Online (Sandbox Code Playgroud)

编辑:我真的不需要输出是字典.我用它来存储我的数据,因为它运行良好并防止重复(我确实有我存储的唯一item.name作为键).但是,出于过滤/分组的目的,我猜它可能是没有问题的列表或数组.我总是可以.ToDictionary之后是key = item.Name.

pho*_*oog 3

var q = from t in dict
    from v in t.Value.elements
    select new { name = t.Key, element = v };
Run Code Online (Sandbox Code Playgroud)

这里的方法是Enumerable.SelectMany。使用扩展方法语法:

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

编辑

请注意,您也可以使用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 };
}
Run Code Online (Sandbox Code Playgroud)

扩展方法语法怎么样(或者,这里到底发生了什么)?

(这部分是受到 Eric Lippert 在/sf/answers/189335681/的帖子的启发;我有一个更复杂的解释,然后我读了它,并想出了这个:)

假设我们想要避免声明 NameElement 类型。我们可以通过传入函数来使用匿名类型。我们将从以下位置更改调用:

var q = GetResults(dict);
Run Code Online (Sandbox Code Playgroud)

对此:

var q = GetResults(dict, (string1, string2) => new { name = string1, element = string2 });
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

T类型推断允许我们在不指定名称的情况下调用该函数。这很方便,因为(据我们 C# 程序员所知),我们使用的类型没有名称:它是匿名的。

请注意,变量t是 now pair,以避免与类型参数混淆T,并且v是 now e,表示“元素”。我们还将第一个参数的类型更改为其基本类型之一IEnumerable<KeyValuePair<string, MyStruct>>。它比较冗长,但它使该方法更有用,并且最终会有所帮助。由于该类型不再是字典类型,我们还将参数名称从 更改dictpairs

我们可以进一步概括这一点。第二个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;
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以轻松更改签名:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

为了使该方法更有用(并且其实现更简洁),让我们KeyValuePair<string, MyStruct>用类型参数替换类型TSource。我们将同时更改一些其他名称:

T     -> TResult
pairs -> sourceSequence
pair  -> sourceElement
Run Code Online (Sandbox Code Playgroud)

而且,为了好玩,我们将其作为扩展方法:

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;
}
Run Code Online (Sandbox Code Playgroud)

现在你已经看到了:SelectMany!好吧,该函数的名称仍然错误,实际实现包括验证源序列和选择器函数是否为非空,但这就是核心逻辑。

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