Crea un elenco da due elenchi di oggetti con linq


161

Ho la seguente situazione

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

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

Devo combinare le 2 liste in una nuova List<Person> nel caso in cui sia la stessa persona che il record della combinazione avrebbe quel nome, il valore della persona nella lista2, il cambiamento sarebbe il valore della lista2 - il valore della lista1. Il cambiamento è 0 se nessun duplicato


2
Linq è davvero necessario: una buona foreach con un po 'di espressioni linq-ish potrebbe fare altrettanto.
Rashack,

1
Aggiungendo questo commento come una versione del titolo della domanda e la domanda effettiva non corrispondeva: la vera risposta a questa è questa risposta di Mike . Molte altre risposte, sebbene utili, in realtà non risolvono il problema presentato dal poster originale.
Joshua,

Risposte:


254

Questo può essere fatto facilmente usando il metodo di estensione Linq Union. Per esempio:

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

Ciò restituirà un Elenco in cui i due elenchi vengono uniti e i doppi vengono rimossi. Se non si specifica un comparatore nel metodo di estensione Union come nel mio esempio, utilizzerà i metodi Equals e GetHashCode predefiniti nella classe Person. Se, ad esempio, vuoi confrontare le persone confrontando la loro proprietà Nome, devi eseguire l'override di questi metodi per eseguire tu stesso il confronto. Controllare il seguente esempio di codice per farlo. Devi aggiungere questo codice alla tua 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 non si desidera impostare il metodo Equals predefinito della classe Person per utilizzare sempre il nome per confrontare due oggetti, è possibile anche scrivere una classe di confronto che utilizza l'interfaccia IEqualityComparer. È quindi possibile fornire questo comparatore come secondo parametro nel metodo Union di estensione Linq. Ulteriori informazioni su come scrivere un metodo di confronto di questo tipo sono disponibili all'indirizzo http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx


10
Non vedo come questo risponda alla domanda sulla fusione di valori.
Wagner da Silva,

1
Questo non risponde, Union contiene solo gli elementi presenti nei due set, non tutti gli elementi presenti in uno dei due elenchi
J4N,

7
@ J4N stai forse confondendo Unioncon Intersect?
Kos,

11
Per riferimento: c'è anche Concatche non unisce duplicati
Kos,

7
Ti dispiacerebbe modificare questa risposta in modo che risponda effettivamente alla domanda? Trovo ridicolo che una risposta sia così votata nonostante il fatto che non risponda alla domanda, solo perché risponde al titolo e a una query di base di Google ("liste di unione linq").
Rawling,

78

Ho notato che questa domanda non è stata contrassegnata come risposta dopo 2 anni - penso che la risposta più vicina sia Richards, ma può essere semplificata molto a questo:

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

Anche se questo non sarà errore nel caso in cui si hanno i nomi duplicati in entrambi i set.

Alcune altre risposte hanno suggerito di utilizzare l'unione: questa non è sicuramente la strada da percorrere in quanto ti darà solo un elenco distinto, senza fare la combinazione.


8
Questo post risponde effettivamente alla domanda e lo fa bene.
philu

3
Questa dovrebbe essere la risposta accettata. Non ho mai visto una domanda con così tanti voti per le risposte che non rispondono alla domanda posta!
Todd Menier,

Bella risposta. Potrei apportare una piccola modifica ad esso, quindi il valore è in realtà il valore di list2 e quindi il cambiamento continua se hai duplicati: Imposta valore = p2.Value e Change = p1.Change + p2.Value - p1.Value
Ravi Desai,

70

Perché non usi solo Concat?

Concat fa parte di linq ed è più efficiente di fare un AddRange()

nel tuo caso:

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

13
Come fai a sapere che è più efficiente?
Jerry Nixon,

@Jerry Nixon Non l'ha provato, ma la spiegazione sembra logica. stackoverflow.com/questions/1337699/…
Nullius

9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> Commento di 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! questo è il mio punto.
J4N,

2
E il vantaggio è anche che se si utilizza Entity Framework, questo può essere fatto sul lato SQL anziché sul lato C #.
J4N,

4
La vera ragione per cui questo non aiuta è che in realtà non unisce nessuno degli oggetti presenti in entrambi gli elenchi.
Mike Goatly,

15

Questo è Linq

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

Questo è normalmente (AddRange)

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

Questo è normalmente (Foreach)

var mergedList=new List<Person>();

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

Questo è normalmente (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);
   }
}

12

Ci sono alcuni pezzi per farlo, supponendo che ogni elenco non contenga duplicati, Nome sia un identificatore univoco e nessuno dei due elenchi è ordinato.

Innanzitutto crea un metodo di estensione append per ottenere un singolo elenco:

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

Quindi può ottenere un unico elenco:

var oneList = list1.Append(list2);

Quindi raggruppare per nome

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

Quindi può elaborare ciascun gruppo con un helper per elaborare un gruppo alla volta

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

Che può essere applicato a ciascun elemento di grouped:

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

(Avviso: non testato.)


2
Il tuo Appendè un duplicato quasi esatto del pronto intervento Concat.
Rawling,

@Rawling: Lo è, per qualche motivo, mi sono perso Enumerable.Concate quindi rieseguito.
Richard,

2

Hai bisogno di qualcosa come un join esterno completo. System.Linq.Enumerable non ha un metodo che implementa un join esterno completo, quindi dobbiamo farlo da soli.

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

Il seguente codice funziona per il tuo problema? Ho usato un foreach con un po 'di linq all'interno per fare la combinazione di liste e ho ipotizzato che le persone fossero uguali se i loro nomi corrispondono, e sembra stampare i valori previsti quando vengono eseguiti. Resharper non offre alcun suggerimento per convertire foreach in linq, quindi questo è probabilmente buono come lo farà in questo modo.

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

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);
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.