Utilisez LINQ pour obtenir des éléments dans une liste <>, qui ne sont pas dans une autre liste <>

526

Je suppose qu'il existe une simple requête LINQ pour ce faire, je ne sais pas exactement comment.

Compte tenu de ce morceau de code:

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

Je voudrais effectuer une requête LINQ pour me donner toutes les personnes peopleList2qui ne sont pas dans peopleList1.

Cet exemple devrait me donner deux personnes (ID = 4 & ID = 5)

JSprang
la source
3
C'est peut-être une bonne idée de rendre l'ID en lecture seule, car l'identité d'un objet ne devrait pas changer au cours de sa durée de vie. À moins bien sûr que votre framework de test ou ORM ne nécessite qu'il soit modifiable.
CodesInChaos
2
Pourrions-nous appeler cela une "jointure exclue gauche (ou droite)" selon ce diagramme?
The Red Pea

Réponses:

912

Cela peut être résolu à l'aide de l'expression LINQ suivante:

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

Une autre façon d'exprimer cela via LINQ, que certains développeurs trouvent plus lisible:

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

Avertissement: Comme indiqué dans les commentaires, ces approches nécessitent une opération O (n * m) . Cela peut être bien, mais pourrait introduire des problèmes de performances, et surtout si l'ensemble de données est assez volumineux. Si cela ne répond pas à vos exigences de performances, vous devrez peut-être évaluer d'autres options. Étant donné que l'exigence indiquée concerne une solution dans LINQ, ces options ne sont cependant pas explorées ici. Comme toujours, évaluez toute approche par rapport aux exigences de performance de votre projet.

Klaus Byskov Pedersen
la source
34
Vous savez que c'est une solution O (n * m) à un problème qui peut être facilement résolu en temps O (n + m)?
Niki
32
@nikie, l'OP a demandé une solution utilisant Linq. Peut-être qu'il essaie d'apprendre Linq. Si la question avait été pour le moyen le plus efficace, ma question n'aurait pas nécessairement été la même.
Klaus Byskov Pedersen
46
@nikie, vous souhaitez partager votre solution facile?
Rubio
18
C'est équivalent et je trouve plus facile à suivre: var result = peopleList2.Where (p => peopleList1.All (p2 => p2.ID! = P.ID));
AntonK
28
@Menol - il pourrait être un peu injuste de critiquer quelqu'un qui répond correctement à une question. Les gens ne devraient pas avoir besoin d'anticiper toutes les manières et tous les contextes que les personnes futures pourraient tomber sur la réponse. En réalité, vous devriez diriger cela vers Nikie - qui a pris le temps de déclarer qu'il connaissait une alternative sans la proposer.
Chris Rogers
397

Si vous remplacez l'égalité des personnes, vous pouvez également utiliser:

peopleList2.Except(peopleList1)

Exceptdevrait être beaucoup plus rapide que la Where(...Any)variante car elle peut mettre la deuxième liste dans une table de hachage. Where(...Any)a un temps d'exécution de O(peopleList1.Count * peopleList2.Count)tandis que les variantes basées sur HashSet<T>(presque) ont un temps d'exécution de O(peopleList1.Count + peopleList2.Count).

Exceptsupprime implicitement les doublons. Cela ne devrait pas affecter votre cas, mais pourrait être un problème pour des cas similaires.

Ou si vous voulez du code rapide mais ne voulez pas remplacer l'égalité:

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

Cette variante ne supprime pas les doublons.

CodesInChaos
la source
Cela ne fonctionnerait que s'il Equalsavait été remplacé pour comparer les identifiants.
Klaus Byskov Pedersen du
34
C'est pourquoi j'ai écrit que vous devez passer outre l'égalité. Mais j'ai ajouté un exemple qui fonctionne même sans cela.
CodesInChaos
4
Cela fonctionnerait également si Person était un struct. Cependant, Person semble être une classe incomplète car elle possède une propriété appelée "ID" qui ne l'identifie pas - si elle l'identifiait, alors égal serait remplacé pour que ID égal signifie Personne égale. Une fois que ce bogue dans Person est corrigé, cette approche est alors meilleure (à moins que le bogue ne soit corrigé en renommant "ID" en quelque chose qui ne trompe pas en semblant être un identifiant).
Jon Hanna
2
Cela fonctionne également très bien si vous parlez d'une liste de chaînes (ou d'autres objets de base), ce que je cherchais lorsque je suis tombé sur ce fil.
Dan Korn
@DanKorn Même, c'est une solution plus simple, par rapport à où, pour la comparaison de base, int, ref objets, chaînes.
Labyrinthe
73

Ou si vous le voulez sans négation:

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

Fondamentalement, il dit obtenir tout de peopleList2 où tous les identifiants dans peopleList1 sont différents de id dans peuplesList2.

Une approche un peu différente de la réponse acceptée :)

user1271080
la source
5
Cette méthode (liste de plus de 50 000 articles) était nettement plus rapide que la méthode ANY!
DaveN
5
Cela pourrait être plus rapide simplement parce qu'il est paresseux. Notez que cela ne fait pas de vrai travail pour l'instant. Ce n'est qu'après avoir énuméré la liste qu'il fait réellement le travail (en appelant ToList ou en l'utilisant dans le cadre d'une boucle foreach, etc.)
Xtros
32

Étant donné que toutes les solutions à ce jour utilisaient une syntaxe fluide, voici une solution en syntaxe d'expression de requête, pour les personnes intéressées:

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

Je pense qu'elle est suffisamment différente des réponses données pour intéresser certains, même si elle est très probablement sous-optimale pour les listes. Maintenant, pour les tables avec des ID indexés, ce serait certainement la voie à suivre.

Michael Goldshteyn
la source
Je vous remercie. Première réponse qui dérange avec la syntaxe d'expression de requête.
Nom générique
15

Un peu tard pour la fête mais une bonne solution qui est également compatible Linq to SQL est:

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

Félicitations à http://www.dotnet-tricks.com/Tutorial/linq/UXPF181012-SQL-Joins-with-C

Richard Ockerby
la source
12

La réponse de Klaus a été excellente, mais ReSharper vous demandera de "Simplifier l'expression LINQ":

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

Brian T
la source
Il convient de noter que cette astuce ne fonctionnera pas s'il existe plusieurs propriétés liant les deux objets (pensez à la clé composite SQL).
Alrekr
Alrekr - Si ce que vous voulez dire est "vous devrez comparer plus de propriétés si plus de propriétés doivent être comparées", alors je dirais que c'est assez évident.
Lucas Morgan
8

Cette extension énumérable vous permet de définir une liste d'éléments à exclure et une fonction à utiliser pour rechercher la clé à utiliser pour effectuer la comparaison.

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

Vous pouvez l'utiliser de cette façon

list1.Exclude(list2, i => i.ID);
Bertrand
la source
En ayant le code que possède @BrianT, comment pourrais-je le convertir pour utiliser votre code?
Nicke Manarin
0

Voici un exemple de travail qui acquiert des compétences informatiques qu'un candidat à un emploi ne possède pas déjà.

//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
la source
0

tout d'abord, extrayez les identifiants de la collection où condition

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

deuxièmement, utilisez l'estament "compare" pour sélectionner des identifiants différents de la sélection

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

Évidemment, vous pouvez utiliser x.key! = "TEST", mais ce n'est qu'un exemple

Ángel Ibáñez
la source
0

Une fois que vous avez écrit un FuncEqualityComparer générique, vous pouvez l'utiliser partout.

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
la source