Створіть список із двох списків об’єктів за допомогою linq


161

У мене така ситуація

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

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

Мені потрібно об'єднати два списки в новий, List<Person> якщо це одна і та сама особа, що записує комбінат, матиме це ім'я, значення особи в списку2, а зміна буде значенням list2 - значенням list1. Зміна 0, якщо немає дублікату


2
Чи справді потрібна linq - хороший передрік з трохи виразів linq-ish також міг би зробити.
Рашак

1
Додавання цього коментаря як версії заголовка питання та фактичного питання не збігалося: справжня відповідь на це - це відповідь Майка . Більшість інших відповідей, хоча й корисні, насправді не вирішують проблему, представлену оригінальним плакатом.
Джошуа

Відповіді:


254

Це легко зробити за допомогою методу розширення Linq Union. Наприклад:

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

Це поверне Список, у якому два списки об'єднані, а подвійні вилучені. Якщо ви не вказали порівняльник у методі розширення Union, як у моєму прикладі, він використовуватиме методи за замовчуванням Equals та GetHashCode у вашому класі Person. Якщо ви, наприклад, хочете порівняти людей, порівнюючи їх властивість Name, ви повинні перекрити ці методи, щоб виконати порівняння самостійно. Перевірте наступний зразок коду, щоб досягти цього. Ви повинні додати цей код до свого класу 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);
}

Якщо ви не хочете встановлювати за замовчуванням метод вашого класу Person завжди використовувати Ім'я для порівняння двох об'єктів, ви також можете написати клас порівняння, який використовує інтерфейс IEqualityComparer. Потім ви можете надати цей порівняльник як другий параметр у способі з'єднання розширення Linq. Більш детальну інформацію про те, як написати такий метод порівняння, можна знайти на веб- сайті http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx


10
Я не бачу, як це відповідає на питання про злиття цінностей.
Вагнер да Сілва

1
Це не відповідає, Union містить лише предмети, присутні в двох наборах, не будь-який елемент, присутній в одному з двох списків
J4N

7
@ J4N ви , можливо , плутаючи Unionз Intersect?
Кос

11
Для довідки: є і Concatте, що не зливає дублікати
Кос

7
Ви б не хотіли відредагувати цю відповідь, щоб вона насправді відповідала на питання? Мені здається смішним, що відповідь настільки високо голосується, незважаючи на те, що вона не відповідає на питання, лише тому, що відповідає заголовку та базовому запиту Google ("списки злиття linq").
Роулінг

78

Я помітив, що це питання не було позначене як відповідь через 2 роки - я вважаю, що найближча відповідь - Річардс, але це може бути досить спрощено до цього:

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
Цей пост насправді відповідає на питання, і це добре.
філу

3
Це має бути прийнятою відповіддю. Ніколи не бачив запитання з такою кількістю відгуків, які не відповідають на поставлене запитання!
Тодд Меньє

Гарна відповідь. Я можу внести до нього одну невелику зміну, тому значення насправді є значенням зі списку2, і так, щоб Зміни продовжували роботу, якщо у вас є дублікати: встановити значення = p2.Value та Change = p1.Change + p2.Value - p1.Value
Раві Дезай

70

Чому ви не просто використовуєте Concat?

Concat є частиною linq та більш ефективним, ніж робити AddRange()

у вашому випадку:

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

13
Як ви знаєте, що це ефективніше?
Джеррі Ніксон

@Jerry Nixon Він / вона цього не перевіряв, але пояснення здається логічним. stackoverflow.com/questions/1337699 / ...
Nullius

9
stackoverflow.com/questions/100196/net-listt-concat-vs-addrange -> Коментар Грега: 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
Справжня причина, чому це не допомагає, полягає в тому, що він насправді не об'єднує жоден із об’єктів, присутніх в обох списках.
Майк Козлі

15

Це Лінк

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

Це нормально (AddRange)

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

Це нормально (Foreach)

var mergedList=new List<Person>();

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

Це нормально (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: Це я чомусь не зник Enumerable.Concatі таким чином повторно реалізував це.
Річард

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 з трохи linq всередині, щоб зробити комбінування списків і припустив, що люди рівні, якщо їх імена збігаються, і, здається, друкують очікувані значення під час запуску. 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);
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.