【问题标题】:Merge contents of multiple lists of custom objects - C#合并多个自定义对象列表的内容 - C#
【发布时间】:2023-03-13 12:15:01
【问题描述】:

我有一个类项目

public class Project 
{   public int ProjectId { get; set; }
    public string ProjectName { get; set; }
    public string Customer { get; set; }
    public string Address{ get; set; }
}

我有 3 个列表

List<Project> lst1; List<Project> lst2; List<Project> lst3;

lst1 包含具有 ProjectId 和 ProjectName 的 Person 对象。

ProjectId =1, ProjectName = "X", Customer = null, Address = null

ProjectId =2, ProjectName = "Y", Customer = null, Address = null

lst2 包含带有 ProjectId 和 Customer 的 Person 对象

ProjectId =1,ProjectName = null, Customer = "c1", Address = null

ProjectId =2,ProjectName = null, Customer = "c2", Address = null , 和

lst3 包含带有 ProjectId 和地址的 Person 对象

ProjectId = 1, ProjectName = null, Customer =null, Address = "a1"

ProjectId = 2, ProjectName = null, Customer =null, Address = "a2".

考虑到每个列表中有多个这样的记录,并且每个项目的 ProjectId 都是唯一的,我如何合并/组合这些列表以获得一个包含合并对象的列表

ProjectId=1, ProjectName="X", Customer="c1", address="a1"

ProjectId=2, ProjectName="Y", Customer="c2", address="a2"

我发现这些链接相似并尝试使用它但无法满足结果

Create a list from two object lists with linq

How to merge two lists using LINQ?

谢谢。

【问题讨论】:

  • 在每个列表中,是否总是(除了 projectId)恰好有一个属性不为空?
  • 还有预先知道的固定属性集吗?
  • @phg :对这两个问题都没有。这只是一个例子。并且列表可能包含不同数量的记录。

标签: c# list


【解决方案1】:

这可以通过多步骤方法非常简单地完成。首先,定义一个Func&lt;Project, Project, Project&gt; 来处理实际的记录合并。也就是说,您正在定义一个签名等效于public Project SomeMethod(Project p1, Project p2) 的方法。此方法实现了您上面概述的合并逻辑。接下来,我们将列表中的元素连接在一起,然后将它们按ProjectId 分组,使用我们的合并委托作为GroupBy 重载中的聚合函数,该函数接受结果选择器:

Func<Project, Project, Project> mergeFunc = (p1,p2) => new Project
    {
        ProjectId = p1.ProjectId,
        ProjectName = p1.ProjectName == null ? p2.ProjectName : p1.ProjectName,
        Customer = p1.Customer == null ? p2.Customer : p1.Customer,
        Address = p1.Address == null ? p2.Address : p1.Address    
    };

var output = lst1.Concat(lst2).Concat(lst3)
                 .GroupBy(x => x.ProjectId, (k, g) => g.Aggregate(mergeFunc)); 

这是对上述逻辑的快速而肮脏的测试以及输出:

List<Project> lst1; List<Project> lst2; List<Project> lst3;
lst1 = new List<Project> 
    {
        new Project { ProjectId = 1, ProjectName = "P1" },
        new Project { ProjectId = 2, ProjectName = "P2" },
        new Project { ProjectId = 3, ProjectName = "P3" }
    };
lst2 = new List<Project>
    {
        new Project { ProjectId = 1, Customer = "Cust1"},
        new Project { ProjectId = 2, Customer = "Cust2"},
        new Project { ProjectId = 3, Customer = "Cust3"}
    };
lst3 = new List<Project>
    {
        new Project { ProjectId = 1, Address = "Add1"},
        new Project { ProjectId = 2, Address = "Add2"},
        new Project { ProjectId = 3, Address = "Add3"}
    };

Func<Project, Project, Project> mergeFunc = (p1,p2) => new Project
    {
        ProjectId = p1.ProjectId,
        ProjectName = p1.ProjectName == null ? p2.ProjectName : p1.ProjectName,
        Customer = p1.Customer == null ? p2.Customer : p1.Customer,
        Address = p1.Address == null ? p2.Address : p1.Address    
    };

var output = lst1
    .Concat(lst2)
    .Concat(lst3)
    .GroupBy(x => x.ProjectId, (k, g) => g.Aggregate(mergeFunc));

IEnumerable<bool> assertedCollection = output.Select((x, i) => 
    x.ProjectId == (i + 1) 
    && x.ProjectName == "P" + (i+1) 
    && x.Customer == "Cust" + (i+1) 
    && x.Address == "Add" + (i+1));

Debug.Assert(output.Count() == 3);  
Debug.Assert(assertedCollection.All(x => x == true));

--- 输出---

IEnumerable<Project> (3 items)   
ProjectId ProjectName Customer Address 
1 P1 Cust1 Add1 
2 P2 Cust2 Add2 
3 P3 Cust3 Add3 

【讨论】:

    【解决方案2】:

    使用 Lookup 你可以这样做:

            List<Project> lst = lst1.Union(lst2).Union(lst3).ToLookup(x => x.ProjectId).Select(x => new Project()
            {
                ProjectId = x.Key,
                ProjectName = x.Select(y => y.ProjectName).Aggregate((z1,z2) => z1 ?? z2),
                Customer = x.Select(y => y.Customer).Aggregate((z1, z2) => z1 ?? z2),
                Address = x.Select(y => y.Address).Aggregate((z1, z2) => z1 ?? z2)
            }).ToList();
    

    【讨论】:

    • 完美运行。谢谢你。我想我需要搜索一下 ToLookup 是如何工作的。
    • 虽然此解决方案有效,但您无需在聚合之前投影属性。请参阅我的答案,了解如何以更具可读性的方式做同样的事情。查找也是可选的,在这种情况下大致相当于 GroupBy。
    【解决方案3】:

    我相信以下是 LINQ Join 的工作原理:

    var mergedProjects =
        lst1
            .Join(lst2,
                proj1 => proj1.ProjectID,
                proj2 => proj2.ProjectID,
                (proj1, proj2) => new { Proj1 = proj1, Proj2 = proj2 })
            .Join(lst3,
                pair => pair.Proj1.ProjectID,
                proj3 => proj3.ProjectID,
                (pair, proj3) => new Project
                {
                    ProjectID = proj3.ProjectID,
                    ProjectName = pair.Proj1.ProjectName,
                    Customer = pair.Proj2.Customer,
                    Address = proj3.Address
                });
    

    如果在所有三个列表中都找不到ProjectID,这将不会返回任何结果。

    如果这是一个问题,我认为您最好手动执行此操作而不是使用 LINQ。

    【讨论】:

    • 虽然这解决了 OP 问题,但它做得非常狭窄。这个解决方案似乎有点脆弱。 IMO,如果代码在操作中明确合并逻辑而不是假设 projXX.PropXX 以特定方式映射(不反对,顺便说一句,只是观察)会更好
    • @Josh 这个问题非常具体,关于来自给定列表的每个属性,而不是关于每个列表中存在的所有 ID。将具有给定 ID 的所有项目整理到一个匿名对象中,然后作为最后一步收集属性,而不是一个一个地收集属性,这样会更清楚。如果您有其他具有其他属性的列表,或者每个列表有多个属性,则这不是很可扩展,但原始问题中没有说明这些。
    • 总有 1...N 种剥猫皮的方法,对吧?您提出的公平观点。
    【解决方案4】:

    我假设列表包含相同数量的项目并按 ProjectId 排序。

    List<Project> lst1; List<Project> lst2; List<Project> lst3
    

    如果列表没有排序,可以先排序。

    list1.Sort(p => p.ProjectId);
    list2.Sort(p => p.ProjectId);
    list3.Sort(p => p.ProjectId);
    

    用于合并对象

    List<Project> list4 = new List<Project>();
    for(int i=1; i<list.Count; i++)
    { 
        list4.Add(new Project
        {
           ProjectId = list1[i].ProjectId;
           ProjectName = list1[i].ProjectName;
           Customer = list2[i].Customer;
           Address = list3[i].Address;
        });
    
    }
    

    【讨论】:

    • 如果项目都按正确的顺序,这是一种很好的简单处理方式。
    • 列表可能包含不同数量的记录。即使他们有相同的记录 ProjectId 也可能不同。
    • 那你会根据什么条件合并它们呢?
    • @Asif :每个列表都会有一些带有 ProjectId 的项目,因此对于每个 projectId,我想从不同的列表中收集数据并制作一个列表。
    【解决方案5】:

    虽然有点矫枉过正,但我​​还是很想把它作为一种扩展方法:

    public static List<T> MergeWith<T,TKey>(this List<T> list, List<T> other, Func<T,TKey> keySelector, Func<T,T,T> merge)
    {
        var newList = new List<T>();
        foreach(var item in list)
        {
            var otherItem = other.SingleOrDefault((i) => keySelector(i).Equals(keySelector(item)));
            if(otherItem != null)
            {
                newList.Add(merge(item,otherItem));
            }
        }
        return newList;
    }
    

    然后用法是:

    var merged = list1
         .MergeWith(list2, i => i.ProjectId,
           (lhs,rhs) => new Project{ProjectId=lhs.ProjectId,ProjectName=lhs.ProjectName, Customer=rhs.Customer})
        .MergeWith(list3,i => i.ProjectId,
           (lhs,rhs) => new Project{ProjectId=lhs.ProjectId,ProjectName=lhs.ProjectName, Customer=lhs.Customer,Address=rhs.Address});
    

    现场示例:http://rextester.com/ETIVB14254

    【讨论】:

    • 这非常适合我。谢谢你。只有一件事,为什么要矫枉过正?我的意思是性能还是什么?
    • 这太过分了,因为它是一个相对简单问题的复杂解决方案(@Jamiec - 你的意思是什么?)。此外,完成合并的方式将导致/可能导致同一查询表达式的多次执行。最后,还有其他运算符(即.Zip())复制了一些(但不是全部)此功能。
    • @JoshE - 差不多。我写得很快,只是知道会有更好的方法。
    【解决方案6】:

    这是假设您要获取第一个非空值,或恢复为默认值 - 在这种情况下为字符串为空。

    private static IEnumerable<Project> GetMergedProjects(IEnumerable<List<Project>> projects)
    {
        var projectGrouping = projects.SelectMany(p => p).GroupBy(p => p.ProjectId);
        foreach (var projectGroup in projectGrouping)
        {
            yield return new Project
                                {
                                    ProjectId = projectGroup.Key,
                                    ProjectName =
                                        projectGroup.Select(p => p.ProjectName).FirstOrDefault(
                                            p => !string.IsNullOrEmpty(p)),
                                    Customer =
                                        projectGroup.Select(c => c.Customer).FirstOrDefault(
                                            c => !string.IsNullOrEmpty(c)),
                                    Address =
                                        projectGroup.Select(a => a.Address).FirstOrDefault(
                                            a => !string.IsNullOrEmpty(a)),
                                };
        }
    }
    

    如果需要,您也可以将此作为扩展方法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-12
      相关资源
      最近更新 更多