Jak zrobiłbyś zapytanie „nie w” z LINQ?

307

Mam dwie kolekcje, które mają właściwości Emailw obu kolekcjach. Muszę uzyskać listę elementów na pierwszej liście, których Emailnie ma na drugiej liście. W przypadku SQL użyłbym po prostu „nie w”, ale nie znam odpowiednika w LINQ. Jak to się robi?

Do tej pory mam połączenie, jak ...

var matches = from item1 in list1
join item2 in list2 on item1.Email equals item2.Email
select new { Email = list1.Email };

Ale nie mogę dołączyć, ponieważ potrzebuję różnicy, a połączenie się nie powiedzie. Potrzebuję sposobu, w jaki wierzę, zawiera lub istnieje. Po prostu nie znalazłem jeszcze takiego przykładu.

Brennan
źródło
3
Należy pamiętać, że odpowiedź Echostorma tworzy kod, który jest o wiele czytelniejszy do odczytania niż odpowiedź Roberta
Nathan Koop

Odpowiedzi:

302

Nie wiem czy to ci pomoże, ale ...

NorthwindDataContext dc = new NorthwindDataContext();    
dc.Log = Console.Out;

var query =    
    from c in dc.Customers    
    where !(from o in dc.Orders    
            select o.CustomerID)    
           .Contains(c.CustomerID)    
    select c;

foreach (var c in query) Console.WriteLine( c );

od klauzuli NOT IN w LINQ do SQL autorstwa Marco Russo

Robert Rouse
źródło
Ale używam linq do encji, więc otrzymuję „tylko pierwotne typy mogą być użyte błąd”. Czy jest coś do obejrzenia ...? oprócz ręcznego iterowania i znajdowania listy.
Nowicjusz
13
Działa to dla mnie dobrze z LINQ to Entities. SQL staje się zapytaniem GDZIE NIE ISTNIEJE (podzapytanie). Może była aktualizacja, która rozwiązała ten problem?
scottheckel
2
Myślę, że nowsze wersje EF obsługują. Zawiera, plus to pytanie nie oznacza EF (wersja) ani LinqToSQL .. więc może zaistnieć potrzeba zawężenia zakresu pytania i odpowiedzi tutaj ..
Brett Caswell
4
@Robert Rouse - Link do The Not in cluse w linq to sql już nie działa. Po prostu fyi.
JonH
Podany link prowadzi do strony oznaczonej jako zawierająca złośliwe oprogramowanie.
mikesigs
334

Chcesz operatora z wyjątkiem.

var answer = list1.Except(list2);

Lepsze wyjaśnienie tutaj: https://docs.microsoft.com/archive/blogs/charlie/linq-farm-more-on-set-operators

UWAGA: Ta technika działa najlepiej tylko w przypadku typów pierwotnych, ponieważ należy zaimplementować IEqualityComparer, aby użyć Exceptmetody ze złożonymi typami.

Echostorm
źródło
7
Korzystanie z wyjątkiem: jeśli pracujesz z listami typów złożonych, musisz zaimplementować IEqualityComparer <MyComlplexType>, co sprawia, że ​​nie jest to takie miłe
sakito,
4
Nie musisz implementować IEqualityComparer <T>, jeśli chcesz po prostu porównać równość odniesienia lub jeśli przesłoniłeś T.Equals () i T.GetHashCode (). Jeśli nie zaimplementujesz IEqualityComparer <T>, zostanie użyty EqualityComparer <T>. Domyślny .
piedar
2
@Echostorm (i inni czytający), jeśli wykonasz obiekt Select to Anonymous, HashCode zostanie określony przez wartości właściwości; list1.Select(item => new { Property1 = item.Property1, Property2 = item.Property2 }).Except(list2.Select( item => new { Property1 = item.Property1, Property2 = item.Property2 }));jest to szczególnie przydatne, gdy określasz równość, oceniając tylko zestaw wartości typu złożonego.
Brett Caswell
3
Właściwie ktoś wskazał poniżej i myślę słusznie, że nie byłoby potrzeby implementować IEquatityComparor<T,T>ani zastępować metod porównywania obiektów w LinqToSqlscenariuszu; ponieważ zapytanie będzie reprezentowane jako / skompilowane do / wyrażone jako SQL; dlatego wartości będą sprawdzane, a nie odwołanie do obiektu.
Brett Caswell
2
Za pomocą exceptI udało mi się przyspieszyć zapytanie LINQ z 8-10 sekund do pół sekundy
Michael Kniskern
61

Dla osób, które zaczynają od grupy obiektów w pamięci i wykonują zapytania do bazy danych, uważam, że to najlepszy sposób:

var itemIds = inMemoryList.Select(x => x.Id).ToArray();
var otherObjects = context.ItemList.Where(x => !itemIds.Contains(x.Id));

To tworzy ładną WHERE ... IN (...)klauzulę w SQL.

StriplingWarrior
źródło
1
tak naprawdę można to zrobić w 3.5
George Silva,
59

elementy na pierwszej liście, na których e-mail nie istnieje na drugiej liście.

from item1 in List1
where !(list2.Any(item2 => item2.Email == item1.Email))
select item1;
Amy B.
źródło
16

Możesz użyć kombinacji Where i Any do znalezienia nie w:

var NotInRecord =list1.Where(p => !list2.Any(p2 => p2.Email  == p.Email));
DevT
źródło
8

Możesz wziąć obie kolekcje na dwóch różnych listach, powiedzmy list1 i list2.

Więc po prostu napisz

list1.RemoveAll(Item => list2.Contains(Item));

To zadziała.

Chintan Udeshi
źródło
3
Fajnie, ale ma efekt uboczny usuwania elementów z listy.
Tarik
7

W przypadku, gdy używa się ADO.NET Entity Framework , rozwiązanie EchoStorm również działa idealnie. Ale zajęło mi to kilka minut, aby owinąć wokół niego głowę. Zakładając, że masz kontekst bazy danych, dc i chcesz znaleźć wiersze w tabeli x niepołączone w tabeli y, pełna odpowiedź brzmi:

var linked =
  from x in dc.X
  from y in dc.Y
  where x.MyProperty == y.MyProperty
  select x;
var notLinked =
  dc.X.Except(linked);

W odpowiedzi na komentarz Andy'ego tak, w zapytaniu LINQ można mieć dwa z nich. Oto kompletny przykład działania z wykorzystaniem list. Każda klasa, Foo i Bar, ma identyfikator. Foo ma odniesienie do Bar za pomocą „klucza obcego” za pośrednictwem Foo.BarId. Program wybiera wszystkie Foo niepowiązane z odpowiednim paskiem.

class Program
{
    static void Main(string[] args)
    {
        // Creates some foos
        List<Foo> fooList = new List<Foo>();
        fooList.Add(new Foo { Id = 1, BarId = 11 });
        fooList.Add(new Foo { Id = 2, BarId = 12 });
        fooList.Add(new Foo { Id = 3, BarId = 13 });
        fooList.Add(new Foo { Id = 4, BarId = 14 });
        fooList.Add(new Foo { Id = 5, BarId = -1 });
        fooList.Add(new Foo { Id = 6, BarId = -1 });
        fooList.Add(new Foo { Id = 7, BarId = -1 });

        // Create some bars
        List<Bar> barList = new List<Bar>();
        barList.Add(new Bar { Id = 11 });
        barList.Add(new Bar { Id = 12 });
        barList.Add(new Bar { Id = 13 });
        barList.Add(new Bar { Id = 14 });
        barList.Add(new Bar { Id = 15 });
        barList.Add(new Bar { Id = 16 });
        barList.Add(new Bar { Id = 17 });

        var linked = from foo in fooList
                     from bar in barList
                     where foo.BarId == bar.Id
                     select foo;
        var notLinked = fooList.Except(linked);
        foreach (Foo item in notLinked)
        {
            Console.WriteLine(
                String.Format(
                "Foo.Id: {0} | Bar.Id: {1}",
                item.Id, item.BarId));
        }
        Console.WriteLine("Any key to continue...");
        Console.ReadKey();
    }
}

class Foo
{
    public int Id { get; set; }
    public int BarId { get; set; }
}

class Bar
{
    public int Id { get; set; }
}
Brett
źródło
czy dwa froms wor w LINQ? to byłoby pomocne.
Andy
Andy: Tak, patrz poprawiona odpowiedź powyżej.
Brett,
4
var secondEmails = (from item in list2
                    select new { Email = item.Email }
                   ).ToList();

var matches = from item in list1
              where !secondEmails.Contains(item.Email)
              select new {Email = item.Email};
tvanfosson
źródło
4

Można również użyć All()

var notInList = list1.Where(p => list2.All(p2 => p2.Email != p.Email));
Janis S.
źródło
2

Chociaż Exceptjest częścią odpowiedzi, to nie jest cała odpowiedź. Domyślnie Except(podobnie jak kilku operatorów LINQ) dokonuje porównania referencji dla typów referencji. Aby porównać według wartości w obiektach, musisz

  • zaimplementować IEquatable<T>w swoim typie lub
  • zastąp Equalsi GetHashCodew swoim typie, lub
  • przekazać instancję typu implementującą IEqualityComparer<T>dla danego typu
Ryan Lundy
źródło
2
... jeśli mówimy o LINQ do Objects. Jeśli było to LINQ na SQL, zapytanie jest tłumaczone na instrukcje SQL uruchamiane w bazie danych, więc nie ma to zastosowania.
Lucas,
1

Przykład użycia listy int dla uproszczenia.

List<int> list1 = new List<int>();
// fill data
List<int> list2 = new List<int>();
// fill data

var results = from i in list1
              where !list2.Contains(i)
              select i;

foreach (var result in results)
    Console.WriteLine(result.ToString());
Inisheer
źródło
1

Dla każdego, kto chce również używać INoperatora podobnego do SQL w C #, pobierz ten pakiet:

Mshwf.NiceLinq

Ma Ini NotInmetody:

var result = list1.In(x => x.Email, list2.Select(z => z.Email));

Nawet ty możesz tego użyć w ten sposób

var result = list1.In(x => x.Email, "[email protected]", "[email protected]", "[email protected]");
mshwf
źródło
0

Dziękuję, Brett. Twoja sugestia też mi pomogła. Miałem listę obiektów i chciałem ją przefiltrować za pomocą innej listy obiektów. Dzięki jeszcze raz....

Jeśli ktoś potrzebuje, spójrz na mój przykładowy kod:

'First, get all the items present in the local branch database
Dim _AllItems As List(Of LocalItem) = getAllItemsAtBranch(BranchId, RecordState.All)

'Then get the Item Mappings Present for the branch
Dim _adpt As New gItem_BranchesTableAdapter
Dim dt As New ds_CA_HO.gItem_BranchesDataTable
    _adpt.FillBranchMappings(dt, BranchId)

Dim _MappedItems As List(Of LocalItem) = (From _item As LocalItem In _AllItems Join _
    dr As ds_CA_HO.gItem_BranchesRow In dt _
    On _item.Id Equals dr.numItemID _
    Select _item).ToList

_AllItems = _AllItems.Except(_MappedItems.AsEnumerable).ToList

 Return _AllItems
mangeshkt
źródło
0

Nie testowałem tego za pomocą LINQ dla podmiotów :

NorthwindDataContext dc = new NorthwindDataContext();    
dc.Log = Console.Out;

var query =    
    from c in dc.Customers 
    where !dc.Orders.Any(o => o.CustomerID == c.CustomerID)   
    select c;

Alternatywnie:

NorthwindDataContext dc = new NorthwindDataContext();    
dc.Log = Console.Out;

var query =    
    from c in dc.Customers 
    where dc.Orders.All(o => o.CustomerID != c.CustomerID)   
    select c;

foreach (var c in query) 
    Console.WriteLine( c );
Tarik
źródło
0

Czy nie możesz wykonać zewnętrznego połączenia, wybierając elementy z pierwszej listy tylko wtedy, gdy grupa jest pusta? Coś jak:

Dim result = (From a In list1
              Group Join b In list2 
                  On a.Value Equals b.Value 
                  Into grp = Group
              Where Not grp.Any
              Select a)

Nie jestem pewien, czy działałoby to w jakikolwiek efektywny sposób w ramach Entity.

Marten Jacobs
źródło
0

Alternatywnie możesz to zrobić w następujący sposób:

var result = list1.Where(p => list2.All(x => x.Id != p.Id));
nzrytmn
źródło