2009-10-30 10 views
23

Deseo encontrar todos los elementos en una colección que no coincidan con otra colección. Las colecciones no son del mismo tipo, sin embargo; Quiero escribir una expresión lambda para especificar la igualdad.Uso de LINQ to Objects para buscar elementos en una colección que no coincidan con

Un LINQPad ejemplo de lo que estoy tratando de hacer:

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

Los trabajos anteriores, y volverá Bill Empleado (# 10). Sin embargo, no parece elegante, y puede ser ineficiente con colecciones más grandes. En SQL, probablemente haría una unión IZQUIERDA y encontrar elementos donde la segunda identificación era NULL. ¿Cuál es la mejor práctica para hacer esto en LINQ?

EDITAR: Se actualizó para evitar que las soluciones que dependen de Id igualen el índice.

EDITAR: Agregó la solución de cdonner - ¿Alguien tiene algo más simple?

EDITAR: Agregué una variante a la respuesta de Richard Hein, mi favorito actual. Gracias a todos por unas excelentes respuestas!

Respuesta

30

Esto es casi lo mismo que algunos otros ejemplos, pero menos de código:

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

No es nada más sencillo que employees.Where (e => managers.Any (m! => m.EmployeeId == e.Id)) o su sintaxis original, sin embargo.

+0

En realidad, me gusta más que las otras soluciones, su significado es más claro. Reescribí la combinación en la sintaxis de consulta (ver el código de muestra revisado en mi pregunta) por preferencia personal. ¡Gracias! – TrueWill

+0

cuando se trata de una gran colección, excepto que es demasiado lenta. la respuesta de unión es la mejor. –

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

No recuerdo dónde encontré este método.

+0

+1 - Niza. Modifiqué esto ligeramente y lo incorporé en mi pregunta. Sin embargo, quiero ver lo que otros piensan. ¡Gracias! – TrueWill

2

Eche un vistazo a la función Except() LINQ. Hace exactamente lo que necesita.

+0

La función except solo funciona con 2 conjuntos del mismo tipo de objeto, pero no se aplicará directamente a su ejemplo con empleados y gerentes. Por lo tanto, el método sobrecargado en mi respuesta. – cdonner

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

No hay garantía de que EmployeeId coincida con el índice de empleado en la matriz ... –

+0

Buena idea: no pensé en seleccionar los ID para que Excepto con el comparador de igualdad predeterminado comparara los enteros. Sin embargo, el Sr. Levesque tiene razón, y he actualizado el ejemplo para reflejar esto. ¿Puedes dar un ejemplo que devuelva correctamente a los empleados? – TrueWill

+0

Ah tienes razón. La respuesta ha sido actualizada. –

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. Elegante y funciona correctamente. – TrueWill

+1

Gracias. Originalmente lo encontré aquí: http://rsanidad.wordpress.com/2007/10/16/linq-except-and-intersect/ –

3

Es un poco tarde (lo sé).

Estaba viendo el mismo problema, y ​​estaba considerando un HashSet debido a varias pistas de rendimiento en esa dirección inc. @ Skeet de Intersection of multiple lists with IEnumerable.Intersect() - y pidió a alrededor de mi oficina y el consenso fue que un HashSet sería más rápido y más fácil de leer:

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

Entonces me ofrecieron una solución aún más rápido utilizando matrices nativas para crear una solución de tipo bit de máscara-ish (la sintaxis en las consultas de la matriz nativa me impediría usarlas, excepto por razones de rendimiento extremo).

Para dar esta respuesta un poco crédito después de un tiempo tremendamente largo que he ampliado su programa LINQPad y datos con períodos de tiempo para que pueda comparar lo que ahora son seis opciones:

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

¡Datos agradables! ¡Gracias! – TrueWill

+0

Si los ID de sus empleados y gerentes son muy altos, por ejemplo, en los 100.000, sus soluciones de matriz dispersa van a barf formalmente. No hay nada que diga que las identificaciones no pueden ser tan altas, son ints, y creo que es mejor escribir código que no tenga casos de bordes extraños como ese. – ErikE

+0

@ErikE No estoy seguro de a lo que conduces. El OP proporcionó los datos como parte de la pregunta y he sincronizado 6 formas alternativas de procesar esos datos. Si los datos son diferentes, una opción diferente puede estar más optimizada. ¿Hay alguna respuesta que funcione mejor con todos los conjuntos de datos imaginables? Si hay realmente lo apreciaría si lo tendiste para poder usarlo en el futuro. – amelvin

1

Es mejor si se deja unirse el artículo y el filtro con condición nula

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

¡Los gerentes también son empleados! Por lo tanto, la clase Manager debe ser una subclase de la clase Employee (o, si no le gusta eso, entonces deben tanto la subclase de una clase principal, como la clase NonManager).

entonces su problema es tan simple como la implementación de la interfaz IEquatable en su Employee superclase (por GetHashCode simplemente devolver el EmployeeID) y luego usar este código:

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

Buenos puntos; este fue solo un ejemplo desinfectado sin embargo. El problema general de encontrar no coincidencias es bueno para resolverlo. – TrueWill

+0

¡Sin embargo, esta podría ser una buena solución para muchos problemas generales! Si dos objetos diferentes se pueden combinar de alguna manera, entonces es posible que compartan una relación que podría expresarse a través de una superclase/subclase. En este caso, un gerente tiene una relación "is-a" con un empleado, por lo que tiene mucho sentido usar la herencia. Es menos probable que las relaciones "Has-a" sean susceptibles a mi solución sugerida (pero eso no es necesariamente así, ya que los ciclos de vida y los roles pueden ser difíciles de modelar correctamente y los desarrolladores pueden perder las relaciones "es-a" a veces). – ErikE

Cuestiones relacionadas