Utwórz listę z dwóch list obiektów za pomocą linq

161

Mam następującą sytuację

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

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

Muszę połączyć 2 listy w nową List<Person> w przypadku, gdy jest to ta sama osoba, rekord łączenia miałby to imię, wartość osoby na liście2, zmiana byłaby wartością list2 - wartość list1. Zmiana wynosi 0, jeśli nie ma duplikatu

ΩmegaMan
źródło
2
Czy linq jest naprawdę potrzebny - fajny foreach z odrobiną wyrażeń linq-ish również mógłby zrobić.
Rashack
1
Dodanie tego komentarza jako wersji tytułu pytania i rzeczywistego pytania nie zgadzają się: prawdziwą odpowiedzią na to jest ta odpowiedź od Mike'a . Większość innych odpowiedzi, choć użytecznych, w rzeczywistości nie rozwiązuje problemu przedstawionego przez oryginalny plakat.
Joshua,

Odpowiedzi:

254

Można to łatwo zrobić za pomocą metody rozszerzenia Linq Union. Na przykład:

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

Spowoduje to zwrócenie listy, w której dwie listy zostaną scalone, a podwójne zostaną usunięte. Jeśli nie określisz elementu porównującego w metodzie rozszerzenia Union, jak w moim przykładzie, użyje ona domyślnych metod Equals i GetHashCode w Twojej klasie Person. Jeśli na przykład chcesz porównać osoby, porównując ich właściwość Name, musisz zastąpić te metody, aby samodzielnie wykonać porównanie. Sprawdź poniższy przykład kodu, aby to osiągnąć. Musisz dodać ten kod do swojej klasy 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);
}

Jeśli nie chcesz ustawiać domyślnej metody Equals klasy Person, aby zawsze używać Name do porównywania dwóch obiektów, możesz również napisać klasę porównującą, która używa interfejsu IEqualityComparer. Następnie można podać tę funkcję porównującą jako drugi parametr w metodzie Union rozszerzenia Linq. Więcej informacji o tym, jak napisać taką metodę porównującą, można znaleźć pod adresem http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

Koen Zomers
źródło
10
Nie rozumiem, jak to odpowiada na pytanie o scalenie wartości.
Wagner da Silva
1
To nie odpowiada, Unia będzie zawierała tylko elementy obecne w dwóch zestawach, a nie żaden element obecny na jednej z dwóch list
J4N
7
@ J4N ty chyba myli Unionsię Intersect?
Kos
11
Dla porównania: jest też to, Concatże nie łączy duplikatów
Kos
7
Czy możesz zmodyfikować tę odpowiedź, aby faktycznie odpowiadała na pytanie? Uważam za śmieszne, że odpowiedź jest tak wysoko oceniana, mimo że nie odpowiada na pytanie, tylko dlatego, że odpowiada na tytuł i podstawowe zapytanie Google („listy linq merge list”).
Rawling,
78

Zauważyłem, że to pytanie nie zostało oznaczone jako udzielone po 2 latach - myślę, że najbliższa odpowiedź to Richards, ale można to bardzo uprościć:

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

Chociaż nie będzie to błąd w przypadku, gdy masz zduplikowane nazwy w jednym z zestawów.

Niektóre inne odpowiedzi sugerują użycie łączenia - zdecydowanie nie jest to właściwe rozwiązanie, ponieważ da ci to tylko odrębną listę, bez wykonywania łączenia.

Mike Goatly
źródło
8
Ten post faktycznie odpowiada na pytanie i robi to dobrze.
philu
3
To powinna być akceptowana odpowiedź. Nigdy nie widziałem pytania z tyloma pozytywnymi odpowiedziami, które nie odpowiadają na zadane pytanie!
Todd Menier
Niezła odpowiedź. Mogę wprowadzić jedną małą zmianę, więc wartość jest w rzeczywistości wartością z listy2, a zmiana zachowuje ważność, jeśli masz duplikaty: Ustaw wartość = p2.Wartość i zmiana = p1.Change + p2.Value - p1.Value
Ravi Desai
70

Dlaczego po prostu nie używasz Concat?

Concat jest częścią linq i jest bardziej wydajny niż robienie AddRange()

w Twoim przypadku:

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);
J4N
źródło
13
Skąd wiesz, że jest bardziej wydajny?
Jerry Nixon
@Jerry Nixon Nie przetestował tego, ale wyjaśnienie wydaje się logiczne. stackoverflow.com/questions/1337699/…
Nullius,
9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> Komentarz Grega: 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! To jest mój punkt widzenia.
J4N
2
Zaletą jest również to, że jeśli używasz Entity Framework, można to zrobić po stronie SQL zamiast po stronie C #.
J4N
4
Prawdziwym powodem, dla którego to nie pomaga, jest to, że w rzeczywistości nie łączy żadnego z obiektów obecnych na obu listach.
Mike Goatly
15

To jest Linq

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

To jest normalne (AddRange)

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

To jest normalne (dla każdego)

var mergedList=new List<Person>();

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

To jest normalne (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
źródło
12

Jest na to kilka sposobów, zakładając, że każda lista nie zawiera duplikatów, nazwa jest unikalnym identyfikatorem i żadna z list nie jest uporządkowana.

Najpierw utwórz metodę rozszerzenia dołączania, aby uzyskać jedną listę:

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

W ten sposób można uzyskać jedną listę:

var oneList = list1.Append(list2);

Następnie grupuj według nazwy

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

Następnie możesz przetwarzać każdą grupę z pomocą pomocnika, aby przetwarzać jedną grupę na raz

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

Które można zastosować do każdego elementu grouped:

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

(Ostrzeżenie: nieprzetestowane.)

Richard
źródło
2
Twój Appendjest prawie dokładnym duplikatem gotowego do użycia Concat.
Rawling
@Rawling: Tak jest, z jakiegoś powodu ciągle go brakowało Enumerable.Concati dlatego wdrażałem go ponownie.
Richard
2

Potrzebujesz czegoś takiego jak pełne połączenie zewnętrzne. System.Linq.Enumerable nie ma metody implementującej pełne sprzężenie zewnętrzne, więc musimy to zrobić sami.

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.
źródło
2

Czy poniższy kod działa w przypadku Twojego problemu? Użyłem foreach z odrobiną linq w środku do łączenia list i założyłem, że ludzie są równi, jeśli ich imiona pasują, i wydaje się, że po uruchomieniu wypisuje oczekiwane wartości. Resharper nie oferuje żadnych sugestii dotyczących konwersji foreach na linq, więc prawdopodobnie jest to tak dobre, jak można to zrobić w ten sposób.

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
źródło
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
źródło