从多个from语句进行LINQ查询投影

时间:2019-06-12 16:18:09

标签: c# linq projection

我正在创建一个很大的对象列表,这些对象是一组以上集合中的最小值,最大值和增量的所有可能组合。

但是它起作用了,但我的问题是,如果不重写LINQ查询,就无法轻易排除一个或多个项目集合。从传递的“循环”列表中的顺序可以推断出查询中“ from”语句的顺序。在此示例中,对“步骤模型”列表的查询和最终投影需要处理三个项目集。尽管'from'语句是LINQ,但它看起来仍然像是重复代码,可将自身插入For循环。考虑到在单个查询中结合了投影,我无法弄清楚该如何做。 注意:我可能会添加更多集合,使问题更加复杂!

public static IEnumerable<StepModel> CreateSteps(List<MinToMax> loops)
{
    var steps = 
       from item1 in Enumerable
          .Repeat(loops[0].Minimum, (loops[0].Maximum - loops[0].Minimum) / loops[0].Increment + 1)
          .Select((tr, ti) => tr + loops[0].Increment * ti)

       from item2 in Enumerable
           .Repeat(loops[1].Minimum, (loops[1].Maximum - loops[1].Minimum) / loops[1].Increment + 1)
           .Select((tr, ti) => tr + loops[1].Increment * ti)

       from item3 in Enumerable
           .Repeat(loops[2].Minimum, (loops[2].Maximum - loops[2].Minimum) / loops[2].Increment + 1)
           .Select((tr, ti) => tr + loops[2].Increment * ti)

   select new StepModel
   {
       ItemValues1 = new Step { Value = item1, IsActive = true },
       ItemValues2 = new Step { Value = item2, IsActive = true },
       ItemValues3 = new Step { Value = item3, IsActive = true },
   };

   return steps;
}

public class MinToMax
{
   public int Minimum { get; set; }
   public int Maximum { get; set; }
   public int Increment { get; set; }
   public bool IsActive { get; set; } = true;
}

public class Step
{
   public int Value { get; set; }
   public bool IsActive { get; set; } = true;
}

public class StepModel
{
   public Step ItemValues1 { get; set; }
   public Step ItemValues2 { get; set; }
   public Step ItemValues3 { get; set; }
}

public class ItemSteps
{
    public MinToMax ItemStep1 { get; } = new MinToMax();
    public MinToMax ItemStep2 { get; } = new MinToMax();
    public MinToMax ItemStep3 { get; } = new MinToMax();
}

public static List<MinToMax> GetValuesIntoSteps()
{
    var list = new List<MinToMax>();
    var itemValues = new ItemSteps();

    itemValues.ItemStep1.Minimum = 10;
    itemValues.ItemStep1.Maximum = 100;
    itemValues.ItemStep1.Increment = 10;

    if (itemValues.ItemStep1.IsActive)
    {
        list.Add(itemValues.ItemStep1);
    }

    itemValues.ItemStep2.Minimum = 3;
    itemValues.ItemStep2.Maximum = 30;
    itemValues.ItemStep2.Increment = 3;

    if (itemValues.ItemStep2.IsActive)
    {
        list.Add(itemValues.ItemStep2);
    }

    itemValues.ItemStep3.Minimum = 15;
    itemValues.ItemStep3.Maximum = 75;
    itemValues.ItemStep3.Increment = 5;

    if (itemValues.ItemStep3.IsActive)
    {
        list.Add(itemValues.ItemStep3);
    }

    return list;
}

1 个答案:

答案 0 :(得分:1)

您要在此处进行的操作可以概括地描述为任意数量的序列交叉联接

当您在LINQ中执行多个from子句时,这就是交叉联接。除了第一个以外,其他所有内容都是对SelectMany的调用。为了支持任意数量的输入,您需要遍历它们。不过,一个简单的foreach循环将无法正常工作,因为第一个序列的处理方式有所不同。

如何?两件事情。首先,没有可调用的SelectMany。如果在一个空列表上调用它,则会得到另一个空列表。因此,loops中的第一个元素将建立基本列表。我在下方中提供的实现以空序列开头,但是只有在loops中找不到任何元素的情况下,它才存在。找到第一个元素后,它将被替换。其次,至少在此实现中,您需要从第一个元素发出新列表,而随后的联接需要发出下一个元素并构造一个新列表。

要记住的另一件事是,您需要一个可以处理任意数量元素的结果类型。为此,我选择了IEnumerable<List<Step>> -我选择List<Step>作为元素类型,因为每个列表的每个索引都将与loops参数的索引相关联,因此您可以将它们都索引直。例如,每个结果的元素[5]将来自loops[5]

为实现这一点,我直接使用foreach编写了一个IEnumerator<T>循环的等效内容。对于loops中的第一个元素,它为要返回的每个结果构造一个Step对象的列表。

对于loops中的每个后续项,它使用SelectMany进行交叉连接,并将它们聚合到包含左侧元素和右侧元素的新列表中。

枚举器公开Current属性。这样可以将迭代从绑定到给定的索引中解放出来。您会注意到current曾经在loops[n]处使用过。

值得注意的是,必须强制调用ToList,因为current变量不会被lambda捕获,就像foreach循环中的范围变量会被捕获一样。

这是它的外观:

public static IEnumerable<List<Step>> CreateSteps(IEnumerable<MinToMax> loops)
{
    IEnumerable<List<Step>> sequence = Enumerable.Empty<List<Step>>();

    using (IEnumerator<MinToMax> enumerator = loops.GetEnumerator())
    {
        if (enumerator.MoveNext())
        {
            MinToMax current = enumerator.Current;

            sequence = Enumerable
                .Repeat(current.Minimum, (current.Maximum - current.Minimum) / current.Increment + 1)
                .Select((tr, ti) => new List<Step>() { new Step() { Value = tr + current.Increment * ti, IsActive = true } })
                .ToList();

            while (enumerator.MoveNext())
            {
                current = enumerator.Current;

                sequence = sequence
                    .SelectMany(
                        ctx => Enumerable
                            .Repeat(current.Minimum, (current.Maximum - current.Minimum) / current.Increment + 1)
                            .Select((tr, ti) => new Step() { Value = tr + current.Increment * ti, IsActive = true }),
                        (list, value) => new List<Step>(list) { value }
                        )
                    .ToList();
            }
        }
    }

    return sequence;
}

我也将loops参数更改为IEnumerable<MinToMax>,因为关于该方法如何使用的任何要求都不需要将其作为列表。另一方面,我将返回序列中的项目保留为List,因为它使将它们的元素与源列表相关联变得更加容易,如我之前提到的。

还有更多的重构可以完成,例如将传递给Enumerable.Repeat的表达式提取到一种方法中,以便可以重用。

我没有看到您的使用方式,因此我将其快速地投入使用。对于您提供的loops中的三个元素,这两种实现方式都显示出相同的结果,并且对于我的两个和四个输入,似乎都显示出正确的结果。

static void Main(string[] args)
{
    List<MinToMax> loops = GetValuesIntoSteps();

    foreach (List<Step> loop in CreateSteps(loops))
    {
        foreach (Step step in loop)
        {
            Console.Write($"{step.Value} ");
        }
        Console.WriteLine();
    }
}

我希望有帮助。