Создать список из двух списков объектов с помощью linq

161

У меня следующая ситуация

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

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

Мне нужно объединить 2 списка в новый List<Person> на тот случай, если это будет тот же человек, у которого объединенная запись будет иметь это имя, значение человека в list2, изменение будет значением list2 - значением list1. Изменение равно 0, если нет дубликатов

ΩmegaMan
источник
2
Является ли linq действительно необходимым - хороший foreach с небольшим количеством выражений 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);
}

Если вы не хотите устанавливать метод Equals по умолчанию в своем классе 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 merge lists»).
Роулинг
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
Это должен быть принятый ответ. Никогда не видел вопрос с таким большим количеством голосов за ответы, которые не отвечают на заданный вопрос!
Тодд
Хороший ответ. Я мог бы внести одно небольшое изменение в него, так что значение на самом деле является значением из list2, и поэтому изменение сохраняется, если у вас есть дубликаты: Set Value = 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);
J4N
источник
13
Как вы знаете, это более эффективно?
Джерри Никсон
@ Джерри Никсон Он / она не проверял это, но объяснение кажется логичным. stackoverflow.com/questions/1337699/…
Нуллий
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

Для этого есть несколько частей, при условии, что каждый список не содержит дубликатов, Name является уникальным идентификатором, и ни один список не упорядочен.

Сначала создайте метод расширения append, чтобы получить единый список:

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);
    }
}
pungggi
источник