Используйте LINQ для получения элементов в одном списке <>, которых нет в другом списке <>

526

Я бы предположил, что есть простой запрос 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)

JSprang
источник
3
Возможно, это хорошая идея сделать ID доступным только для чтения, поскольку идентичность объекта не должна меняться в течение времени его существования. Если, конечно, ваша среда тестирования или ORM не требует, чтобы она была изменчивой.
CodesInChaos
2
Можем ли мы назвать это «левым (или правым) исключением соединения» согласно этой схеме?
Красный горох

Ответы:

912

Это можно решить с помощью следующего выражения 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, эти варианты здесь не рассматриваются. Как всегда, оцените любой подход в отношении требований к производительности, которые может иметь ваш проект.

Клаус Бысков Педерсен
источник
34
Вы знаете, что это O (n * m) решение проблемы, которую легко решить за O (n + m) время?
Ники
32
@nikie, ОП попросил решение, использующее Linq. Может быть, он пытается выучить Linq. Если бы вопрос был наиболее эффективным, мой вопрос не обязательно был бы таким же.
Клаус Бысков Педерсен
46
@nikie, хочешь поделиться своим простым решением?
Рубио
18
Это эквивалентно, и мне легче следовать: var result = peopleList2.Where (p => peopleList1.All (p2 => p2.ID! = P.ID));
AntonK
28
@ Менол - может быть немного несправедливо критиковать того, кто правильно отвечает на вопрос. Людям не нужно предвидеть все способы и условия, в которых будущие люди могут наткнуться на ответ. На самом деле, вы должны указать это ники - которые нашли время заявить, что они знали об альтернативе, не предоставляя ее.
Крис Роджерс
397

Если вы переопределите равенство людей, то вы также можете использовать:

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

Этот вариант не удаляет дубликаты.

CodesInChaos
источник
Это сработало бы, только если Equalsбы было отменено сравнение идентификаторов.
Клаус Бысков Педерсен
34
Вот почему я написал, что нужно переопределить равенство. Но я добавил пример, который работает даже без этого.
CodesInChaos
4
Это также сработало бы, если Person был структурой. Однако, как представляется, Person кажется неполным классом, так как у него есть свойство, называемое «ID», которое не идентифицирует его - если бы оно действительно идентифицировало его, то равные были бы переопределены, чтобы равный идентификатор означал равный Person. Как только эта ошибка в Person исправлена, этот подход становится лучше (если только ошибка не исправлена ​​путем переименования «ID» во что-то еще, что не вводит в заблуждение, представившись идентификатором).
Джон Ханна
2
Это также прекрасно работает, если вы говорите о списке строк (или других базовых объектов), который я искал, когда натолкнулся на этот поток.
Дэн Корн
@DanKorn То же самое, это более простое решение, по сравнению с где, для базового сравнения, int, объекты ref, строки.
Лабиринт
73

Или если хотите без отрицания

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

В основном это говорит получить все из peopleList2, где все идентификаторы в peopleList1 отличаются от идентификатора в peopleList2.

Просто немного отличается подход от принятого ответа :)

user1271080
источник
5
Этот метод (список из более чем 50000 элементов) был значительно быстрее, чем ЛЮБОЙ метод!
DaveN
5
Это может быть быстрее только потому, что это ленивый. Обратите внимание, что это пока не делает никакой реальной работы. До тех пор, пока вы не перечислите список, он действительно выполняет свою работу (вызывая ToList или используя его как часть цикла foreach и т. Д.)
Xtros
32

Поскольку во всех решениях на сегодняшний день используется свободный синтаксис, вот решение для синтаксиса выражений запросов для интересующихся:

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

Я думаю, что он достаточно отличается от ответов, которые могут быть интересны для некоторых, даже если бы он был неоптимальным для списков. Теперь для таблиц с индексированными идентификаторами, это определенно будет путь.

Михаил Гольдштейн
источник
Спасибо. Первый ответ, который беспокоит синтаксис выражения запроса.
Общее название
15

Немного опоздал на вечеринку, но хорошее решение, которое также совместимо с 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

Ричард Окерби
источник
12

Клаус ответил великолепно, но ReSharper попросит вас «упростить выражение LINQ»:

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

Брайан Т
источник
Стоит отметить, что этот трюк не сработает, если два объекта связывают два объекта (например, составной ключ SQL).
Alrekr
Alrekr - Если вы хотите сказать, что «вам нужно будет сравнить больше свойств, если нужно сравнивать больше свойств», то я бы сказал, что это довольно очевидно.
Лукас Морган
8

Это Enumerable Extension позволяет вам определить список исключаемых элементов и функцию, используемую для поиска ключа, который будет использоваться для сравнения.

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);
Bertrand
источник
Имея код, который есть у @BrianT, как я могу преобразовать его для использования вашего кода?
Nicke Manarin
0

Вот рабочий пример, который дает ИТ-навыки, которых у кандидата на работу еще нет.

//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);
Брайан Куинн
источник
0

во-первых, извлечь идентификаторы из коллекции, где условие

List<int> indexes_Yes = this.Contenido.Where(x => x.key == 'TEST').Select(x => x.Id).ToList();

во-вторых, используйте положение «сравнить», чтобы выбрать идентификаторы, различающиеся для выбора

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

Очевидно, что вы можете использовать x.key! = "TEST", но это только пример

Анхель Ибаньес
источник
0

Как только вы напишите общий 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);
}
Wouter
источник