Erstellen Sie mit linq eine Liste aus zwei Objektlisten

161

Ich habe die folgende Situation

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

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

Ich muss die 2 Listen zu einer neuen kombinieren, List<Person> falls es dieselbe Person ist, die der Kombinationsdatensatz diesen Namen haben würde, Wert der Person in Liste2, Änderung wäre der Wert von Liste2 - der Wert von Liste1. Änderung ist 0, wenn kein Duplikat vorhanden ist

ΩmegaMan
quelle
2
Wird linq wirklich benötigt - ein netter foreach mit ein paar linq-artigen Ausdrücken könnte auch reichen.
Rashack
1
Das Hinzufügen dieses Kommentars als Version des Titels der Frage und der tatsächlichen Frage stimmte nicht überein: Die eigentliche Antwort darauf ist diese Antwort von Mike . Die meisten anderen Antworten sind zwar nützlich, lösen jedoch nicht das Problem, das das Originalplakat darstellt.
Joshua

Antworten:

254

Dies kann einfach mit der Linq-Erweiterungsmethode Union erfolgen. Beispielsweise:

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

Dies gibt eine Liste zurück, in der die beiden Listen zusammengeführt und Doppel entfernt werden. Wenn Sie in der Union-Erweiterungsmethode keinen Vergleicher wie in meinem Beispiel angeben, werden die Standardmethoden Equals und GetHashCode in Ihrer Person-Klasse verwendet. Wenn Sie beispielsweise Personen durch Vergleichen ihrer Name-Eigenschaft vergleichen möchten, müssen Sie diese Methoden überschreiben, um den Vergleich selbst durchzuführen. Überprüfen Sie dazu das folgende Codebeispiel. Sie müssen diesen Code Ihrer Personenklasse hinzufügen.

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

Wenn Sie nicht festlegen möchten, dass die Standardmethode Equals Ihrer Person-Klasse immer den Namen zum Vergleichen von zwei Objekten verwendet, können Sie auch eine Vergleichsklasse schreiben, die die IEqualityComparer-Schnittstelle verwendet. Sie können diesen Vergleicher dann als zweiten Parameter in der Linq-Erweiterungs-Union-Methode angeben. Weitere Informationen zum Schreiben einer solchen Vergleichsmethode finden Sie unter http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

Koen Zomers
quelle
10
Ich sehe nicht, wie dies die Frage nach der Verschmelzung von Werten beantwortet.
Wagner da Silva
1
Dies reagiert nicht, Union wird nur Elemente enthalten, die in den beiden Sätzen vorhanden sind, kein Element, das in einer der beiden Listen vorhanden ist
J4N
7
@ J4N verwechseln Sie vielleicht Unionmit Intersect?
Kos
11
Als Referenz: Es gibt auch Concatkeine Duplikate
Kos
7
Würde es Ihnen etwas ausmachen, diese Antwort so zu bearbeiten, dass sie die Frage tatsächlich beantwortet? Ich finde es lächerlich, dass eine Antwort so hoch bewertet wird, obwohl sie die Frage nicht beantwortet, nur weil sie den Titel und eine grundlegende Google-Abfrage ("Linq Merge Lists") beantwortet.
Rawling
78

Ich habe festgestellt, dass diese Frage nach 2 Jahren nicht als beantwortet markiert wurde. Ich denke, die nächste Antwort ist Richards, aber sie kann erheblich vereinfacht werden:

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

Obwohl dies kein Fehler sein wird , wenn Sie in beiden Sätzen doppelte Namen haben.

Einige andere Antworten haben vorgeschlagen, Unioning zu verwenden - dies ist definitiv nicht der richtige Weg, da Sie nur eine eindeutige Liste erhalten, ohne die Kombination durchzuführen.

Mike Goatly
quelle
8
Dieser Beitrag beantwortet die Frage tatsächlich und macht es gut.
Philu
3
Dies sollte die akzeptierte Antwort sein. Ich habe noch nie eine Frage mit so vielen positiven Stimmen für Antworten gesehen, die die gestellte Frage nicht beantworten!
Todd Menier
Gute Antwort. Ich könnte eine kleine Änderung daran vornehmen, sodass der Wert tatsächlich der Wert aus Liste2 ist und die Änderung erhalten bleibt, wenn Sie Duplikate haben: Setzen Sie Wert = p2.Wert und Änderung = p1.Wechsel + p2.Wert - p1.Wert
Ravi Desai
70

Warum benutzt du nicht einfach Concat ?

Concat ist ein Teil von linq und effizienter als ein AddRange()

in deinem Fall:

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);
J4N
quelle
13
Woher wissen Sie, dass es effizienter ist?
Jerry Nixon
@ Jerry Nixon Er / sie hat es nicht getestet, aber die Erklärung scheint logisch. stackoverflow.com/questions/1337699/…
Nullius
9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> Gregs Kommentar: 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! Dies ist mein Punkt.
J4N
2
Der Vorteil ist auch, dass bei Verwendung von Entity Framework dies auf der SQL-Seite anstelle der C # -Seite erfolgen kann.
J4N
4
Der wahre Grund, warum dies nicht hilft, ist, dass keines der in beiden Listen vorhandenen Objekte zusammengeführt wird.
Mike Goatly
15

Das ist Linq

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

Dies ist Normal (AddRange)

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

Dies ist Normal (Foreach)

var mergedList=new List<Person>();

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

Dies ist 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
quelle
12

Dies erfordert einige Schritte, vorausgesetzt, jede Liste enthält keine Duplikate, Name ist eine eindeutige Kennung und keine der beiden Listen ist geordnet.

Erstellen Sie zunächst eine Append-Erweiterungsmethode, um eine einzelne Liste zu erhalten:

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

Somit kann eine einzelne Liste erhalten werden:

var oneList = list1.Append(list2);

Dann gruppieren Sie nach Namen

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

Dann kann jede Gruppe mit einem Helfer verarbeitet werden, um jeweils eine Gruppe zu verarbeiten

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

Welche kann auf jedes Element angewendet werden von grouped:

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

(Warnung: ungetestet.)

Richard
quelle
2
Sie Appendsind ein fast genaues Duplikat des Out-of-the-Box Concat.
Rawling
@Rawling: Aus irgendeinem Grund habe ich es immer wieder vermisst Enumerable.Concatund es daher neu implementiert.
Richard
2

Sie benötigen so etwas wie eine vollständige äußere Verknüpfung. System.Linq.Enumerable hat keine Methode, die eine vollständige äußere Verknüpfung implementiert, daher müssen wir dies selbst tun.

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.
quelle
2

Funktioniert der folgende Code für Ihr Problem? Ich habe ein foreach mit ein wenig Linq verwendet, um Listen zu kombinieren, und angenommen, dass die Leute gleich sind, wenn ihre Namen übereinstimmen, und es scheint, als würden die erwarteten Werte beim Ausführen ausgedruckt. Resharper bietet keine Vorschläge zur Konvertierung des foreach in linq an, daher ist dies wahrscheinlich so gut, wie es auf diese Weise möglich ist.

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