Créer une liste à partir de deux listes d'objets avec linq

161

J'ai la situation suivante

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

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

Je dois combiner les 2 listes en une nouvelle List<Person> au cas où ce serait la même personne que l'enregistrement de combinaison aurait ce nom, la valeur de la personne dans list2, le changement serait la valeur de list2 - la valeur de list1. Le changement est égal à 0 s'il n'y a pas de doublon

ΩmegaMan
la source
2
Linq est-il vraiment nécessaire - un joli foreach avec un peu d'expressions linq-ish pourrait également le faire.
Rashack
1
L'ajout de ce commentaire en tant que version du titre de la question et la question réelle ne correspond pas: la vraie réponse à cette question est cette réponse de Mike . La plupart des autres réponses, bien qu'utiles, ne résolvent pas réellement le problème présenté par l'affiche originale.
Joshua

Réponses:

254

Cela peut facilement être fait en utilisant la méthode d'extension Linq Union. Par exemple:

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

Cela renverra une liste dans laquelle les deux listes sont fusionnées et les doubles sont supprimés. Si vous ne spécifiez pas de comparateur dans la méthode d'extension Union comme dans mon exemple, il utilisera les méthodes Equals et GetHashCode par défaut dans votre classe Person. Si, par exemple, vous souhaitez comparer des personnes en comparant leur propriété Name, vous devez remplacer ces méthodes pour effectuer la comparaison vous-même. Consultez l'exemple de code suivant pour ce faire. Vous devez ajouter ce code à votre 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);
}

Si vous ne souhaitez pas définir la méthode Equals par défaut de votre classe Person pour toujours utiliser le Name pour comparer deux objets, vous pouvez également écrire une classe de comparateur qui utilise l'interface IEqualityComparer. Vous pouvez ensuite fournir ce comparateur comme deuxième paramètre dans la méthode Union d'extension Linq. Vous trouverez plus d'informations sur l'écriture d'une telle méthode de comparaison sur http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

Koen Zomers
la source
10
Je ne vois pas comment cela répond à la question sur la fusion des valeurs.
Wagner da Silva
1
Cela ne répond pas, Union ne contiendra que les éléments présents dans les deux ensembles, aucun élément présent dans l'une des deux listes
J4N
7
@ J4N vous confondez peut-être Unionavec Intersect?
Kos
11
Pour référence: il y a aussi Concatqui ne fusionne pas les doublons
Kos
7
Pourriez-vous modifier cette réponse pour qu'elle réponde réellement à la question? Je trouve ridicule qu'une réponse soit si votée malgré le fait qu'elle ne réponde pas à la question, simplement parce qu'elle répond au titre et à une requête de base de Google ("linq merge lists").
Rawling
78

J'ai remarqué que cette question n'était pas marquée comme réponse après 2 ans - je pense que la réponse la plus proche est Richards, mais elle peut être beaucoup simplifiée à ceci:

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

Bien que ce ne soit pas une erreur dans le cas où vous avez des noms en double dans l'un ou l'autre ensemble.

Certaines autres réponses ont suggéré d'utiliser l'union - ce n'est certainement pas la voie à suivre car cela ne vous donnera qu'une liste distincte, sans faire la combinaison.

Mike Goatly
la source
8
Cet article répond en fait à la question et le fait bien.
philu
3
Cela devrait être la réponse acceptée. Jamais vu une question avec autant de votes positifs pour des réponses qui ne répondent pas à la question posée!
Todd Menier
Bonne réponse. Je pourrais y apporter un petit changement, donc la valeur est en fait la valeur de list2, et pour que le changement continue si vous avez des doublons: Set Value = p2.Value et Change = p1.Change + p2.Value - p1.Value
Ravi Desai
70

Pourquoi vous n'utilisez pas seulement Concat ?

Concat fait partie de linq et est plus efficace que de faire un AddRange()

dans ton cas:

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);
J4N
la source
13
Comment savez-vous que c'est plus efficace?
Jerry Nixon
@Jerry Nixon Il / elle ne l'a pas testé, mais l'explication semble logique. stackoverflow.com/questions/1337699/…
Nullius
9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> Commentaire 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! C'est mon point.
J4N
2
Et l'avantage est également que si vous utilisez Entity Framework, cela peut être fait du côté SQL au lieu du côté C #.
J4N
4
La vraie raison pour laquelle cela n'aide pas est que cela ne fusionne aucun des objets présents dans les deux listes.
Mike Goatly
15

C'est Linq

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

C'est normal (AddRange)

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

C'est normal (Foreach)

var mergedList=new List<Person>();

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

C'est normal (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
la source
12

Il y a quelques éléments à faire, en supposant que chaque liste ne contient pas de doublons, que le nom est un identifiant unique et qu'aucune des deux listes n'est ordonnée.

Commencez par créer une méthode d'extension d'ajout pour obtenir une seule liste:

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

Ainsi peut obtenir une seule liste:

var oneList = list1.Append(list2);

Puis groupe sur le nom

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

Puis peut traiter chaque groupe avec un assistant pour traiter un groupe à la fois

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

Qui peut être appliqué à chaque élément de grouped:

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

(Attention: non testé.)

Richard
la source
2
Votre Appendest une copie presque exacte du prêt à l'emploi Concat.
Rawling
@Rawling: C'est le cas, pour une raison quelconque, j'ai continué à manquer Enumerable.Concatet à le réimplémenter .
Richard
2

Vous avez besoin de quelque chose comme une jointure externe complète. System.Linq.Enumerable n'a pas de méthode qui implémente une jointure externe complète, nous devons donc le faire nous-mêmes.

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
la source
2

Le code suivant fonctionne-t-il pour votre problème? J'ai utilisé un foreach avec un peu de linq à l'intérieur pour combiner des listes et j'ai supposé que les gens sont égaux si leurs noms correspondent, et il semble imprimer les valeurs attendues lors de l'exécution. Resharper n'offre aucune suggestion pour convertir le foreach en linq, c'est donc probablement aussi bien que cela peut être fait de cette façon.

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
la source
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
la source