80 votes

Fusionner deux listes d'objets avec linq

Je suis dans la situation suivante

Class Person
{
    string Name
    int Value
    int Change
}

List<Person> list1
List<Person> list2

J'ai besoin de combiner les deux listes en une nouvelle liste. s'il s'agit de la même personne, l'enregistrement combiné aura ce nom, la valeur de la personne dans la liste 2, le changement sera la valeur de la liste 2 - la valeur de la liste 1. Le changement est 0 si aucun doublon

151voto

Koen Zomers Points 2419

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, elle utilisera les méthodes Equals et GetHashCode par défaut de votre classe Person. Si vous souhaitez par exemple comparer des personnes en comparant leur propriété Name, vous devez surcharger ces méthodes pour effectuer la comparaison vous-même. Pour ce faire, consultez l'exemple de code suivant. 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 voulez pas que la méthode Equals par défaut de votre classe Person utilise toujours le Name pour comparer deux objets, vous pouvez également écrire une classe de comparaison qui utilise l'interface IEqualityComparer. Vous pouvez ensuite fournir ce comparateur comme deuxième paramètre de la méthode Union de l'extension Linq. Pour plus d'informations sur l'écriture d'une telle méthode de comparaison, consultez le site suivant http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

34voto

J4N Points 2422

Pourquoi vous n'utilisez pas simplement Concat ?

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

dans votre cas :

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);

11voto

Mike Goatly Points 1925

J'ai remarqué que cette question n'a pas été marquée comme répondue après 2 ans - je pense que la réponse la plus proche est Richards, mais on peut la simplifier beaucoup à 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 cela ne soit pas erreur dans le cas où vous avez des noms en double dans l'un ou l'autre des ensembles.

D'autres réponses ont suggéré d'utiliser l'union - ce n'est absolument pas la solution, car vous n'obtiendrez qu'une liste distincte, sans faire la combinaison.

8voto

Richard Points 54016

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

Créez d'abord une méthode d'extension append pour obtenir une liste unique :

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

On peut ainsi obtenir une liste unique :

var oneList = list1.Append(list2);

Puis groupe sur le nom

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

Ensuite, on peut traiter chaque groupe avec une aide 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é.)

1voto

David B Points 53123

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, donc nous devons 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();

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X