Verwenden Sie LINQ, um Elemente in einer Liste <> abzurufen, die sich nicht in einer anderen Liste <> befinden

526

Ich würde annehmen, dass es eine einfache LINQ-Abfrage gibt, ich bin mir einfach nicht ganz sicher, wie.

Angesichts dieses Codes:

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

Ich möchte eine LINQ-Abfrage durchführen, um mir alle Personen mitzuteilen peopleList2, die nicht anwesend sind peopleList1.

Dieses Beispiel sollte mir zwei Personen geben (ID = 4 & ID = 5)

JSprang
quelle
3
Vielleicht ist es eine gute Idee, die ID schreibgeschützt zu machen, da sich die Identität eines Objekts während seiner Lebensdauer nicht ändern sollte. Es sei denn natürlich, Ihr Test- oder ORM-Framework erfordert, dass es veränderbar ist.
CodesInChaos
2
Könnten wir dies gemäß diesem Diagramm
Die rote Erbse

Antworten:

912

Dies kann mit dem folgenden LINQ-Ausdruck behoben werden:

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

Eine alternative Möglichkeit, dies über LINQ auszudrücken, die einige Entwickler besser lesbar finden:

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

Warnung: Wie in den Kommentaren erwähnt, erfordern diese Ansätze eine O (n * m) -Operation. Das mag in Ordnung sein, kann aber zu Leistungsproblemen führen, insbesondere wenn der Datensatz ziemlich groß ist. Wenn dies Ihren Leistungsanforderungen nicht entspricht, müssen Sie möglicherweise andere Optionen bewerten. Da die angegebene Anforderung eine Lösung in LINQ ist, werden diese Optionen hier jedoch nicht untersucht. Bewerten Sie wie immer jeden Ansatz anhand der Leistungsanforderungen, die Ihr Projekt möglicherweise hat.

Klaus Byskov Pedersen
quelle
34
Sie wissen, dass dies eine O (n * m) -Lösung für ein Problem ist, das leicht in O (n + m) -Zeit gelöst werden kann?
Niki
32
@nikie, das OP fragte nach einer Lösung, die Linq verwendet. Vielleicht versucht er Linq zu lernen. Wenn die Frage am effizientesten gewesen wäre, wäre meine Frage nicht unbedingt dieselbe gewesen.
Klaus Byskov Pedersen
46
@nikie, möchtest du deine einfache Lösung teilen?
Rubio
18
Dies ist äquivalent und ich finde es einfacher zu folgen: var result = peopleList2.Where (p => peopleList1.All (p2 => p2.ID! = P.ID));
AntonK
28
@Menol - es könnte etwas unfair sein, jemanden zu kritisieren, der auf eine Frage richtig antwortet. Die Menschen sollten nicht alle Wege und Kontexte vorhersehen müssen, über die zukünftige Menschen auf die Antwort stoßen könnten. In Wirklichkeit sollten Sie das an Nikie weiterleiten - der sich die Zeit genommen hat, um zu erklären, dass er von einer Alternative wusste, ohne sie bereitzustellen.
Chris Rogers
396

Wenn Sie die Gleichheit der Menschen überschreiben, können Sie auch Folgendes verwenden:

peopleList2.Except(peopleList1)

Exceptsollte deutlich schneller sein als die Where(...Any)Variante, da sie die zweite Liste in eine Hashtabelle einfügen kann. Where(...Any)hat eine Laufzeit von, O(peopleList1.Count * peopleList2.Count)während Varianten, die auf HashSet<T>(fast) basieren , eine Laufzeit von haben O(peopleList1.Count + peopleList2.Count).

ExceptEntfernt implizit Duplikate. Dies sollte sich nicht auf Ihren Fall auswirken, könnte jedoch in ähnlichen Fällen ein Problem darstellen.

Oder wenn Sie schnellen Code möchten, aber die Gleichheit nicht überschreiben möchten:

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

Diese Variante entfernt keine Duplikate.

CodesInChaos
quelle
Das würde nur funktionieren, wenn Equalses überschrieben worden wäre, um IDs zu vergleichen.
Klaus Byskov Pedersen
34
Deshalb habe ich geschrieben, dass Sie die Gleichheit außer Kraft setzen müssen. Aber ich habe ein Beispiel hinzugefügt, das auch ohne das funktioniert.
CodesInChaos
4
Es würde auch funktionieren, wenn Person eine Struktur wäre. Wie es jedoch ist, scheint Person eine unvollständige Klasse zu sein, da sie eine Eigenschaft namens "ID" hat, die sie nicht identifiziert. Wenn sie identifiziert wird, werden Gleiche gleich überschrieben, sodass gleiche ID gleiche Person bedeutet. Sobald dieser Fehler in Person behoben ist, ist dieser Ansatz besser (es sei denn, der Fehler wird behoben, indem "ID" in etwas anderes umbenannt wird, das nicht irreführend erscheint, indem es als Kennung erscheint).
Jon Hanna
2
Es funktioniert auch hervorragend, wenn Sie über eine Liste von Zeichenfolgen (oder anderen Basisobjekten) sprechen, nach denen ich gesucht habe, als ich auf diesen Thread gestoßen bin.
Dan Korn
@DanKorn Same, dies ist eine einfachere Lösung im Vergleich zu where, für den grundlegenden Vergleich int, object ref, strings.
Labyrinth
73

Oder wenn Sie es ohne Verneinung wollen:

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

Grundsätzlich heißt es, alle von peopleList2 abrufen, wobei sich alle IDs in peopleList1 von den IDs in peopleList2 unterscheiden.

Nur ein bisschen anders als die akzeptierte Antwort :)

user1271080
quelle
5
Diese Methode (Liste von über 50.000 Artikeln) war deutlich schneller als die ANY-Methode!
DaveN
5
Dies könnte schneller sein, nur weil es faul ist. Beachten Sie, dass dies noch keine echte Arbeit leistet. Erst wenn Sie die Liste auflisten, erledigt sie die Arbeit tatsächlich (indem Sie ToList aufrufen oder als Teil einer foreach-Schleife usw. verwenden)
Xtros
32

Da alle bisherigen Lösungen eine flüssige Syntax verwendeten, finden Sie hier eine Lösung für die Syntax von Abfrageausdrücken für Interessierte:

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

Ich denke, es unterscheidet sich genug von den Antworten, die für einige von Interesse sind, obwohl ich dachte, dass es für Listen höchstwahrscheinlich nicht optimal wäre. Für Tabellen mit indizierten IDs wäre dies definitiv der richtige Weg.

Michael Goldshteyn
quelle
Vielen Dank. Erste Antwort, die die Syntax des Abfrageausdrucks stört.
Generischer Name
15

Etwas spät zur Party, aber eine gute Lösung, die auch mit Linq to SQL kompatibel ist, ist:

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

Ein großes Lob an http://www.dotnet-tricks.com/Tutorial/linq/UXPF181012-SQL-Joins-with-C

Richard Ockerby
quelle
12

Klaus 'Antwort war großartig, aber ReSharper wird Sie bitten, "den LINQ-Ausdruck zu vereinfachen":

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

Brian T.
quelle
Es ist zu beachten, dass dieser Trick nicht funktioniert, wenn mehr als eine Eigenschaft die beiden Objekte bindet (denken Sie an den zusammengesetzten SQL-Schlüssel).
Alrekr
Alrekr - Wenn Sie sagen wollen "Sie müssen mehr Eigenschaften vergleichen, wenn mehr Eigenschaften verglichen werden müssen", dann würde ich sagen, dass das ziemlich offensichtlich ist.
Lucas Morgan
8

Mit dieser Enumerable-Erweiterung können Sie eine Liste der auszuschließenden Elemente und eine Funktion zum Suchen des Schlüssels definieren, der zum Durchführen eines Vergleichs verwendet werden soll.

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

Sie können es auf diese Weise verwenden

list1.Exclude(list2, i => i.ID);
Bertrand
quelle
Wie kann ich den Code von @BrianT konvertieren, um Ihren Code zu verwenden?
Nicke Manarin
0

Hier ist ein Arbeitsbeispiel, das IT-Kenntnisse vermittelt, über die ein Bewerber noch nicht verfügt.

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

Extrahieren Sie zunächst IDs aus der Sammlung where-Bedingung

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

Zweitens: Verwenden Sie "Vergleichen", um IDs auszuwählen, die sich von der Auswahl unterscheiden

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

Natürlich können Sie x.key! = "TEST" verwenden, dies ist jedoch nur ein Beispiel

Ángel Ibáñez
quelle
0

Sobald Sie einen generischen FuncEqualityComparer geschrieben haben, können Sie ihn überall verwenden.

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
quelle