2009-10-30 13 views
23

Chcę znaleźć wszystkie elementy w jednej kolekcji, które nie pasują do innej kolekcji. Kolekcje nie są tego samego typu; Chcę napisać wyrażenie lambda, aby określić równość.Używanie LINQ do obiektów do znajdowania elementów w jednej kolekcji, które nie pasują do innej

LINQPad przykładem tego, co próbuję zrobić:

void Main() 
{ 
    var employees = new[] 
    { 
     new Employee { Id = 20, Name = "Bob" }, 
     new Employee { Id = 10, Name = "Bill" }, 
     new Employee { Id = 30, Name = "Frank" } 
    }; 

    var managers = new[] 
    { 
     new Manager { EmployeeId = 20 }, 
     new Manager { EmployeeId = 30 } 
    }; 

    var nonManagers = 
    from employee in employees 
    where !(managers.Any(x => x.EmployeeId == employee.Id)) 
    select employee; 

    nonManagers.Dump(); 

    // Based on cdonner's answer: 

    var nonManagers2 = 
    from employee in employees 
    join manager in managers 
     on employee.Id equals manager.EmployeeId 
    into tempManagers 
    from manager in tempManagers.DefaultIfEmpty() 
    where manager == null 
    select employee; 

    nonManagers2.Dump(); 

    // Based on Richard Hein's answer: 

    var nonManagers3 = 
    employees.Except(
     from employee in employees 
     join manager in managers 
      on employee.Id equals manager.EmployeeId 
     select employee); 

    nonManagers3.Dump(); 
} 

public class Employee 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 

public class Manager 
{ 
    public int EmployeeId { get; set; } 
} 

Powyższe prace i powróci Pracownik Bill (# 10). Nie wygląda jednak elegancko i może być nieskuteczne przy większych kolekcjach. W SQL prawdopodobnie wykonałbym LEFT JOIN i znalazłem przedmioty, w których drugi identyfikator był NULL. Jaka jest najlepsza praktyka w LINQ?

EDYCJA: Zaktualizowano, aby zapobiec rozwiązaniom zależnym od Id równego indeksu.

EDYCJA: Dodano rozwiązanie cdonnera - czy ktoś ma coś prostszego?

EDYCJA: Dodano wariant odpowiedzi Richarda Heina, mojego obecnego faworyta. Dziękuję wszystkim za doskonałe odpowiedzi!

Odpowiedz

30

To jest prawie taki sam, jak kilka innych przykładów, ale mniej kodu:

employees.Except(employees.Join(managers, e => e.Id, m => m.EmployeeId, (e, m) => e)); 

To nie jest prostsze niż employees.Where (e => managers.Any (m! => m.EmployeeId == e.Id)) lub oryginalnej składni.

+0

Właściwie to lubię to lepiej niż inne rozwiązania - uważam, że jego znaczenie jest wyraźniejsze. Przepisałem łączenie w składni zapytania (zobacz poprawiony przykładowy kod w moim pytaniu) z osobistych preferencji. Dziękuję Ci! – TrueWill

+0

, gdy bierze się pod uwagę dużą kolekcję, z wyjątkiem tego, że jest wolna. odpowiedź łączenia jest najlepsza. –

5
/// <summary> 
    /// This method returns items in a set that are not in 
    /// another set of a different type 
    /// </summary> 
    /// <typeparam name="T"></typeparam> 
    /// <typeparam name="TOther"></typeparam> 
    /// <typeparam name="TKey"></typeparam> 
    /// <param name="items"></param> 
    /// <param name="other"></param> 
    /// <param name="getItemKey"></param> 
    /// <param name="getOtherKey"></param> 
    /// <returns></returns> 
    public static IEnumerable<T> Except<T, TOther, TKey>(
              this IEnumerable<T> items, 
              IEnumerable<TOther> other, 
              Func<T, TKey> getItemKey, 
              Func<TOther, TKey> getOtherKey) 
    { 
     return from item in items 
       join otherItem in other on getItemKey(item) 
       equals getOtherKey(otherItem) into tempItems 
       from temp in tempItems.DefaultIfEmpty() 
       where ReferenceEquals(null, temp) || temp.Equals(default(TOther)) 
       select item; 
    } 

Nie pamiętam, gdzie znalazłem tę metodę.

+0

+1 - Nicea. Zmodyfikowałem to nieco i włączyłem to do mojego pytania. Ale chcę zobaczyć, co inni wymyślą. Dzięki! – TrueWill

2

Spójrz na funkcję Except() LINQ. Robi dokładnie to, czego potrzebujesz.

+0

Funkcja except działa tylko z 2 zestawami tego samego typu obiektu, ale nie będzie direclty dotyczyć jego przykładu z pracownikami i menedżerami. Dlatego przeciążona metoda w mojej odpowiedzi. – cdonner

3
var nonmanagers = employees.Select(e => e.Id) 
    .Except(managers.Select(m => m.EmployeeId)) 
    .Select(id => employees.Single(e => e.Id == id)); 
+1

Nie ma gwarancji, że EmployeeId będzie pasować do indeksu pracownika w tablicy ... –

+0

Niezły pomysł - nie myślałem o wyborze identyfikatorów, tak aby wyjątkiem porównania z domyślnym porównywaniem równości porównać liczby całkowite. Pan Levesque ma jednak rację i zaktualizowałem ten przykład, aby to odzwierciedlić. Czy możesz podać przykład, który poprawnie zwraca pracowników? – TrueWill

+0

Ach masz rację. Odpowiedź została zaktualizowana. –

5

 
     var nonManagers = (from e1 in employees 
          select e1).Except(
            from m in managers 
            from e2 in employees 
            where m.EmployeeId == e2.Id 
            select e2); 

+0

+1. Elegancki i działa poprawnie. – TrueWill

+1

Dzięki. Oryginalnie znalazłem to tutaj: http://rsanidad.wordpress.com/2007/10/16/linq-except-and-intersect/ –

3

Trochę się spóźniam (wiem).

Szukałem tego samego problemu i rozważałem HashSet z powodu różnych wskazówek dotyczących wydajności w tym kierunku inc. @ Skeet na Intersection of multiple lists with IEnumerable.Intersect() - i poprosił wokół mojego biura i konsensus było to, że HashSet byłoby szybciej i bardziej czytelne:

HashSet<int> managerIds = new HashSet<int>(managers.Select(x => x.EmployeeId)); 
nonManagers4 = employees.Where(x => !managerIds.Contains(x.Id)).ToList(); 

Potem zaproponowano mi jeszcze szybsze rozwiązanie przy użyciu rodzimych tablice do stworzenia nieco maska-owski rozwiązanie typu (Składnia w macierzystych zapytaniach tablicowych zniechęciłaby mnie do korzystania z nich, z wyjątkiem ekstremalnych przyczyn wydajności).

Aby dać tę odpowiedź za małą wiarę po strasznie długo mam rozszerzonego programu LINQPad i danych z synchronizacją, dzięki czemu można porównać to, co jest teraz sześć opcji:

void Main() 
{ 
    var employees = new[] 
    { 
     new Employee { Id = 20, Name = "Bob" }, 
     new Employee { Id = 10, Name = "Kirk NM" }, 
     new Employee { Id = 48, Name = "Rick NM" }, 
     new Employee { Id = 42, Name = "Dick" }, 
     new Employee { Id = 43, Name = "Harry" }, 
     new Employee { Id = 44, Name = "Joe" }, 
     new Employee { Id = 45, Name = "Steve NM" }, 
     new Employee { Id = 46, Name = "Jim NM" }, 
     new Employee { Id = 30, Name = "Frank"}, 
     new Employee { Id = 47, Name = "Dave NM" }, 
     new Employee { Id = 49, Name = "Alex NM" }, 
     new Employee { Id = 50, Name = "Phil NM" }, 
     new Employee { Id = 51, Name = "Ed NM" }, 
     new Employee { Id = 52, Name = "Ollie NM" }, 
     new Employee { Id = 41, Name = "Bill" }, 
     new Employee { Id = 53, Name = "John NM" }, 
     new Employee { Id = 54, Name = "Simon NM" } 
    }; 

    var managers = new[] 
    { 
     new Manager { EmployeeId = 20 }, 
     new Manager { EmployeeId = 30 }, 
     new Manager { EmployeeId = 41 }, 
     new Manager { EmployeeId = 42 }, 
     new Manager { EmployeeId = 43 }, 
     new Manager { EmployeeId = 44 } 
    }; 

    System.Diagnostics.Stopwatch watch1 = new System.Diagnostics.Stopwatch(); 

    int max = 1000000; 

    watch1.Start(); 
    List<Employee> nonManagers1 = new List<Employee>(); 
    foreach (var item in Enumerable.Range(1,max)) 
    { 
     nonManagers1 = (from employee in employees where !(managers.Any(x => x.EmployeeId == employee.Id)) select employee).ToList(); 

    } 
    nonManagers1.Dump(); 
    watch1.Stop(); 
    Console.WriteLine("Any: " + watch1.ElapsedMilliseconds); 

    watch1.Restart();  
    List<Employee> nonManagers2 = new List<Employee>(); 
    foreach (var item in Enumerable.Range(1,max)) 
    { 
     nonManagers2 = 
     (from employee in employees 
     join manager in managers 
      on employee.Id equals manager.EmployeeId 
     into tempManagers 
     from manager in tempManagers.DefaultIfEmpty() 
     where manager == null 
     select employee).ToList(); 
    } 
    nonManagers2.Dump(); 
    watch1.Stop(); 
    Console.WriteLine("temp table: " + watch1.ElapsedMilliseconds); 

    watch1.Restart();  
    List<Employee> nonManagers3 = new List<Employee>(); 
    foreach (var item in Enumerable.Range(1,max)) 
    { 
     nonManagers3 = employees.Except(employees.Join(managers, e => e.Id, m => m.EmployeeId, (e, m) => e)).ToList(); 
    } 
    nonManagers3.Dump(); 
    watch1.Stop(); 
    Console.WriteLine("Except: " + watch1.ElapsedMilliseconds); 

    watch1.Restart();  
    List<Employee> nonManagers4 = new List<Employee>(); 
    foreach (var item in Enumerable.Range(1,max)) 
    { 
     HashSet<int> managerIds = new HashSet<int>(managers.Select(x => x.EmployeeId)); 
     nonManagers4 = employees.Where(x => !managerIds.Contains(x.Id)).ToList(); 

    } 
    nonManagers4.Dump(); 
    watch1.Stop(); 
    Console.WriteLine("HashSet: " + watch1.ElapsedMilliseconds); 

     watch1.Restart(); 
     List<Employee> nonManagers5 = new List<Employee>(); 
     foreach (var item in Enumerable.Range(1, max)) 
     { 
        bool[] test = new bool[managers.Max(x => x.EmployeeId) + 1]; 
        foreach (var manager in managers) 
        { 
         test[manager.EmployeeId] = true; 
        } 

        nonManagers5 = employees.Where(x => x.Id > test.Length - 1 || !test[x.Id]).ToList(); 


     } 
     nonManagers5.Dump(); 
     watch1.Stop(); 
     Console.WriteLine("Native array call: " + watch1.ElapsedMilliseconds); 

     watch1.Restart(); 
     List<Employee> nonManagers6 = new List<Employee>(); 
     foreach (var item in Enumerable.Range(1, max)) 
     { 
        bool[] test = new bool[managers.Max(x => x.EmployeeId) + 1]; 
        foreach (var manager in managers) 
        { 
         test[manager.EmployeeId] = true; 
        } 

        nonManagers6 = employees.Where(x => x.Id > test.Length - 1 || !test[x.Id]).ToList(); 
     } 
     nonManagers6.Dump(); 
     watch1.Stop(); 
     Console.WriteLine("Native array call 2: " + watch1.ElapsedMilliseconds); 
} 

public class Employee 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
} 

public class Manager 
{ 
    public int EmployeeId { get; set; } 
} 
+0

Ładne dane! Dziękuję Ci! – TrueWill

+0

Jeśli identyfikatory Twoich pracowników i menedżerów są bardzo wysokie, powiedzmy, w 100 000, Twoje rzadkie rozwiązania macierzyńskie będą barfowo barfować. Nic nie wskazuje na to, że identyfikatory nie mogą być tak wysokie - są one ints i myślę, że lepiej napisać kod, który nie ma takich dziwnych przypadków. – ErikE

+0

@EriE Nie jestem pewien, do czego zmierzasz. OP dostarczył dane jako część pytania, a ja wybrałam 6 alternatywnych sposobów przetwarzania tych danych. Jeśli dane byłyby różne, inna opcja może być bardziej zoptymalizowana. Czy istnieje odpowiedź, która najlepiej sprawdzi się przy każdym możliwym zestawie danych? Jeśli jest, to naprawdę bym to docenił, gdybyś go wyłożył, abym mógł go użyć w przyszłości. – amelvin

1

Jest lepiej, jeśli w lewo przyłączyć element i filtr z warunkiem zerowym

var finalcertificates = (from globCert in resultCertificate 
             join toExcludeCert in certificatesToExclude 
              on globCert.CertificateId equals toExcludeCert.CertificateId into certs 
             from toExcludeCert in certs.DefaultIfEmpty() 
             where toExcludeCert == null 
             select globCert).Union(currentCertificate).Distinct().OrderBy(cert => cert.CertificateName); 
0

Menedżerowie to także pracownicy! Zatem klasa Manager powinna być podklasą klasy Employee (lub, jeśli tego nie lubisz, powinny one obie podklasy z klasy nadrzędnej lub utworzyć klasę NonManager).

Wtedy problem jest tak proste, jak wdrożenie interfejsu IEquatable na Employee nadrzędnej (dla GetHashCode prostu zwrócić EmployeeID), a następnie przy użyciu tego kodu:

var nonManagerEmployees = employeeList.Except(managerList); 
+0

Dobre punkty; był to jednak tylko oczyszczony przykład. Ogólny problem ze znalezieniem niezgodności jest dobry do rozwiązania. – TrueWill

+0

To może być dobre rozwiązanie wielu problemów ogólnych! Jeśli dwa różne obiekty mogą być w jakiś sposób scalone, możliwe jest, że dzielą się relacją, która może być wyrażona poprzez nadklasę/podklasę. W takim przypadku menedżer ma relację "jest-a" z pracownikiem, więc używanie dziedziczenia ma sens. "Relacje" Has-a "są mniej prawdopodobne, że będą podatne na moje sugerowane rozwiązanie (ale to niekoniecznie musi się zdarzyć, ponieważ cykle życia i role mogą być trudne do prawidłowego modelowania, a programiści mogą czasem przegapić relacje" is-a "). – ErikE

Powiązane problemy