使用LINQ获取两个IEnumerables的交集

5

我有两个类型为 IEnumerable 的实例,如下所示。

IEnumerable<Type1> type1 = ...;
IEnumerable<Type2> type2 = ...;

无论是 Type1 还是 Type2 都包含名为 common 的成员,因此即使它们属于不同的类,我们仍然可以像这样进行关联。

type1[0].common == type2[4].common

我正在尝试过滤掉那些在type2中没有对应common值的type1元素,并且基于每个元素选择一个值创建一个字典。现在,我是通过下面的双重循环来实现这一点。

Dictionary<String, String> intersection = ...;
foreach (Type1 t1 in type1)
  foreach(Type2 t2 in type2)
    if (t1.common == t2.common)
      intersection.Add(t1.Id, t2.Value);

现在,我已经尝试使用LINQ,但是所有的.Where.Select.ForEach都让我头疼。有没有一种方法可以使用LINQ来整洁地执行相同的操作呢?


LINQ 最终还是要枚举的。你遇到了性能问题吗? - paparazzo
不是的。但其他程序员正在嘲笑我。 :( 他们说我太累了,找不到代码,这是不可能的。虽然我可以承认前者,但我拒绝同意后者。 :) - user1675891
1
当您有一个具有两个匹配的t2的t1时会发生什么? - Eric Lippert
@EricLippert 说得好。然而,在这种情况下是无关紧要的。要比较的值是GUID,因此如果有多个实例,我们面临的问题比别人嘲笑我更大,呵呵。尽管如此,我应该提到它,这个观点是正确的。 - user1675891
@Servy 谢谢。我同意。昨天我非常累,就是无法让它工作。当我能够说明可以使用LINQ解决问题并展示一个可行的代码时,这将是一个更有力的观点,而不仅仅是说明可以使用LINQ解决问题。 :) - user1675891
显示剩余2条评论
5个回答

15

当两个序列有共同点,并且你想基于这个共性筛选它们的乘积时,高效的查询方式是连接(join)。假设Type1CustomerType2Order。每个顾客都有一个CustomerID,每个订单也有一个CustomerID。然后你就可以这样说:

var query = from customer in customers
            join order in orders 
              on customer.CustomerId equals order.CustomerId
            select new { customer.Name, order.Product };

迭代将为您提供一个序列,其中包含每个已下订单的客户名称和他们所有的产品。因此,如果客户Suzy订购了一份煎饼和一份披萨,而客户Bob则订购了一份牛排,那么您将获得这些对。

Suzy, pancake
Suzy, pizza
Bob, steak

如果你想将它们按顾客分组,使每个顾客都有自己订单的列表,那么这就是一个分组连接。

var query = from customer in customers
            join order in orders 
              on customer.CustomerId equals order.CustomerId 
              into products
            select new { customer.Name, products };

迭代它将给你一些 pairs,其中第一个项目是名称,第二个项目是产品序列。

Suzy, { pancake, pizza }
Bob, { steak }

纯金,嘿嘿!这一次,第一种方法正是我所需要的(我不知道为什么我自己没能设置它)。但是另一种方法对我来说是新的。我不知道分组,但它很快就会派上用场。 - user1675891
我认为在查询表达式中加入等效的扩展方法调用会使人更容易理解发生了什么以及性能特征是什么。 - CodesInChaos

1

另一个选择是加入。我在下面做了一个快速的控制台应用程序,但不得不自己编造数据。希望我正确理解了你的问题。

public class Type1
{
    public string ID { get; set; }
    public Guid common { get; set; }
}
public class Type2
{
    public string Value { get; set; }
    public Guid common { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        Guid CommonGuid = Guid.NewGuid();

        IEnumerable<Type1> EnumType1 = new List<Type1>()
        {
            new Type1() {
                ID = "first",
                common = CommonGuid
            },
            new Type1() {
                ID = "second",
                common = CommonGuid
            },
            new Type1() {
                ID = "third",
                common = Guid.NewGuid()
            }
        } as IEnumerable<Type1>;

        IEnumerable<Type2> EnumType2 = new List<Type2>()
        {
            new Type2() {
                Value = "value1",
                common = CommonGuid
            },
            new Type2() {
                Value = "value2",
                common = Guid.NewGuid()
            },
            new Type2() {
                Value = "value3",
                common = CommonGuid
            }
        } as IEnumerable<Type2>;

        //--The part that matters
        EnumType1                       //--First IEnumerable
            .Join(                      //--Command
                EnumType2,              //--Second IEnumerable
                outer => outer.common,  //--Key to join by from EnumType1
                inner => inner.common,  //--Key to join by from EnumType2
                (inner, outer) => new { ID = inner.ID, Value = outer.Value })  //--What to do with matching "rows"
            .ToList()   //--Not necessary, just used so that I can use the foreach below
            .ForEach(item =>
                {
                    Console.WriteLine("{0}: {1}", item.ID, item.Value);
                });

        Console.ReadKey();
    }
}

以下是显示的内容:
第一个: 值1
第一个: 值3
第二个: 值1
第二个: 值3


是的。很好的例子 - 非常清晰和有信息量。我以它为起点,将操作缩短为只有四行LINQ。 - user1675891

0

假设您仍然希望将交集保留为 Dictionary<string, string>:

IEnumerable<Type1> list1;
IEnumerable<Type2> list2;

Dictionary<string, string> intersection = 
    (from item1 in list1
     from item2 in list2
     where item1.common = item2.common
     select new { Key = item1.Id, Value = item2.Value })
         .ToDictionary(x => x.Key, x => x.Value);

这种方法的效率比Join要低得多。你创建了很多对,然后再进行筛选,而不是像Join一样从一开始就创建所有正确的对。 - Servy

-1
type1.where(i=>type2.where(j=>j.common == i.common).Count > 0);

这将为您提供仅匹配的列表。


这比“Join”要低效得多。您在此处通过线性搜索执行了许多“type2”迭代,而不是对集合进行基于集合的操作。您还没有将“t1”项与“type2”中的配对进行匹配。 - Servy

-1

我可能漏掉了什么,但这样做可以吗:

type1
 .where(t1 => type2.Any(t2 => t1.common == t2.common)
 .ToDictionary(t1 => t1.Id)

或者像Servy建议的那样

type1
  .Join(type2, a => a.common, b => b.common, (a1,b1) => a1)
  .ToDictionary(t1 => t1.Id)

这比“Join”要低效得多。您在此处通过线性搜索执行了许多“type2”迭代,而不是对集合进行基于集合的操作。您还没有将“t1”项与“type2”中的伴侣配对。 - Servy
@Servy 您是正确的。效率从来不是一个问题。我试图向一个更熟悉for循环的人传达LINQ的概念。为什么不发表您的答案呢! - Richard Schneider
为什么我要在已经有两个正确解决此问题的答案的情况下发布一个答案,尤其是其中一个来自Eric Lippert。 在我看来,传达LINQ概念意味着教授适当的方法来完成手头的任务,而这不是。 作为一般规则,每当您看到使用嵌套AnyWhere模式时,就会出现问题。 如果可能的话,type2应该是使用ContainsHashSet。 无论如何,这个解决方案甚至都不起作用。 结果需要是一对项目,而不仅仅是其中之一。 - Servy
@Servy 请停止打我。我在Eric之前发布了。 - Richard Schneider
这没关系,但是你告诉我在他发表答案很久之后再发布另一个答案。你的答案仍然是错误的,并且没有解决问题,而且还朝着错误的方向开始了。它应该被编辑为正确回答问题,或者直接删除,因为有多个其他正确的答案。 - Servy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接