Crie uma lista de duas listas de objetos com linq

161

Eu tenho a seguinte situação

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

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

Eu preciso combinar as 2 listas em uma nova List<Person> , caso seja a mesma pessoa que o registro de combinação teria esse nome, valor da pessoa na lista2, a mudança seria o valor da lista2 - o valor da lista1. A mudança é 0 se não houver duplicado

ΩmegaMan
fonte
2
O linq é realmente necessário - um bom foreach com um pouco de expressões linq-ish também poderia ser feito.
Rashack
1
A adição deste comentário como uma versão do título da pergunta e a pergunta real não correspondeu: a resposta real a essa é a resposta de Mike . A maioria das outras respostas, embora úteis, na verdade não resolve o problema apresentado pelo pôster original.
Joshua

Respostas:

254

Isso pode ser feito facilmente usando o método de extensão Linq Union. Por exemplo:

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

Isso retornará uma lista na qual as duas listas são mescladas e as duplas são removidas. Se você não especificar um comparador no método de extensão Union, como no meu exemplo, ele usará os métodos Equals e GetHashCode padrão na sua classe Person. Se, por exemplo, você deseja comparar pessoas, comparando a propriedade Name, você deve substituir esses métodos para realizar a comparação sozinho. Verifique o seguinte exemplo de código para fazer isso. Você deve adicionar esse código à sua classe Person.

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

Se você não quiser definir o método Equals padrão da sua classe Person para sempre usar o Name para comparar dois objetos, também poderá escrever uma classe comparadora que use a interface IEqualityComparer. Você pode fornecer esse comparador como o segundo parâmetro no método Union da extensão Linq. Mais informações sobre como escrever esse método comparador podem ser encontradas em http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

Koen Zomers
fonte
10
Não vejo como isso responde à pergunta sobre a mesclagem de valores.
Wagner da Silva
1
Isso não responde, União irá contém apenas os itens presentes nos dois conjuntos, não qualquer elemento presente em um dos dois lista
J4N
7
@ J4N talvez você está confundindo Unioncom Intersect?
Kos
11
Para referência: há também Concatque não mesclar duplicatas
Kos
7
Você se importaria de editar esta resposta para que ela realmente responda à pergunta? Acho ridículo que uma resposta seja tão bem votada, apesar de não responder à pergunta, apenas porque responde ao título e a uma consulta básica do Google ("linq merge lists").
Rawling
78

Percebi que essa pergunta não foi marcada como respondida após 2 anos - acho que a resposta mais próxima é Richards, mas pode ser bastante simplificada para isso:

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

Embora isso não ocorra erros no caso em que você tem nomes duplicados em qualquer um dos conjuntos.

Algumas outras respostas sugeriram o uso da união - esse definitivamente não é o caminho a seguir, pois você apenas obterá uma lista distinta, sem fazer a combinação.

Mike Goatly
fonte
8
Este post realmente responde à pergunta e faz bem.
philu
3
Essa deve ser a resposta aceita. Nunca vi uma pergunta com tantos votos positivos por respostas que não respondem à pergunta!
Todd Menier
Boa resposta. Posso fazer uma pequena alteração, para que o Valor seja realmente o valor da lista2 e, para que o Change continue, se houver duplicatas: Defina o Valor = p2.Value e Change = p1.Change + p2.Value - p1.Value
Ravi Desai
70

Por que você não usa apenas Concat?

O Concat faz parte do linq e é mais eficiente do que fazer um AddRange()

no seu caso:

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);
J4N
fonte
13
Como você sabe que é mais eficiente?
22413 Jerry Nixon
@Jerry Nixon Ele / ela não testou, mas a explicação parece lógica. stackoverflow.com/questions/1337699/…
Nullius
9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> Comentário 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 é o meu ponto.
J4N
2
E a vantagem também é que, se você usar o Entity Framework, isso poderá ser feito no lado do SQL e não no lado do C #.
J4N
4
A verdadeira razão pela qual isso não ajuda é que na verdade não mescla nenhum dos objetos presentes nas duas listas.
Mike Goatly 23/09/15
15

Este é o Linq

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

Esta é Normaly (AddRange)

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

Esta é Normaly (Foreach)

var mergedList=new List<Person>();

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

Esta é 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
fonte
12

Existem algumas partes para fazer isso, supondo que cada lista não contenha duplicatas, Name seja um identificador exclusivo e nenhuma lista seja ordenada.

Primeiro, crie um método de extensão de acréscimo para obter uma única lista:

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

Assim, pode obter uma única lista:

var oneList = list1.Append(list2);

Em seguida, agrupe no nome

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

Em seguida, pode processar cada grupo com um auxiliar para processar um grupo de cada 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 pode ser aplicado a cada elemento de grouped:

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

(Aviso: não testado.)

Richard
fonte
2
Você Appendé uma duplicata quase exata da pronta entrega Concat.
quer
@Rawling: É, por algum motivo, eu continuava desaparecido Enumerable.Concate, portanto, re-implementando-o.
Richard
2

Você precisa de algo como uma junção externa completa. System.Linq.Enumerable não possui um método que implemente uma associação externa completa; portanto, precisamos fazer isso sozinhos.

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
fonte
2

O código a seguir funciona para o seu problema? Eu usei um foreach com um pouco de linq dentro para fazer a combinação de listas e assumi que as pessoas são iguais se seus nomes corresponderem, e parece imprimir os valores esperados quando executados. O Resharper não oferece sugestões para converter o foreach em linq, portanto, provavelmente é tão bom quanto possível.

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
fonte
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
fonte