Buat daftar dari dua daftar objek dengan LINQ

161

Saya memiliki situasi berikut

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

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

Saya perlu menggabungkan 2 daftar menjadi yang baru List<Person> jika itu orang yang sama catatan menggabungkan akan memiliki nama itu, nilai orang di list2, perubahan akan menjadi nilai list2 - nilai list1. Perubahan adalah 0 jika tidak ada duplikat

GamegaMan
sumber
2
Apakah LINQ benar-benar diperlukan - sebuah pendahuluan yang bagus dengan sedikit ekspresi LINQ-ish bisa dilakukan juga.
Rashack
1
Menambahkan komentar ini sebagai versi judul pertanyaan dan pertanyaan aktual tidak cocok: jawaban sesungguhnya untuk ini adalah jawaban dari Mike ini . Sebagian besar jawaban lain, meskipun bermanfaat, tidak benar-benar menyelesaikan masalah yang disajikan oleh poster aslinya.
Joshua

Jawaban:

254

Ini dapat dengan mudah dilakukan dengan menggunakan metode ekstensi Linq Union. Sebagai contoh:

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

Ini akan mengembalikan Daftar tempat kedua daftar digabungkan dan dobel dihapus. Jika Anda tidak menentukan pembanding dalam metode ekstensi Union seperti dalam contoh saya, ini akan menggunakan metode Persamaan dan GetHashCode default di kelas Person Anda. Misalnya, jika Anda ingin membandingkan orang dengan membandingkan properti Nama mereka, Anda harus mengganti metode ini untuk melakukan perbandingan sendiri. Periksa contoh kode berikut untuk mencapai itu. Anda harus menambahkan kode ini ke kelas Person Anda.

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

Jika Anda tidak ingin mengatur metode Persamaan default kelas Person Anda untuk selalu menggunakan Nama untuk membandingkan dua objek, Anda juga dapat menulis kelas pembanding yang menggunakan antarmuka IEqualityComparer. Anda kemudian dapat memberikan pembanding ini sebagai parameter kedua dalam metode Union extension Linq. Informasi lebih lanjut tentang cara menulis metode pembanding seperti ini dapat ditemukan di http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

Koen Zomers
sumber
10
Saya tidak melihat bagaimana ini menjawab pertanyaan tentang penggabungan nilai.
Wagner da Silva
1
Ini tidak menjawab, Union hanya akan berisi item yang ada dalam dua set, tidak ada elemen yang ada di salah satu dari dua daftar
J4N
7
@ J4N yang Anda mungkin membingungkan Uniondengan Intersect?
Kos
11
Untuk referensi: ada juga Concatyang tidak menggabungkan duplikat
Kos
7
Maukah Anda mengedit jawaban ini sehingga sebenarnya menjawab pertanyaan? Saya merasa konyol bahwa jawaban sangat dipilih meskipun kenyataannya tidak menjawab pertanyaan, hanya karena menjawab judul dan permintaan dasar Google ("daftar gabungan linq").
Rawling
78

Saya perhatikan bahwa pertanyaan ini tidak ditandai sebagai dijawab setelah 2 tahun - saya pikir jawaban terdekat adalah Richards, tetapi dapat disederhanakan cukup banyak untuk ini:

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

Meskipun ini tidak akan kesalahan dalam kasus di mana Anda memiliki nama duplikat di setiap set.

Beberapa jawaban lain menyarankan menggunakan penyatuan - ini jelas bukan cara untuk pergi karena hanya akan memberi Anda daftar yang berbeda, tanpa melakukan penggabungan.

Mike Goatly
sumber
8
Posting ini sebenarnya menjawab pertanyaan, dan melakukannya dengan baik.
philu
3
Ini harus menjadi jawaban yang diterima. Belum pernah melihat pertanyaan dengan begitu banyak upvotes untuk jawaban yang tidak menjawab pertanyaan yang diajukan!
Todd Menier
Jawaban bagus. Saya mungkin membuat satu perubahan kecil untuk itu, jadi Nilai sebenarnya adalah nilai dari list2, dan agar Perubahan terus berlanjut jika Anda memiliki duplikat: Tetapkan Nilai = p2.Value dan Ubah = p1.Change + p2.Value - p1.Value
Ravi Desai
70

Kenapa kamu tidak pakai saja Concat?

Concat adalah bagian dari LINQ dan lebih efisien daripada melakukan AddRange()

dalam kasus Anda:

List<Person> list1 = ...
List<Person> list2 = ...
List<Person> total = list1.Concat(list2);
J4N
sumber
13
Bagaimana Anda tahu itu lebih efisien?
Jerry Nixon
@ Jerry Nixon Dia tidak mengujinya, tetapi penjelasannya tampak masuk akal. stackoverflow.com/questions/1337699/…
Nullius
9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> komentar 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! Ini adalah poin saya.
J4N
2
Dan keuntungannya juga adalah jika Anda menggunakan Entity Framework, ini bisa dilakukan di sisi SQL alih-alih sisi C #.
J4N
4
Alasan sebenarnya ini tidak membantu adalah bahwa itu tidak benar-benar menggabungkan objek yang ada di kedua daftar.
Mike Goatly
15

Ini Linq

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

Ini adalah Normaly (AddRange)

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

Ini Normaly (Foreach)

var mergedList=new List<Person>();

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

Ini adalah Normaly (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
sumber
12

Ada beberapa bagian untuk melakukan ini, dengan asumsi setiap daftar tidak mengandung duplikat, Nama adalah pengidentifikasi unik, dan tidak ada daftar yang dipesan.

Pertama buat metode penambahan append untuk mendapatkan satu daftar:

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

Dengan demikian bisa mendapatkan satu daftar:

var oneList = list1.Append(list2);

Kemudian kelompokkan nama

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

Kemudian dapat memproses setiap kelompok dengan helper untuk memproses satu kelompok pada suatu waktu

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

Yang dapat diterapkan untuk setiap elemen grouped:

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

(Peringatan: tidak diuji.)

Richard
sumber
2
AppendDuplikat Anda hampir persis sama dengan out-of-the-box Concat.
Rawling
@ Rawling: Ya, untuk beberapa alasan saya terus menghilang Enumerable.Concatdan dengan demikian mengimplementasikannya kembali.
Richard
2

Anda memerlukan sesuatu seperti gabungan luar penuh. System.Linq.Enumerable tidak memiliki metode yang mengimplementasikan gabungan luar penuh, jadi kita harus melakukannya sendiri.

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
sumber
2

Apakah kode berikut berfungsi untuk masalah Anda? Saya telah menggunakan foreach dengan sedikit linq di dalam untuk melakukan menggabungkan daftar dan berasumsi bahwa orang sama jika nama mereka cocok, dan tampaknya untuk mencetak nilai yang diharapkan saat dijalankan. Resharper tidak menawarkan saran untuk mengubah foreach menjadi LINQ jadi ini mungkin sebaik itu akan melakukannya dengan cara ini.

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