Cree una lista de dos listas de objetos con linq

161

Tengo la siguiente situación

class Person
{
    string Name;
    int Value;
    int Change;
}

List<Person> list1;
List<Person> list2;

Necesito combinar las 2 listas en una nueva List<Person> en caso de que sea la misma persona que el registro combinado tendría ese nombre, el valor de la persona en list2, el cambio sería el valor de list2, el valor de list1. El cambio es 0 si no hay duplicado

ΩmegaMan
fuente
2
¿Es realmente necesario linq? Un buen foreach con un poco de expresiones linq-ish también podría funcionar.
Rashack
1
Agregar este comentario como una versión del título de la pregunta y la pregunta real no coincidía: la respuesta real es la respuesta de Mike . La mayoría de las otras respuestas, si bien son útiles, en realidad no resuelven el problema presentado por el póster original.
Joshua

Respuestas:

254

Esto se puede hacer fácilmente utilizando el método de extensión Linq Union. Por ejemplo:

var mergedList = list1.Union(list2).ToList();

Esto devolverá una Lista en la que se fusionan las dos listas y se eliminan los dobles. Si no especifica un comparador en el método de extensión Union como en mi ejemplo, utilizará los métodos predeterminados Equals y GetHashCode en su clase Person. Si, por ejemplo, desea comparar personas comparando su propiedad Nombre, debe anular estos métodos para realizar la comparación usted mismo. Verifique el siguiente ejemplo de código para lograr eso. Debe agregar este código a su clase de Persona.

/// <summary>
/// Checks if the provided object is equal to the current Person
/// </summary>
/// <param name="obj">Object to compare to the current Person</param>
/// <returns>True if equal, false if not</returns>
public override bool Equals(object obj)
{        
    // Try to cast the object to compare to to be a Person
    var person = obj as Person;

    return Equals(person);
}

/// <summary>
/// Returns an identifier for this instance
/// </summary>
public override int GetHashCode()
{
    return Name.GetHashCode();
}

/// <summary>
/// Checks if the provided Person is equal to the current Person
/// </summary>
/// <param name="personToCompareTo">Person to compare to the current person</param>
/// <returns>True if equal, false if not</returns>
public bool Equals(Person personToCompareTo)
{
    // Check if person is being compared to a non person. In that case always return false.
    if (personToCompareTo == null) return false;

    // If the person to compare to does not have a Name assigned yet, we can't define if it's the same. Return false.
    if (string.IsNullOrEmpty(personToCompareTo.Name) return false;

    // Check if both person objects contain the same Name. In that case they're assumed equal.
    return Name.Equals(personToCompareTo.Name);
}

Si no desea establecer el método Equals predeterminado de su clase Persona para usar siempre el Nombre para comparar dos objetos, también puede escribir una clase comparadora que use la interfaz IEqualityComparer. Luego puede proporcionar este comparador como el segundo parámetro en el método de Unión de extensión de Linq. Puede encontrar más información sobre cómo escribir un método de comparación en http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

Koen Zomers
fuente
10
No veo cómo esto responde a la pregunta sobre la fusión de valores.
Wagner da Silva
1
Esto no responde, Union contendrá solo elementos presentes en los dos conjuntos, no ningún elemento presente en una de las dos listas
J4N
77
@ J4N estás quizá confundiendo Unioncon Intersect?
Kos
11
Como referencia: también hay Concatque no combina duplicados
Kos
77
¿Te importaría editar esta respuesta para que realmente responda la pregunta? Me parece ridículo que una respuesta sea tan votada a pesar del hecho de que no responde a la pregunta, solo porque responde el título y una consulta básica de Google ("listas de combinación de linq").
Rawling
78

Noté que esta pregunta no se marcó como respondida después de 2 años; creo que la respuesta más cercana es Richards, pero se puede simplificar bastante a esto:

list1.Concat(list2)
    .ToLookup(p => p.Name)
    .Select(g => g.Aggregate((p1, p2) => new Person 
    {
        Name = p1.Name,
        Value = p1.Value, 
        Change = p2.Value - p1.Value 
    }));

Aunque esto no fallará en el caso de que tenga nombres duplicados en cualquier conjunto.

Algunas otras respuestas han sugerido el uso de la unión; definitivamente, este no es el camino a seguir, ya que solo obtendrá una lista distinta, sin hacer la combinación.

Mike Goatly
fuente
8
Esta publicación en realidad responde a la pregunta, y lo hace bien.
philu
3
Esta debería ser la respuesta aceptada. ¡Nunca he visto una pregunta con tantos votos a favor para respuestas que no respondan a la pregunta que se hace!
Todd Menier
Buena respuesta. Podría hacer un pequeño cambio, por lo que el Valor es en realidad el valor de list2, y para que el Cambio se mantenga si tiene duplicados: Establecer Valor = p2.Value y Change = p1.Change + p2.Value - p1.Value
Ravi Desai
70

¿Por qué no solo usas Concat?

Concat es parte de linq y es más eficiente que hacer un AddRange()

en tu caso:

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);
J4N
fuente
13
¿Cómo sabes que es más eficiente?
Jerry Nixon
@Jerry Nixon Él / ella no lo probó, pero la explicación parece lógica. stackoverflow.com/questions/1337699/…
Nullius
9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> Comentario de Greg: Actually, due to deferred execution, using Concat would likely be faster because it avoids object allocation - Concat doesn't copy anything, it just creates links between the lists so when enumerating and you reach the end of one it transparently takes you to the start of the next! este es mi punto.
J4N
2
Y la ventaja también es que si usa Entity Framework, esto se puede hacer en el lado de SQL en lugar del lado de C #.
J4N
44
La verdadera razón por la que esto no ayuda es porque en realidad no fusiona ninguno de los objetos presentes en ambas listas.
Mike Goatly
15

Este es linq

var mergedList = list1.Union(list2).ToList();

Esto es Normaly (AddRange)

var mergedList=new List<Person>();
mergeList.AddRange(list1);
mergeList.AddRange(list2);

Esta es Normaly (Foreach)

var mergedList=new List<Person>();

foreach(var item in list1)
{
    mergedList.Add(item);
}
foreach(var item in list2)
{
     mergedList.Add(item);
}

Esta es Normaly (Foreach-Dublice)

var mergedList=new List<Person>();

foreach(var item in list1)
{
    mergedList.Add(item);
}
foreach(var item in list2)
{
   if(!mergedList.Contains(item))
   {
     mergedList.Add(item);
   }
}
Alper Şaldırak
fuente
12

Hay algunas piezas para hacer esto, suponiendo que cada lista no contenga duplicados, Nombre es un identificador único y ninguna de las listas está ordenada.

Primero cree un método de extensión anexa para obtener una lista única:

static class Ext {
  public static IEnumerable<T> Append(this IEnumerable<T> source,
                                      IEnumerable<T> second) {
    foreach (T t in source) { yield return t; }
    foreach (T t in second) { yield return t; }
  }
}

Por lo tanto, puede obtener una sola lista:

var oneList = list1.Append(list2);

Luego agrupe por nombre

var grouped = oneList.Group(p => p.Name);

Luego puede procesar cada grupo con un ayudante para procesar un grupo a la vez

public Person MergePersonGroup(IGrouping<string, Person> pGroup) {
  var l = pGroup.ToList(); // Avoid multiple enumeration.
  var first = l.First();
  var result = new Person {
    Name = first.Name,
    Value = first.Value
  };
  if (l.Count() == 1) {
    return result;
  } else if (l.Count() == 2) {
    result.Change = first.Value - l.Last().Value;
    return result;
  } else {
    throw new ApplicationException("Too many " + result.Name);
  }
}

Que se puede aplicar a cada elemento de grouped:

var finalResult = grouped.Select(g => MergePersonGroup(g));

(Advertencia: no probado).

Ricardo
fuente
2
Su Appendes un duplicado casi exacto de la lista para usar Concat.
Rawling
@Rawling: Lo es, por alguna razón, seguí desaparecida Enumerable.Concaty, por lo tanto, la volví a implementar.
Richard
2

Necesita algo como una unión externa completa. System.Linq.Enumerable no tiene ningún método que implemente una combinación externa completa, por lo que debemos hacerlo nosotros mismos.

var dict1 = list1.ToDictionary(l1 => l1.Name);
var dict2 = list2.ToDictionary(l2 => l2.Name);
    //get the full list of names.
var names = dict1.Keys.Union(dict2.Keys).ToList();
    //produce results
var result = names
.Select( name =>
{
  Person p1 = dict1.ContainsKey(name) ? dict1[name] : null;
  Person p2 = dict2.ContainsKey(name) ? dict2[name] : null;
      //left only
  if (p2 == null)
  {
    p1.Change = 0;
    return p1;
  }
      //right only
  if (p1 == null)
  {
    p2.Change = 0;
    return p2;
  }
      //both
  p2.Change = p2.Value - p1.Value;
  return p2;
}).ToList();
Amy B
fuente
2

¿El siguiente código funciona para su problema? He usado un foreach con un poco de linq adentro para hacer la combinación de listas y asumí que las personas son iguales si sus nombres coinciden, y parece que imprime los valores esperados cuando se ejecuta. Resharper no ofrece ninguna sugerencia para convertir el foreach en linq, por lo que probablemente sea tan bueno como lo hará de esta manera.

public class Person
{
   public string Name { get; set; }
   public int Value { get; set; }
   public int Change { get; set; }

   public Person(string name, int value)
   {
      Name = name;
      Value = value;
      Change = 0;
   }
}


class Program
{
   static void Main(string[] args)
   {
      List<Person> list1 = new List<Person>
                              {
                                 new Person("a", 1),
                                 new Person("b", 2),
                                 new Person("c", 3),
                                 new Person("d", 4)
                              };
      List<Person> list2 = new List<Person>
                              {
                                 new Person("a", 4),
                                 new Person("b", 5),
                                 new Person("e", 6),
                                 new Person("f", 7)
                              };

      List<Person> list3 = list2.ToList();

      foreach (var person in list1)
      {
         var existingPerson = list3.FirstOrDefault(x => x.Name == person.Name);
         if (existingPerson != null)
         {
            existingPerson.Change = existingPerson.Value - person.Value;
         }
         else
         {
            list3.Add(person);
         }
      }

      foreach (var person in list3)
      {
         Console.WriteLine("{0} {1} {2} ", person.Name,person.Value,person.Change);
      }
      Console.Read();
   }
}
Sean Reid
fuente
1
public void Linq95()
{
    List<Customer> customers = GetCustomerList();
    List<Product> products = GetProductList();

    var customerNames =
        from c in customers
        select c.CompanyName;
    var productNames =
        from p in products
        select p.ProductName;

    var allNames = customerNames.Concat(productNames);

    Console.WriteLine("Customer and product names:");
    foreach (var n in allNames)
    {
        Console.WriteLine(n);
    }
}
pungggi
fuente