使用Linq从两个对象列表创建一个列表

170

我有如下情况

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

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

我需要将这两个列表合并为一个新的List<Person>,如果是同一个人,则合并记录应该是该人的名字,列表2中的值,变化的值是列表2的值减去列表1的值。如果没有重复,则变化为0。


3
LINQ真的必要吗?一个带有一些类似LINQ表达式的好的foreach循环也可以实现同样的功能。 - Rashack
2
由于问题的标题和实际问题不匹配,因此添加此评论作为问题标题的版本:真正的答案是Mike的这个答案。大多数其他答案虽然有用,但实际上并没有解决原始发布者提出的问题。 - JoshuaTheMiller
8个回答

262

您可以使用Linq扩展方法Union轻松地完成此操作。例如:

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

这将返回一个List,其中合并了两个列表并删除了重复项。如果您不像我的示例那样在Union扩展方法中指定比较器,它将使用Person类中的默认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);
}

如果您不想将Person类的默认Equals方法设置为始终使用名称来比较两个对象,您也可以编写一个使用IEqualityComparer接口的比较器类。然后,您可以将此比较器作为Linq扩展Union方法中的第二个参数提供。有关如何编写此类比较器方法的更多信息,请参阅http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

14
我看不出这个回答与价值合并的问题有什么关系。 - Wagner Danda da Silva Filho
1
这不会有响应,Union将只包含两个集合中存在的项,而不包含任何一个列表中存在的元素。 - J4N
7
你是否把 UnionIntersect 搞混了? - Kos
14
供参考:还有一个不会合并重复项的 Concat - Kos
8
你介意编辑一下这个答案,让它真正回答问题吗?我觉得很荒谬,一个答案明明没有回答问题,只是回答了标题和一个基本的谷歌查询(“linq合并列表”),却获得了如此高的投票。 - Rawling
显示剩余3条评论

86

我注意到这个问题在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 
    }));

尽管在任何一个集合中有重复的名称时,这不会导致错误

其他一些答案建议使用union操作-这绝对不是正确的方法,因为它只会给你一个不同的列表,而不进行组合。


9
这篇文章实际上很好地回答了问题。 - philu
3
这应该是被采纳的答案。我从来没有看过有这么多人赞同一些与问题无关的回答! - Todd Menier
很好的答案。我可能会对它进行一些小改动,使Value实际上是来自list2的值,并且如果有重复项,则Change也会跟上:设置Value = p2.Value和Change = p1.Change + p2.Value - p1.Value - Ravi Desai

76

为什么不直接使用 Concat 呢?

Concat 是 Linq 的一部分,比使用 AddRange() 更高效。

在你的情况下:

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

14
你如何知道它更有效率? - Jerry Nixon
10
格雷格的评论:实际上,由于延迟执行,使用Concat可能会更快,因为它避免了对象分配——Concat不会复制任何内容,它只是在列表之间创建链接,所以当枚举并到达一个列表的末尾时,它会自动地将您带到下一个列表的开头!这就是我的观点。 - J4N
2
而且优点也在于,如果您使用实体框架,这可以在 SQL 侧而不是 C# 侧完成。 - J4N
4
这并没有帮助的真正原因在于它实际上没有合并两个列表中存在的任何对象。 - Mike Goatly
1
Concat函数只是简单地遍历每个列表。Union函数也是这样做的,但它还运行一个相等性检查并删除重复项。简而言之,如果您需要合并两个列表,请使用concat函数。如果您需要在同一操作中执行concat和distinct操作,请使用union函数。https://github.com/Microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs - Robear
显示剩余2条评论

19

这是Linq

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是唯一标识符,且两个列表均未排序,则需要完成以下几个步骤。
首先创建一个追加扩展方法以获取单个列表:
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循环,并在其中使用了一些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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接