简体   繁体   English

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

[英]Create a list from two object lists with linq

I have the following situation 我有以下情况

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

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

I need to combine the 2 lists into a new List<Person> in case it's the same person the combine record would have that name, value of the person in list2, change would be the value of list2 - the value of list1. 我需要将2个列表组合成一个新的List<Person> ,以防它是同一个人,组合记录将具有该名称,list2中的人的值,更改将是list2的值 - list1的值。 Change is 0 if no duplicate 如果没有重复,则更改为0

This can easily be done by using the Linq extension method Union. 这可以通过使用Linq扩展方法Union轻松完成。 For example: 例如:

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

This will return a List in which the two lists are merged and doubles are removed. 这将返回一个List,其中合并了两个列表,并删除了双精度数。 If you don't specify a comparer in the Union extension method like in my example, it will use the default Equals and GetHashCode methods in your Person class. 如果您没有像我的示例中那样在Union扩展方法中指定比较器,它将使用Person类中的默认Equals和GetHashCode方法。 If you for example want to compare persons by comparing their Name property, you must override these methods to perform the comparison yourself. 例如,如果您想通过比较其Name属性来比较人员,则必须覆盖这些方法以自行执行比较。 Check the following code sample to accomplish that. 检查以下代码示例以完成该操作。 You must add this code to your Person class. 您必须将此代码添加到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);
}

If you don't want to set the default Equals method of your Person class to always use the Name to compare two objects, you can also write a comparer class which uses the IEqualityComparer interface. 如果您不想将Person类的默认Equals方法设置为始终使用Name来比较两个对象,您还可以编写一个使用IEqualityComparer接口的比较器类。 You can then provide this comparer as the second parameter in the Linq extension Union method. 然后,您可以将此比较器作为Linq扩展联合方法中的第二个参数。 More information on how to write such a comparer method can be found on http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx 有关如何编写这种比较器方法的更多信息,请访问http://msdn.microsoft.com/en-us/library/system.collections.iequalitycomparer.aspx

I noticed that this question was not marked as answered after 2 years - I think the closest answer is Richards, but it can be simplified quite a lot to this: 我注意到这个问题在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 
    }));

Although this won't error in the case where you have duplicate names in either set. 虽然在任何一组中都有重复名称的情况下这不会出错

Some other answers have suggested using unioning - this is definitely not the way to go as it will only get you a distinct list, without doing the combining. 其他一些答案建议使用联合 - 这绝对不是要走的路,因为它只会让你得到一个独特的列表,而不进行组合。

Why you don't just use Concat ? 为什么你不只是使用Concat

Concat is a part of linq and more efficient than doing an AddRange() Concat是linq的一部分,比AddRange()更有效率

in your case: 在你的情况下:

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

This is Linq 这是Linq

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

This is Normaly (AddRange) 这是Normaly(AddRange)

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

This is Normaly (Foreach) 这是Normaly(Foreach)

var mergedList=new List<Person>();

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

This is Normaly (Foreach-Dublice) 这是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);
   }
}

There are a few pieces to doing this, assuming each list does not contain duplicates, Name is a unique identifier, and neither list is ordered. 这样做有几件事,假设每个列表不包含重复项,Name是唯一标识符,并且两个列表都没有排序。

First create an append extension method to get a single list: 首先创建一个追加扩展方法来获取单个列表:

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

Thus can get a single list: 因此可以得到一个列表:

var oneList = list1.Append(list2);

Then group on name 然后分组名称

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

Then can process each group with a helper to process one group at a time 然后可以使用帮助程序处理每个组以一次处理一个组

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

Which can be applied to each element of grouped : 哪个可以应用于grouped每个元素:

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

(Warning: untested.) (警告:未经测试。)

Does the following code work for your problem? 以下代码是否适用于您的问题? I've used a foreach with a bit of linq inside to do the combining of lists and assumed that people are equal if their names match, and it seems to print the expected values out when run. 我已经使用了一个带有一点linq的foreach来组合列表并假设人们在名字匹配时是相同的,并且它似乎在运行时打印出预期的值。 Resharper doesn't offer any suggestions to convert the foreach into linq so this is probably as good as it'll get doing it this way. 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();
   }
}

You need something like a full outer join. 你需要一个完整的外部联接。 System.Linq.Enumerable has no method that implements a full outer join, so we have to do it ourselves. 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();
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);
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM