web-dev-qa-db-fra.com

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

J'ai la situation suivante

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

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

J'ai besoin de combiner les 2 listes dans un nouveau List<Person> au cas où il s'agirait de la même personne, l'enregistrement de combinaison aurait ce nom, la valeur de la personne dans list2, change serait la valeur de list2 - la valeur de list1. Le changement est 0 si aucun doublon

150
ssl

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

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

Cela retournera une liste dans laquelle les deux listes sont fusionnées et les doublons 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 par défaut Equals et GetHashCode dans votre classe Person. Si vous souhaitez par exemple comparer des personnes en comparant leur propriété Name, vous devez redéfinir ces méthodes pour effectuer la comparaison vous-même. Vérifiez l'exemple de code suivant pour accomplir cela. 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 que la méthode Equals par défaut de votre classe Person utilise toujours le nom pour comparer deux objets, vous pouvez également écrire une classe comparateur utilisant l'interface IEqualityComparer. Vous pouvez ensuite fournir ce comparateur en tant que deuxième paramètre de la méthode de l'union d'extension Linq. Pour plus d'informations sur la manière d'écrire une telle méthode de comparaison, consultez http://msdn.Microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

242
Koen Zomers

J'ai remarqué que cette question n'avait pas été corrigée au bout de 2 ans. Je pense que la réponse la plus proche est Richards, mais on peut la simplifier beaucoup:

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.

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

71
Mike Goatly

Pourquoi n'utilisez-vous pas simplement 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);
67
J4N

C'est linq

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

C'est normalement (AddRange)

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

C'est normalement (Foreach)

var mergedList=new List<Person>();

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

C'est normalement (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);
   }
}
14
Alper Şaldırak

Pour ce faire, il existe quelques éléments, en supposant que chaque liste ne contient pas de doublons, Nom est un identificateur unique et qu'aucune liste n'est ordonnée.

Commencez par créer 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; }
  }
}

Ainsi peut obtenir une liste unique:

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

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

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

(Attention: non testé.)

12
Richard

Le code suivant fonctionne-t-il pour votre problème? J'ai utilisé un foreach avec un peu de linq à l'intérieur pour faire la combinaison de listes et supposé que les personnes sont égales 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 qu'il le fera 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();
   }
}
2
Sean Reid

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();
2
Amy B
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);
    }
}
1
pungggi