Użyj LINQ, aby uzyskać elementy z jednej listy <>, które nie znajdują się na innej liście <>

525

Zakładam, że jest to proste zapytanie LINQ, ale nie jestem pewien, jak to zrobić.

Biorąc pod uwagę ten fragment kodu:

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

Chciałbym wykonać zapytanie LINQ, aby dać mi wszystkie osoby peopleList2, których nie ma peopleList1.

Ten przykład powinien dać mi dwie osoby (ID = 4 i ID = 5)

JSprang
źródło
3
Być może dobrym pomysłem jest wykonanie ID tylko do odczytu, ponieważ tożsamość obiektu nie powinna zmieniać się w czasie jego działania. O ile oczywiście środowisko testowania lub ORM nie wymaga modyfikacji.
CodesInChaos
2
Czy możemy to nazwać „lewą (lub prawą) bez połączenia” zgodnie z tym schematem?
The Red Pea,

Odpowiedzi:

911

Można to rozwiązać za pomocą następującego wyrażenia LINQ:

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

Alternatywny sposób wyrażenia tego za pomocą LINQ, który niektórzy programiści uważają za bardziej czytelny:

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

Ostrzeżenie: Jak zauważono w komentarzach, te podejścia wymagają operacji O (n * m) . Może to być w porządku, ale może powodować problemy z wydajnością, szczególnie jeśli zestaw danych jest dość duży. Jeśli to nie spełnia wymagań dotyczących wydajności, być może trzeba będzie ocenić inne opcje. Ponieważ podane wymaganie dotyczy rozwiązania w LINQ, te opcje nie są tutaj omawiane. Jak zawsze oceń każde podejście pod kątem wymagań wydajnościowych, jakie może mieć Twój projekt.

Klaus Byskov Pedersen
źródło
34
Czy zdajesz sobie sprawę, że jest to rozwiązanie problemu O (n * m), które można łatwo rozwiązać w czasie O (n + m)?
Niki
32
@nikie, OP poprosił o rozwiązanie wykorzystujące Linq. Może próbuje nauczyć się Linq. Gdyby pytanie dotyczyło najbardziej efektywnego sposobu, moje pytanie niekoniecznie byłoby takie samo.
Klaus Byskov Pedersen
46
@nikie, chcesz udostępnić swoje łatwe rozwiązanie?
Rubio
18
Jest to równoważne i łatwiejsze do naśladowania: var result = peopleList2.Where (p => peopleList1.All (p2 => p2.ID! = P.ID));
AntonK,
28
@Menol - krytykowanie kogoś, kto poprawnie odpowie na pytanie, może być trochę niesprawiedliwe. Ludzie nie powinni przewidywać wszystkich sposobów i kontekstów, na które przyszli ludzie mogą natknąć się na odpowiedź. W rzeczywistości powinieneś to skierować do nikie - która nie spieszyła się z stwierdzeniem, że zna alternatywę, nie podając jej.
Chris Rogers,
396

Jeśli zastąpisz równość osób, możesz również użyć:

peopleList2.Except(peopleList1)

Exceptpowinien być znacznie szybszy niż Where(...Any)wariant, ponieważ może umieścić drugą listę w tablicy mieszającej. Where(...Any)ma czas działania, O(peopleList1.Count * peopleList2.Count)podczas gdy warianty oparte na HashSet<T>(prawie) mają czas działania O(peopleList1.Count + peopleList2.Count).

Exceptniejawnie usuwa duplikaty. Nie powinno to mieć wpływu na twoją sprawę, ale może być problemem w podobnych przypadkach.

Lub jeśli chcesz szybkiego kodu, ale nie chcesz nadpisywać równości:

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

Ten wariant nie usuwa duplikatów.

CodesInChaos
źródło
To działałoby tylko, gdyby Equalszostało zastąpione w celu porównania identyfikatorów.
Klaus Byskov Pedersen
34
Dlatego napisałem, że musisz przełamać równość. Ale dodałem przykład, który działa nawet bez tego.
CodesInChaos
4
Działałoby również, gdyby Osoba była strukturą. W tej chwili Osoba wydaje się niekompletną klasą, ponieważ ma właściwość o nazwie „ID”, która go nie identyfikuje - gdyby ją zidentyfikowała, wówczas równość byłaby nadpisana, aby równy identyfikator oznaczał równą osobę. Po naprawieniu tego błędu w Osobie podejście to jest lepsze (chyba że błąd został naprawiony przez zmianę nazwy „ID” na coś innego, co nie wprowadza w błąd pozornym identyfikatorem).
Jon Hanna
2
Działa również świetnie, jeśli mówisz o liście ciągów (lub innych obiektów podstawowych), czego szukałem, gdy natknąłem się na ten wątek.
Dan Korn
@ DanKorn To samo, to prostsze rozwiązanie, w porównaniu do tego, gdzie dla podstawowego porównania int, obiekty ref, ciągi.
Maze
73

Lub jeśli chcesz to bez negacji:

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

Zasadniczo mówi get get from peopleList2, gdzie wszystkie identyfikatory w peopleList1 różnią się od identyfikatora w peoplesList2.

Tylko trochę inne podejście od przyjętej odpowiedzi :)

użytkownik1271080
źródło
5
Ta metoda (lista ponad 50 000 pozycji) była znacznie szybsza niż DOWOLNA metoda!
DaveN
5
Może to być szybsze tylko dlatego, że jest leniwy. Zauważ, że nie wykonuje to jeszcze żadnej prawdziwej pracy. Dopiero po wyliczeniu listy faktycznie działa (przez wywołanie ToList lub użycie jej jako części pętli foreach itp.)
Xtros
32

Ponieważ wszystkie dotychczasowe rozwiązania wykorzystywały płynną składnię, oto rozwiązanie w składni wyrażeń zapytań dla zainteresowanych:

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

Myślę, że jest wystarczająco różny od udzielonych odpowiedzi, aby zainteresować niektórych, nawet sądziłem, że prawdopodobnie nie będzie optymalny dla List. Teraz, jeśli chodzi o tabele z indeksowanymi identyfikatorami, zdecydowanie byłby to właściwy sposób.

Michael Goldshteyn
źródło
Dziękuję Ci. Pierwsza odpowiedź, która przeszkadza w składni wyrażenia zapytania.
Nazwa ogólna
15

Trochę za późno na imprezę, ale dobrym rozwiązaniem, które jest również zgodne z Linq na SQL, jest:

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

Wyrazy uznania dla http://www.dotnet-tricks.com/Tutorial/linq/UXPF181012-SQL-Joins-with-C

Richard Ockerby
źródło
12

Odpowiedź Klausa była świetna, ale ReSharper poprosi cię o „Uprość wyrażenie LINQ”:

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

Brian T.
źródło
Warto zauważyć, że ta sztuczka nie zadziała, jeśli istnieje więcej niż jedna właściwość wiążąca dwa obiekty (pomyśl klucz złożony SQL).
Alrekr
Alrekr - Jeśli masz na myśli to, że „musisz porównać więcej właściwości, jeśli więcej właściwości wymaga porównania”, to powiedziałbym, że to dość oczywiste.
Lucas Morgan
8

To Enumerable Extension pozwala zdefiniować listę elementów do wykluczenia oraz funkcję, która ma zostać użyta do znalezienia klucza do wykonania porównania.

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

Możesz użyć tego w ten sposób

list1.Exclude(list2, i => i.ID);
Bertrand
źródło
Posiadając kod, który ma @BrianT, jak mogę go przekonwertować, aby używał twojego kodu?
Nicke Manarin
0

Oto działający przykład, który pozwala zdobyć umiejętności informatyczne, których kandydat do pracy jeszcze nie posiada.

//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
źródło
0

najpierw wyodrębnij identyfikatory z kolekcji, w której warunek

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

po drugie, użyj wartości „porównaj”, aby wybrać identyfikatory różne od wyboru

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

Oczywiście możesz użyć x.key! = "TEST", ale to tylko przykład

Ángel Ibáñez
źródło
0

Po napisaniu ogólnego FuncEqualityComparer możesz go używać wszędzie.

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
źródło