使用LINQ从一个List<>中获取不在另一个List<>中的项

724

我认为有一个简单的LINQ查询可以做到这一点,但我不确定怎么做。

给定这段代码:

class Program
{
    static void Main(string[] args)
    {
        List<Person> peopleList1 = new List<Person>();
        peopleList1.Add(new Person() { ID = 1 });
        peopleList1.Add(new Person() { ID = 2 });
        peopleList1.Add(new Person() { ID = 3 });

        List<Person> peopleList2 = new List<Person>();
        peopleList2.Add(new Person() { ID = 1 });
        peopleList2.Add(new Person() { ID = 2 });
        peopleList2.Add(new Person() { ID = 3 });
        peopleList2.Add(new Person() { ID = 4 });
        peopleList2.Add(new Person() { ID = 5 });
    }
}

class Person
{
    public int ID { get; set; }
}

我希望执行一个LINQ查询,以便给出所有在peopleList2中但不在peopleList1中的人。

这个例子应该给我两个人(ID = 4 和 ID = 5)。


3
将ID设置为只读可能是个好主意,因为一个对象的身份标识在其生命周期内不应该改变。当然,除非你的测试或ORM框架要求它是可变的。 - CodesInChaos
3
根据这个图表,我们能称之为“左 (或右) 排除连接”吗? - Nate Anderson
11个回答

1229

可以使用以下的LINQ表达式来解决这个问题:

var result = peopleList2.Where(p => !peopleList1.Any(p2 => p2.ID == p.ID));

通过 LINQ 表达式,有一种替代方法,一些开发人员认为更易读:

var result = peopleList2.Where(p => peopleList1.All(p2 => p2.ID != p.ID));

警告:正如评论中指出的那样,这些方法需要进行一个 O(n*m) 的操作。虽然这可能还好,但可能会引入性能问题,尤其是如果数据集非常大的话。如果这不能满足您的性能要求,您可能需要评估其他选项。然而,由于所述的要求是使用 LINQ 解决方案,因此这里没有探讨其他选项。像往常一样,请根据您的项目对性能要求评估任何方法。


46
你知道这个问题有一个O(n*m)的解决方案,但其实它可以在O(n+m)的时间内轻松解决吗? - Niki
43
@nikie,OP要求使用Linq来解决问题。也许他正在学习Linq。如果问题是关于最高效的方法,我的回答可能会有所不同。 - Klaus Byskov Pedersen
70
@nikie,您能分享一下您的简单解决方案吗? - Rubio
23
这段代码等价且我发现更容易理解:var result = peopleList2.Where(p => peopleList1.All(p2 => p2.ID != p.ID)); - AntonK
49
@Menol - 批评一个正确回答问题的人可能有些不公平。人们不需要预料未来的人会以何种方式和情境遇到这个答案。实际上,你应该直接向nikie发出批评,因为他花时间表示知道替代方案却没有提供它。 - Chris Rogers
显示剩余10条评论

539

如果你重写了 People 类型的相等性,那么你也可以使用:

peopleList2.Except(peopleList1)

Except 的速度应该比 Where(...Any) 变体快得多,因为它可以将第二个列表放入哈希表中。Where(...Any) 的运行时间为 O(peopleList1.Count * peopleList2.Count),而基于 HashSet<T> 的变体(几乎)具有 O(peopleList1.Count + peopleList2.Count) 的运行时间。

Except 隐式地删除重复项。这不会影响您的情况,但可能会对类似情况造成问题。

或者,如果您想要快速的代码,但又不想覆盖相等性:

var excludedIDs = new HashSet<int>(peopleList1.Select(p => p.ID));
var result = peopleList2.Where(p => !excludedIDs.Contains(p.ID));

这个变量不会删除重复项。


1
只有在重写Equals方法来比较ID的情况下,这才能起作用。 - Klaus Byskov Pedersen
52
这就是为什么我写道你需要覆盖相等性(override the equality)。但是我已经添加了一个例子,即使没有这样做也可以运行。 - CodesInChaos
4
如果Person是一个结构体,这个方法也可以使用。然而,由于Person类有一个名为“ID”的属性,但它并不能完全标识该类 - 如果它能够标识该类,那么就需要重写equals方法以使相等的ID值意味着相等的对象。一旦Person类中的这个错误被修复,那么这种方法就更优了(除非将“ID”重命名为其他的名称,以避免误导)。 - Jon Hanna
6
当谈论一串字符串(或其他基本对象)时,它也非常有效,这正是我在查找时发现这个帖子的主题。 - Dan Korn
@Ian 实际上,无论如何你都必须覆盖Equals()和GetHashCode() - leguminator
显示剩余4条评论

82

或者如果你想没有否定:

var result = peopleList2.Where(p => peopleList1.All(p2 => p2.ID != p.ID));

基本上它表示从peopleList2获取所有内容,其中peopleList1中的所有id与peoplesList2中的id不同。

与被接受的答案略有不同的方法 :)


6
这种方法(超过50,000个项目的列表)比“ANY”方法显著更快! - DaveN
12
这可能会更快,只因它很懒。请注意,目前还没有执行任何实际工作。只有在列举列表时,才会真正执行工作(通过调用ToList或将其用作foreach循环的一部分等)。 - Xtros

37

既然至今为止所有的解决方案都使用了流畅语法,那么对于那些有兴趣的人,这里提供一种查询表达式语法的解决方案:

var peopleDifference = 
  from person2 in peopleList2
  where !(
      from person1 in peopleList1 
      select person1.ID
    ).Contains(person2.ID)
  select person2;

我认为这个答案与其他给出的答案有足够的不同,可能会对一些人很有用,尽管它可能不是处理列表的最佳选择。但对于包含索引 ID 的表格,这绝对是最好的选择。


2
谢谢。第一个回答涉及查询表达式语法。 - Generic Name
我只找到一个带有查询表达式语法的答案。谢谢! - Shankar Naru

19

我来晚了,但是一个既好用又兼容Linq to SQL的解决方案是:

List<string> list1 = new List<string>() { "1", "2", "3" };
List<string> list2 = new List<string>() { "2", "4" };

List<string> inList1ButNotList2 = (from o in list1
                                   join p in list2 on o equals p into t
                                   from od in t.DefaultIfEmpty()
                                   where od == null
                                   select o).ToList<string>();

List<string> inList2ButNotList1 = (from o in list2
                                   join p in list1 on o equals p into t
                                   from od in t.DefaultIfEmpty()
                                   where od == null
                                   select o).ToList<string>();

List<string> inBoth = (from o in list1
                       join p in list2 on o equals p into t
                       from od in t.DefaultIfEmpty()
                       where od != null
                       select od).ToList<string>();

http://www.dotnet-tricks.com/Tutorial/linq/UXPF181012-SQL-Joins-with-C致敬。


16

这个 Enumerable 扩展允许您定义一个要排除的项目列表和一个用于查找用于执行比较的键的函数。

public static class EnumerableExtensions
{
    public static IEnumerable<TSource> Exclude<TSource, TKey>(this IEnumerable<TSource> source,
    IEnumerable<TSource> exclude, Func<TSource, TKey> keySelector)
    {
       var excludedSet = new HashSet<TKey>(exclude.Select(keySelector));
       return source.Where(item => !excludedSet.Contains(keySelector(item)));
    }
}

你可以这样使用它

list1.Exclude(list2, i => i.ID);

通过拥有@BrianT的代码,我如何将其转换为使用您的代码? - Nicke Manarin
在某个地方创建一个新的类,其中包含 Bertrand 回复中的 EnumerableExtensions 代码。在执行查询的类中添加 using 语句。然后将选择代码更改为 var result = peopleList2.Exclude(peopleList1, i => i.ID); - Shane Knowles

14

Klaus的回答很好,但Resharper会要求你“简化LINQ表达式”:

var result = peopleList2.Where(p => peopleList1.All(p2 => p2.ID != p.ID));

(意思是可以用Resharper工具自动简化以上代码)


值得注意的是,如果有超过一个属性绑定了这两个对象(比如 SQL 复合键),这个技巧就不起作用了。 - Alrekr
Alrekr - 如果你的意思是“如果需要比较更多的属性,则需要比较更多的属性”,那么我认为这是非常明显的。 - Lucas

2
一旦你编写了一个通用的FuncEqualityComparer,你就可以在任何地方使用它。
peopleList2.Except(peopleList1, new FuncEqualityComparer<Person>((p, q) => p.ID == q.ID));

public class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, T, bool> comparer;
    private readonly Func<T, int> hash;

    public FuncEqualityComparer(Func<T, T, bool> comparer)
    {
        this.comparer = comparer;
        if (typeof(T).GetMethod(nameof(object.GetHashCode)).DeclaringType == typeof(object))
            hash = (_) => 0;
        else
            hash = t => t.GetHashCode(); 
    }

    public bool Equals(T x, T y) => comparer(x, y);
    public int GetHashCode(T obj) => hash(obj);
}

1
首先,从满足条件的集合中提取ID。
List<int> indexes_Yes = this.Contenido.Where(x => x.key == 'TEST').Select(x => x.Id).ToList();

其次,使用“比较”语句来选择与所选不同的ID。

List<int> indexes_No = this.Contenido.Where(x => !indexes_Yes.Contains(x.Id)).Select(x => x.Id).ToList();

显然你可以使用 x.key != "TEST",但这只是一个例子。


0

这里有一个可行的示例,可以获取求职者尚未具备的IT技能。

//Get a list of skills from the Skill table
IEnumerable<Skill> skillenum = skillrepository.Skill;
//Get a list of skills the candidate has                   
IEnumerable<CandSkill> candskillenum = candskillrepository.CandSkill
       .Where(p => p.Candidate_ID == Candidate_ID);             
//Using the enum lists with LINQ filter out the skills not in the candidate skill list
IEnumerable<Skill> skillenumresult = skillenum.Where(p => !candskillenum.Any(p2 => p2.Skill_ID == p.Skill_ID));
//Assign the selectable list to a viewBag
ViewBag.SelSkills = new SelectList(skillenumresult, "Skill_ID", "Skill_Name", 1);

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