【问题标题】:Merge many Lists into one list with properties for each list将多个列表合并到一个列表中,每个列表都有属性
【发布时间】:2021-01-10 22:28:14
【问题描述】:

为了展示我想要实现的目标,我准备了一个简化的示例:

假设我有一个这样的对象:

public class SoldProduct
{
    public string Name { get; set; }
    public DateTime Date { get; set; }
}

然后,在某处我有一个方法可以获取SoldProduct(来自不同类别)的不同列表,如下所示:

List<SoldProduct> toolsSold = GetToolSales();
List<SoldProduct> materialsSold = GetMaterialSales();
List<SoldProduct> foodsSold = GetFoodSales();

现在,我需要将它们合并到一个行列表中,其中一行是这样的:

public class SoldProductRow
{
    public SoldProduct ToolSold { get; set; }
    public SoldProduct MaterialSold { get; set; }
    public SoldProduct FoodSold { get; set; }
}

问题:

那么,如何将这 3 个列表转换为 List&lt;SoldProductRow&gt;?是否有一些简单、高效的 LINQ 查询?也许有一种非常简单有效的手动编写方式?

让我们假设这一行类似于 Excel 行;我们希望有 2 列用于工具销售的名称和日期,2 列用于材料销售的名称和日期,以及 2 列用于食品销售的名称和日期。 基本上并排列出它们。

困难:

列表可以有不同的长度,所以如果我们在 toolsSold 中有 3 个元素,但在 materialsSoldfoodsSold 中有 5 个元素,那么行列表应该有 5 个元素(最长列表长度),其中前 3 个元素行列表将具有所有属性,但第 4 和第 5 个元素应在 ToolSold 中具有 null 并在 MaterialSoldFoodSold 中具有适当的值

List&lt;SoldProduct&gt; 的长度通常在 1 到 3 之间,但实际上 List&lt;Parent&gt; 可能有数百个对象,每个父级有 5 个 List&lt;SoldProduct&gt; 所以,我需要创建数百行。

示例:

如果我有清单:

toolsSold: [
  {Name: hammer, Date:24-09-2020}
]

materialsSold: [
  {Name: wood, Date:23-09-2020},
  {Name: steel, Date:17-08-2020}
]

foodsSold: [
  {Name: apple, Date:07-02-2020}
]

我想要这个结果(2 行,因为最长的列表有 2 个实体):

SoldProductsRows: [
  {
     ToolSold: {Name: hammer, Date:24-09-2020}
     MaterialSold: {Name: wood, Date:23-09-2020}
     FoodSold: {Name: apple, Date:07-02-2020}
  },
  {
     ToolSold: null
     MaterialSold: {Name: steel, Date:17-08-2020}
     FoodSold: null
  },
]

【问题讨论】:

  • 不同列表之间有什么关系?产品名称?还是日期?我们如何知道“行”中的内容是什么?
  • 它们没有关系,这就是为什么在行类中每个列表都有属性。
  • 我添加了类似 json 的示例,以更好地展示我想要实现的目标

标签: c# linq .net-core


【解决方案1】:

这是一个没有 LINQ 的简单解决方案:

int count = Math.Max(Math.Max(toolsSold.Count, materialsSold.Count), foodsSold.Count);

var rowsList = new List<SoldProductRow>();
for (int i = 0; i < count; i++)
{
    rowsList.Add(new SoldProductRow
    {
        ToolSold = toolsSold.ElementAtOrDefault(i),
        MaterialSold = materialsSold.ElementAtOrDefault(i),
        FoodSold = foodsSold.ElementAtOrDefault(i)
    });
}

步骤:

  • 使用Math.Max() 获取最长列表的计数。
  • 如果存在,使用ElementAtOrDefault()扩展方法获取特定索引处的每个列表的元素;如果没有,请使用null
  • 使用上一步中的值创建一个新的SolidProductRow 对象并将其添加到最终列表中。

Try it online.

【讨论】:

  • 类似i &lt; toolsSold.Count ? toolsSold[i] : null 的性能会与使用.ElementAtOrDefault 相同,性能更差还是更好?
  • 我会分两点回答你:1)性能几乎相同,因为当源是IList(例如List&lt;T&gt;或数组时,你提出的是exactly what ElementAtOrDefault() does internally )。 2) 每当您发现自己问“哪个更快”时,您应该仔细阅读this amazing article 中提到的问题列表。为什么?因为通常情况下,“哪个更快”的答案并不重要。
【解决方案2】:

如果你绝对想使用 LINQ,你可以这样做:

int max = new[] { toolsSold.Count, materialsSold.Count, foodsSold.Count }.Max();
List<SoldProductRow> result = 
    toolsSold.Concat(Enumerable.Repeat(null, max - toolsSold.Length))
        .Zip(materialsSold.Concat(Enumerable.Repeat(null, max - materialsSold.Length)))
        .Zip(foodsSold.Concat(Enumerable.Repeat(null, max - foodsSold.Length)))
        .Select(x => new SoldProductRow
        {
            ToolSold = x.First.First,
            MaterialSold = x.First.Second,
            FoodSold = x.Second
        })
        .ToList();

【讨论】:

    【解决方案3】:

    LINQ 解决方案是可行的,但正如my other answer 中所展示的,for 循环在这种特殊情况下看起来更具可读性。我能想到的最简单的 LINQ 解决方案是:

    var rowsList2 =
        Enumerable.Range(0, count)
        .Select(i => new SoldProductRow
        {
            ToolSold = toolsSold.ElementAtOrDefault(i),
            MaterialSold = materialsSold.ElementAtOrDefault(i),
            FoodSold = foodsSold.ElementAtOrDefault(i)
        }).ToList();
    

    ..这基本上是一个变相的循环。

    您也可以使用修改过的“特殊”版本的Zip() 来获得相同的结果,但这会变得不必要地复杂。如果您要经常使用它,您可以创建一个可重用的通用扩展方法。只是为了好玩,我想出了以下方法1

    public static IEnumerable<TResult> ZipThreeWithDefaults<TFirst, TSecond, TThird, TResult>(
        this IEnumerable<TFirst> first,
        IEnumerable<TSecond> second,
        IEnumerable<TThird> third,
        Func<TFirst, TSecond, TThird, TResult> func)
    {
        using (var e1 = first.GetEnumerator())
        using (var e2 = second.GetEnumerator())
        using (var e3 = third.GetEnumerator())
        {
            while (e1.MoveNext())
            {
                var current2 = (e2.MoveNext() ? e2.Current : default(TSecond));
                var current3 = (e3.MoveNext() ? e3.Current : default(TThird));
    
                yield return func(e1.Current, current2,current3);
            }
            while (e2.MoveNext())
            {
                var current3 = (e3.MoveNext() ? e3.Current : default(TThird));
                yield return func(default(TFirst), e2.Current, current3);
            }
            while (e3.MoveNext())
            {
                yield return func(default(TFirst), default(TSecond), e3.Current);
            }
        }
    }
    

    用法:

    var rowsList3 = 
        toolsSold.ZipThreeWithDefaults(materialsSold, 
                                       foodsSold, (t, m, f) => new SoldProductRow
        {
            ToolSold = t,
            MaterialSold = m,
            FoodSold = f
        }).ToList();
    

    这是try online的完整示例。


    1灵感来自this answerMarc Gravell

    【讨论】:

      猜你喜欢
      • 2021-10-12
      • 1970-01-01
      • 2019-01-18
      • 2017-02-08
      • 2017-10-26
      • 1970-01-01
      • 2021-06-02
      • 2020-05-01
      • 1970-01-01
      相关资源
      最近更新 更多