Use LINQ para obtener elementos en una Lista <>, que no están en otra Lista <>

526

Supongo que hay una simple consulta LINQ para hacer esto, simplemente no estoy exactamente seguro de cómo.

Dado este pedazo de código:

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

Me gustaría realizar una consulta LINQ para darme a todas las personas peopleList2que no están peopleList1.

Este ejemplo debería darme dos personas (ID = 4 e ID = 5)

JSprang
fuente
3
Tal vez sea una buena idea hacer que la identificación sea de solo lectura, ya que la identidad de un objeto no debería cambiar durante su tiempo de vida. A menos que, por supuesto, su marco de prueba u ORM requiera que sea mutable.
CodesInChaos
2
¿Podríamos llamar a esto una "Izquierda (o Derecha) Excluyendo Unir" de acuerdo con este diagrama?
The Red Pea

Respuestas:

912

Esto puede abordarse utilizando la siguiente expresión LINQ:

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

Una forma alternativa de expresar esto a través de LINQ, que algunos desarrolladores encuentran más legible:

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

Advertencia: Como se señaló en los comentarios, estos enfoques requieren una operación O (n * m) . Eso puede estar bien, pero podría presentar problemas de rendimiento, y especialmente si el conjunto de datos es bastante grande. Si esto no satisface sus requisitos de rendimiento, es posible que deba evaluar otras opciones. Sin embargo, dado que el requisito establecido es una solución en LINQ, esas opciones no se exploran aquí. Como siempre, evalúe cualquier enfoque con respecto a los requisitos de rendimiento que pueda tener su proyecto.

Klaus Byskov Pedersen
fuente
34
¿Sabe que es una solución O (n * m) a un problema que puede resolverse fácilmente en tiempo O (n + m)?
Niki
32
@nikie, el OP solicitó una solución que use Linq. Tal vez está tratando de aprender Linq. Si la pregunta hubiera sido la más eficiente, mi pregunta no habría sido necesariamente la misma.
Klaus Byskov Pedersen
46
@nikie, ¿quieres compartir tu solución fácil?
Rubio
18
Esto es equivalente y me resulta más fácil de seguir: var result = peopleList2.Where (p => peopleList1.All (p2 => p2.ID! = P.ID));
AntonK
28
@Menol: puede ser un poco injusto criticar a alguien que responde correctamente a una pregunta. Las personas no deberían necesitar anticipar todas las formas y contextos en los que las personas futuras podrían tropezar con la respuesta. En realidad, deberías dirigir eso a Nikie, que se tomó el tiempo para decir que conocía una alternativa sin proporcionarla.
Chris Rogers
397

Si anula la igualdad de las personas, también puede usar:

peopleList2.Except(peopleList1)

Exceptdebería ser significativamente más rápido que la Where(...Any)variante ya que puede poner la segunda lista en una tabla hash. Where(...Any)tiene un tiempo de ejecución de O(peopleList1.Count * peopleList2.Count)mientras que las variantes basadas en HashSet<T>(casi) tienen un tiempo de ejecución de O(peopleList1.Count + peopleList2.Count).

Exceptelimina implícitamente los duplicados. Eso no debería afectar su caso, pero podría ser un problema para casos similares.

O si desea un código rápido pero no quiere anular la igualdad:

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

Esta variante no elimina duplicados.

CodesInChaos
fuente
Eso solo funcionaría si se Equalshubiera anulado para comparar las identificaciones.
Klaus Byskov Pedersen
34
Es por eso que escribí que debes anular la igualdad. Pero he agregado un ejemplo que funciona incluso sin eso.
CodesInChaos
44
También funcionaría si Person fuera una estructura. Sin embargo, como parece, la Persona parece una clase incompleta ya que tiene una propiedad llamada "ID" que no la identifica: si lo identificara, entonces igual se anularía para que una ID igual significara una Persona igual. Una vez que se corrige ese error en Persona, este enfoque es mejor (a menos que se corrija el error cambiando el nombre de "ID" a otra cosa que no se confunda al parecer un identificador).
Jon Hanna
2
También funciona muy bien si estás hablando de una lista de cadenas (u otros objetos base), que era lo que estaba buscando cuando encontré este hilo.
Dan Korn
@DanKorn Igual, esta es una solución más simple, en comparación con where, para comparación básica, int, objetos ref, cadenas.
Laberinto
73

O si lo quieres sin negación:

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

Básicamente dice obtener todo de peopleList2 donde todos los identificadores en peopleList1 son diferentes de id en peopleList2.

Solo un enfoque un poco diferente de la respuesta aceptada :)

usuario1271080
fuente
55
¡Este método (lista de más de 50,000 artículos) fue significativamente más rápido que CUALQUIER método!
DaveN
55
Esto podría ser más rápido solo porque es vago. Tenga en cuenta que esto todavía no está haciendo ningún trabajo real. No es hasta que enumera la lista que realmente hace el trabajo (llamando a ToList o usándolo como parte de un bucle foreach, etc.)
Xtros
32

Dado que todas las soluciones hasta la fecha utilizan sintaxis fluida, aquí hay una solución en sintaxis de expresión de consulta, para aquellos interesados:

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

Creo que es lo suficientemente diferente de las respuestas dadas como para ser de interés para algunos, incluso pensé que probablemente sería subóptimo para las listas. Ahora, para las tablas con ID indexados, este sería definitivamente el camino a seguir.

Michael Goldshteyn
fuente
Gracias. Primera respuesta que molesta con la sintaxis de expresión de consulta.
Nombre genérico del
15

Un poco tarde para la fiesta, pero una buena solución que también es compatible con Linq to SQL es:

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

Felicitaciones a http://www.dotnet-tricks.com/Tutorial/linq/UXPF181012-SQL-Joins-with-C

Richard Ockerby
fuente
12

La respuesta de Klaus fue excelente, pero ReSharper le pedirá que "Simplifique la expresión LINQ":

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

Brian T
fuente
Vale la pena señalar que este truco no funcionará si hay más de una propiedad que vincula los dos objetos (piense en la clave compuesta de SQL).
Alrekr
Alrekr: si lo que quiere decir es "necesitará comparar más propiedades si es necesario comparar más propiedades", entonces diría que es bastante obvio.
Lucas Morgan
8

Esta extensión enumerable le permite definir una lista de elementos para excluir y una función para encontrar la clave que se usará para realizar la comparación.

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

Puedes usarlo de esta manera

list1.Exclude(list2, i => i.ID);
Bertrand
fuente
Al tener el código que tiene @BrianT, ¿cómo podría convertirlo para usar su código?
Nicke Manarin
0

Aquí hay un ejemplo de trabajo que obtiene habilidades de TI que un candidato de trabajo no tiene.

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

primero, extraiga los identificadores de la colección donde la condición

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

segundo, use "comparar" para seleccionar los identificadores diferentes a la selección

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

Obviamente puedes usar x.key! = "TEST", pero es solo un ejemplo

Ángel Ibáñez
fuente
0

Una vez que escriba un FuncEqualityComparer genérico, puede usarlo en todas partes.

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
fuente