Use LINQ para obter itens em uma lista <>, que não estão em outra lista <>

526

Eu diria que há uma consulta LINQ simples para fazer isso, mas não sei exatamente como.

Dado este pedaço de código:

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

Gostaria de executar uma consulta LINQ para me fornecer todas as pessoas peopleList2que não estão peopleList1.

Este exemplo deve me dar duas pessoas (ID = 4 e ID = 5)

JSprang
fonte
3
Talvez seja uma boa idéia criar ID somente leitura, pois a identidade de um objeto não deve mudar ao longo do tempo de vida. A menos que sua estrutura de teste ou ORM exija que seja mutável.
CodesInChaos 15/10
2
Podemos chamar isso de "Junção Exclusiva à Esquerda (ou Direita)" de acordo com este diagrama?
The Red Pea

Respostas:

912

Isso pode ser resolvido usando a seguinte expressão LINQ:

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

Uma maneira alternativa de expressar isso via LINQ, que alguns desenvolvedores acham mais legível:

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

Aviso: Conforme observado nos comentários, essas abordagens exigem uma operação O (n * m) . Isso pode ser bom, mas pode apresentar problemas de desempenho, especialmente se o conjunto de dados for muito grande. Se isso não atender aos seus requisitos de desempenho, talvez você precise avaliar outras opções. Como o requisito declarado é para uma solução no LINQ, no entanto, essas opções não são exploradas aqui. Como sempre, avalie qualquer abordagem em relação aos requisitos de desempenho que seu projeto possa ter.

Klaus Byskov Pedersen
fonte
34
Você está ciente de que essa é uma solução O (n * m) para um problema que pode ser facilmente resolvido em O (n + m)?
Niki
32
@nikie, o OP pediu uma solução que use o Linq. Talvez ele esteja tentando aprender Linq. Se a pergunta tivesse sido da maneira mais eficiente, minha pergunta não teria sido necessariamente a mesma.
Klaus Byskov Pedersen
46
@nikie, gostaria de compartilhar sua solução fácil?
Rubio
18
Isso é equivalente e acho mais fácil de seguir: var result = peopleList2.Where (p => peopleList1.All (p2 => p2.ID! = P.ID));
AntonK
28
@ Menol - pode ser um pouco injusto criticar alguém que responde corretamente a uma pergunta. As pessoas não precisam prever todas as formas e contextos em que as pessoas futuras podem encontrar a resposta. Na realidade, você deve direcionar isso para nikie - que gastou um tempo para afirmar que sabia de uma alternativa sem fornecê-la.
21420 Chris Rogers
397

Se você substituir a igualdade de Pessoas, também poderá usar:

peopleList2.Except(peopleList1)

Exceptdeve ser significativamente mais rápido que a Where(...Any)variante, pois pode colocar a segunda lista em uma hashtable. Where(...Any)tem um tempo de execução de O(peopleList1.Count * peopleList2.Count)enquanto as variantes baseadas em HashSet<T>(quase) têm um tempo de execução de O(peopleList1.Count + peopleList2.Count).

Exceptremove implicitamente duplicatas. Isso não deve afetar o seu caso, mas pode ser um problema para casos semelhantes.

Ou se você deseja um código rápido, mas não deseja substituir a igualdade:

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

Esta variante não remove duplicatas.

CodesInChaos
fonte
Isso só funcionaria se Equalstivesse sido substituído para comparar IDs.
Klaus Byskov Pedersen
34
Foi por isso que escrevi que você precisa substituir a igualdade. Mas eu adicionei um exemplo que funciona mesmo sem isso.
CodesInChaos
4
Também funcionaria se Person fosse uma estrutura. No entanto, Person parece uma classe incompleta, pois possui uma propriedade chamada "ID" que não a identifica - se a identificasse, os iguais seriam substituídos para que o ID igual significasse Person. Uma vez que o bug no Person seja corrigido, essa abordagem será melhor (a menos que o bug seja corrigido renomeando "ID" para outra coisa que não se engane ao parecer um identificador).
Jon Hanna
2
Também funciona muito bem se você está falando sobre uma lista de strings (ou outros objetos básicos), que era o que eu estava procurando quando me deparei com este tópico.
Dan Korn
@ DanKorn Mesmo, esta é uma solução mais simples, em comparação com o where, para comparação básica, int, ref de objetos, seqüências de caracteres.
Maze
73

Ou se você quiser sem negação:

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

Basicamente, ele diz que obtém tudo do peopleList2, onde todos os IDs do peopleList1 são diferentes do id do PeopleList2.

Abordagem apenas um pouco diferente da resposta aceita :)

user1271080
fonte
5
Este método (lista de mais de 50.000 itens) foi significativamente mais rápido que o QUALQUER método!
DaveN
5
Isso pode ser mais rápido apenas porque é preguiçoso. Observe que isso ainda não está fazendo nenhum trabalho real. Não é até que você enumerar a lista que ele realmente faz o trabalho (chamando ToList ou usá-lo como parte de um loop foreach, etc.)
Xtros
32

Como todas as soluções até o momento usavam sintaxe fluente, eis uma solução na sintaxe da expressão de consulta, para os interessados:

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

Eu acho que é diferente o suficiente das respostas dadas para interessar a alguns, mesmo pensando que provavelmente seria subótimo para Listas. Agora, para tabelas com IDs indexados, esse seria definitivamente o caminho.

Michael Goldshteyn
fonte
Obrigado. Primeira resposta que incomoda com a sintaxe da expressão de consulta.
Nome genérico
15

Tarde demais para a festa, mas uma boa solução que também é compatível com 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>();

Parabéns pelo http://www.dotnet-tricks.com/Tutorial/linq/UXPF181012-SQL-Joins-with-C

Richard Ockerby
fonte
12

A resposta de Klaus foi ótima, mas o ReSharper solicitará que você "Simplifique a expressão LINQ":

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

Brian T
fonte
Vale ressaltar que esse truque não funcionará se houver mais de uma propriedade vinculando os dois objetos (pense na chave composta SQL).
Alrekr
Alrekr - Se o que você quer dizer é "você precisará comparar mais propriedades, se mais propriedades precisarem ser comparadas", então eu diria que isso é bastante óbvio.
Lucas Morgan
8

Esta extensão enumerável permite que você defina uma lista de itens a serem excluídos e uma função a ser usada para encontrar a chave a ser usada na comparação.

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

Você pode usá-lo desta maneira

list1.Exclude(list2, i => i.ID);
Bertrand
fonte
Por ter o código que @BrianT possui, como eu o converti para usar seu código?
Nicke Manarin
0

Aqui está um exemplo de trabalho que obtém habilidades de TI que um candidato a emprego ainda não possui.

//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);
Brian Quinn
fonte
0

primeiro, extraia os IDs da coleção em que a condição

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

segundo, use o parâmetro "compare" para selecionar os IDs diferentes da seleção

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

Obviamente, você pode usar x.key! = "TEST", mas é apenas um exemplo

Ángel Ibáñez
fonte
0

Depois de escrever um FuncEqualityComparer genérico, você pode usá-lo em qualquer lugar.

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
fonte