Utilizzare LINQ per ottenere elementi in un Elenco <>, che non sono in un altro Elenco <>


526

Suppongo che ci sia una semplice query LINQ per farlo, non sono proprio sicuro di come.

Dato questo codice:

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

Vorrei eseguire una query LINQ per darmi tutte le persone peopleList2che non ci sono peopleList1.

Questo esempio dovrebbe darmi due persone (ID = 4 e ID = 5)


3
Forse è una buona idea creare un ID in sola lettura poiché l'identità di un oggetto non dovrebbe cambiare nel tempo reale. A meno che, naturalmente, il framework di test o ORM non richieda che sia mutabile.
CodesInChaos,

2
Potremmo chiamarlo un "Join Escluso Sinistra (o Destra)" secondo questo diagramma?
The Red Pea,

Risposte:


912

Questo può essere risolto usando la seguente espressione LINQ:

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

Un modo alternativo di esprimere questo tramite LINQ, che alcuni sviluppatori trovano più leggibili:

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

Avvertenza: come notato nei commenti, questi approcci richiedono un'operazione O (n * m) . Questo potrebbe andare bene, ma potrebbe introdurre problemi di prestazioni, soprattutto se il set di dati è piuttosto grande. Se ciò non soddisfa i requisiti di prestazione, potrebbe essere necessario valutare altre opzioni. Poiché il requisito dichiarato è per una soluzione in LINQ, tuttavia, tali opzioni non vengono esplorate qui. Come sempre, valuta qualsiasi approccio rispetto ai requisiti di prestazione che il tuo progetto potrebbe avere.


34
Sei consapevole che questa è una soluzione O (n * m) a un problema che può essere facilmente risolto in tempo O (n + m)?
Niki,

32
@nikie, l'OP ha chiesto una soluzione che utilizza Linq. Forse sta cercando di imparare Linq. Se la domanda fosse stata per il modo più efficiente, la mia domanda non sarebbe stata necessariamente la stessa.
Klaus Byskov Pedersen,

46
@nikie, ti interessa condividere la tua soluzione semplice?
Rubio

18
Questo è equivalente e trovo più facile da seguire: var result = peopleList2.Where (p => peopleList1.All (p2 => p2.ID! = P.ID));
AntonK,

28
@Menol - potrebbe essere un po 'ingiusto criticare qualcuno che risponde correttamente a una domanda. Le persone non dovrebbero aver bisogno di anticipare tutti i modi e i contesti in cui le persone future potrebbero imbattersi nella risposta. In realtà, dovresti indirizzarlo al nikie, che si è preso il tempo di affermare di conoscere un'alternativa senza fornirla.
Chris Rogers,

397

Se si ignora l'uguaglianza delle persone, è possibile utilizzare anche:

peopleList2.Except(peopleList1)

Exceptdovrebbe essere significativamente più veloce della Where(...Any)variante poiché può mettere il secondo elenco in una tabella hash. Where(...Any)ha un runtime di O(peopleList1.Count * peopleList2.Count)mentre le varianti basate su HashSet<T>(quasi) hanno un runtime di O(peopleList1.Count + peopleList2.Count).

Exceptrimuove implicitamente i duplicati. Ciò non dovrebbe influire sul tuo caso, ma potrebbe essere un problema per casi simili.

O se vuoi un codice veloce ma non vuoi sovrascrivere l'uguaglianza:

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

Questa variante non rimuove i duplicati.


Funzionerebbe solo se Equalsfosse stato sovrascritto per confrontare gli ID.
Klaus Byskov Pedersen,

34
Ecco perché ho scritto che devi scavalcare l'uguaglianza. Ma ho aggiunto un esempio che funziona anche senza quello.
CodesInChaos,

4
Funzionerebbe anche se Person fosse una struttura. Tuttavia, Person sembra una classe incompleta in quanto ha una proprietà chiamata "ID" che non lo identifica - se lo identificasse, allora gli uguali verrebbero sovrascritti in modo tale che l'identificativo uguale significasse Person uguale. Una volta risolto quel bug in Person, questo approccio è quindi migliore (a meno che il bug non sia risolto rinominando "ID" in qualcos'altro che non induce in errore sembrando essere un identificatore).
Jon Hanna,

2
Funziona benissimo anche se stai parlando di un elenco di stringhe (o altri oggetti di base), che era quello che stavo cercando quando mi sono imbattuto in questo thread.
Dan Korn,

@DanKorn Lo stesso, questa è una soluzione più semplice, rispetto a dove, per un confronto di base, int, ref degli oggetti, stringhe.
Maze,

73

O se lo vuoi senza negazione:

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

Fondamentalmente dice di ottenere tutto da peopleList2 dove tutti gli id ​​in peopleList1 sono diversi da id in peoplesList2.

Solo un approccio leggermente diverso dalla risposta accettata :)


5
Questo metodo (elenco di oltre 50.000 articoli) è stato significativamente più veloce del QUALSIASI metodo!
DaveN,

5
Questo potrebbe essere più veloce solo perché è pigro. Nota che questo non sta ancora facendo un vero lavoro. Non è fino a quando non si elenca l'elenco che effettivamente fa il lavoro (chiamando ToList o utilizzandolo come parte di un ciclo foreach, ecc.)
Xtros,

32

Poiché tutte le soluzioni finora utilizzate hanno utilizzato una sintassi fluida, ecco una soluzione nella sintassi dell'espressione di query, per chi è interessato:

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

Penso che sia abbastanza diverso dalle risposte fornite per essere di interesse per alcuni, anche se molto probabilmente sarebbe non ottimale per gli Elenchi. Ora per le tabelle con ID indicizzati, questa sarebbe sicuramente la strada da percorrere.


Grazie. Prima risposta che infastidisce con la sintassi dell'espressione di query.
Nome generico

15

Un po 'tardi alla festa, ma una buona soluzione che è anche compatibile con 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>();

Complimenti a http://www.dotnet-tricks.com/Tutorial/linq/UXPF181012-SQL-Joins-with-C


12

La risposta di Klaus è stata fantastica, ma ReSharper ti chiederà di "Semplificare l'espressione LINQ":

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


Vale la pena notare che questo trucco non funzionerà se c'è più di una proprietà che lega i due oggetti (pensa alla chiave composita SQL).
Alrekr,

Alrekr - Se ciò che intendi dire è "dovrai confrontare più proprietà se più proprietà devono essere confrontate", direi che è abbastanza ovvio.
Lucas Morgan,

8

Questa estensione enumerabile consente di definire un elenco di elementi da escludere e una funzione da utilizzare per trovare la chiave da utilizzare per eseguire il confronto.

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

Puoi usarlo in questo modo

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

Avendo il codice che ha @BrianT, come potrei convertirlo per usare il tuo codice?
Nicke Manarin,

0

Ecco un esempio funzionante che acquisisce competenze IT che un candidato non ha già.

//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

in primo luogo, estrarre gli id ​​dalla raccolta in cui condizione

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

in secondo luogo, utilizzare l'estensione "confronta" per selezionare gli ID diversi dalla selezione

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

Ovviamente puoi usare x.key! = "TEST", ma è solo un esempio


0

Dopo aver scritto un FuncEqualityComparer generico, puoi usarlo ovunque.

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);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.