สร้างรายการจากสองรายการวัตถุด้วย linq


161

ฉันมีสถานการณ์ต่อไปนี้

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

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

ฉันต้องรวม 2 รายการเข้าด้วยกันList<Person> ในกรณีที่เป็นคนเดียวกันที่บันทึกรวมจะมีชื่อนั้นค่าของบุคคลในรายการ 2 การเปลี่ยนแปลงจะเป็นค่าของ list2 - มูลค่าของ list1 เปลี่ยนเป็น 0 ถ้าไม่ซ้ำกัน


2
จำเป็นต้องใช้ linq หรือไม่ - เป็น foreach ที่ดีที่มีการแสดงออกเล็กน้อยของ linq-ish สามารถทำได้เช่นกัน
Rashack

1
เพิ่มความคิดเห็นนี้เป็นรุ่นของชื่อคำถามและคำถามที่เกิดขึ้นจริงไม่ตรงกับ: คำตอบที่แท้จริงนี้คือคำตอบนี้จากไมค์ คำตอบอื่น ๆ ส่วนใหญ่ในขณะที่มีประโยชน์ไม่ได้แก้ปัญหาที่นำเสนอโดยผู้โพสต์ดั้งเดิม
Joshua

คำตอบ:


254

สามารถทำได้อย่างง่ายดายโดยใช้ส่วนขยายของ Linq ตัวอย่างเช่น:

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

นี่จะส่งคืนรายการที่รวมสองรายการเข้าด้วยกันและลบออกเป็นสองเท่า หากคุณไม่ได้ระบุตัวเปรียบเทียบในวิธีการขยาย Union เหมือนกับในตัวอย่างของฉันมันจะใช้เมธอด Equals และ GetHashCode ที่เป็นค่าเริ่มต้นในคลาส 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);
}

หากคุณไม่ต้องการตั้งค่าเมธอด Equals ที่เป็นค่าเริ่มต้นของคลาส Person ให้ใช้ชื่อเพื่อเปรียบเทียบสองออบเจ็กต์คุณยังสามารถเขียนคลาส Comparer ที่ใช้อินเตอร์เฟส IEqualityComparer จากนั้นคุณสามารถระบุเครื่องมือเปรียบเทียบนี้เป็นพารามิเตอร์ตัวที่สองในวิธีการขยายส่วนขยาย Linq ข้อมูลเพิ่มเติมเกี่ยวกับวิธีการเขียนวิธีการ Comparer นั้นสามารถดูได้ที่http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx


10
ฉันไม่เห็นว่าสิ่งนี้ตอบคำถามเกี่ยวกับการผสานค่าอย่างไร
Wagner da Silva

1
สิ่งนี้ไม่ตอบสหภาพจะมีเฉพาะรายการที่มีอยู่ในสองชุดไม่ใช่องค์ประกอบใด ๆ ที่อยู่ในรายการใดรายการหนึ่งในสองรายการ
J4N

7
@ J4N คุณอาจจะทำให้เกิดความสับสนUnionกับIntersect?
คอส

11
สำหรับการอ้างอิง: นอกจากนี้ยังConcatไม่รวมการซ้ำซ้อน
Kos

7
คุณจะแก้ไขคำตอบนี้เพื่อที่จะตอบคำถามได้หรือไม่ ฉันพบว่ามันไร้สาระที่คำตอบนั้นได้รับการโหวตอย่างสูงแม้ว่าที่จริงแล้วมันไม่ได้ตอบคำถามเพียงเพราะมันตอบคำถามและคำสืบค้นพื้นฐานของ Google ("รายการ linq merge")
Rawling

78

ฉันสังเกตเห็นว่าคำถามนี้ไม่ได้ถูกทำเครื่องหมายเป็นคำตอบหลังจาก 2 ปี - ฉันคิดว่าคำตอบที่ใกล้ที่สุดคือ Richards แต่สามารถทำให้ง่ายขึ้นมากสำหรับเรื่องนี้:

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

แม้ว่าจะไม่เกิดข้อผิดพลาดในกรณีที่คุณมีชื่อซ้ำกันในทั้งสองชุด

มีคำตอบอื่น ๆ ที่แนะนำโดยใช้การรวมกัน - นี่ไม่ใช่วิธีที่จะไปอย่างแน่นอนเพราะคุณจะได้รับรายการที่แตกต่างโดยไม่ต้องรวม


8
โพสต์นี้จริงตอบคำถามและทำได้ดี
philu

3
นี่ควรเป็นคำตอบที่ยอมรับได้ ไม่เคยเห็นคำถามที่มี upvotes มากมายสำหรับคำตอบที่ไม่ตอบคำถามที่ถาม!
Todd Menier

คำตอบที่ดี ฉันอาจทำการเปลี่ยนแปลงเพียงเล็กน้อยดังนั้นค่าจึงเป็นค่าจากรายการ 2 และเพื่อให้การเปลี่ยนแปลงติดตามได้ถ้าคุณทำซ้ำ: ตั้งค่า = p2.Value และเปลี่ยน = p1.Change + p2.Value - p1.Value
Ravi Desai

70

ทำไมคุณไม่เพียงแค่ใช้Concat?

Concat เป็นส่วนหนึ่งของ linq และมีประสิทธิภาพมากกว่าการทำ AddRange()

ในกรณีของคุณ:

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

13
คุณจะรู้ได้อย่างไรว่ามันมีประสิทธิภาพมากขึ้น?
Jerry Nixon

@Jerry Nixon เขา / เธอไม่ได้ทดสอบ แต่คำอธิบายนั้นดูสมเหตุสมผล stackoverflow.com/questions/1337699/…
Nullius

9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> ความคิดเห็นของ 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! นี่คือประเด็นของฉัน
J4N

2
และข้อดีก็คือถ้าคุณใช้ Entity Framework สิ่งนี้สามารถทำได้บนฝั่ง SQL แทนที่จะเป็นด้าน C #
J4N

4
เหตุผลที่แท้จริงสิ่งนี้ไม่ได้ช่วยคือมันไม่ได้รวมวัตถุใด ๆ ที่มีอยู่ในทั้งสองรายการ
Mike Goatly

15

นี่คือ Linq

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

นี่คือ Normaly (AddRange)

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

นี่คือ Normaly (เบื้องหน้า)

var mergedList=new List<Person>();

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

นี่คือ 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);
   }
}

12

มีสองสามชิ้นในการทำเช่นนี้โดยสมมติว่าแต่ละรายการไม่มีรายการที่ซ้ำกันชื่อเป็นตัวระบุที่ไม่ซ้ำกันและไม่มีรายการใดเรียงลำดับ

ก่อนอื่นให้สร้างวิธีการต่อท้ายเพื่อรับรายการเดียว:

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

ดังนั้นจะได้รับรายการเดียว:

var oneList = list1.Append(list2);

จากนั้นจัดกลุ่มชื่อ

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

จากนั้นสามารถประมวลผลแต่ละกลุ่มด้วยผู้ช่วยในการประมวลผลทีละกลุ่ม

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

ซึ่งสามารถนำไปใช้กับแต่ละองค์ประกอบของgrouped:

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

(คำเตือน: ยังไม่ทดลอง)


2
ของคุณเป็นเกือบแน่นอนซ้ำของออกจากกล่องAppend Concat
Rawling

@Rawling: มันด้วยเหตุผลบางอย่างที่ฉันเก็บไว้หายไปEnumerable.Concatและทำให้อีกครั้งการดำเนินการนั้น
Richard

2

คุณต้องการบางสิ่งเช่นการเข้าร่วมเต็มรูปแบบภายนอก System.Linq.Enumerable ไม่มีเมธอดที่ใช้การรวมภายนอกแบบเต็มดังนั้นเราต้องทำเอง

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

รหัสต่อไปนี้ใช้สำหรับปัญหาของคุณหรือไม่ ฉันใช้ foreach พร้อมลินุกซ์ด้านในเพื่อรวมรายการและคิดว่าผู้คนนั้นเท่ากันถ้าชื่อของพวกเขาตรงกันและดูเหมือนว่าจะพิมพ์ค่าที่คาดหวังออกมาเมื่อทำงาน Resharper ไม่ได้ให้คำแนะนำใด ๆ ในการแปลง foreach เป็น linq ดังนั้นนี่อาจจะดีเท่าที่มันจะทำแบบนี้

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);
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.