来自多个 from 语句的 LINQ 查询投影

LINQ Query projection from multiple from statements

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

但是它有效,我的问题是我无法在不重写 LINQ 查询的情况下轻松排除一个或多个项目集合。查询中 'from' 语句的顺序是从传入的 'loops' 列表中的顺序推断出来的。在此示例中,查询和最终投影到 'Step model' 列表中,需要处理三个项目集。尽管 '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;
}

您在此处尝试执行的操作可以概括地描述为任意数量的序列交叉连接

当您像这样在 LINQ 中执行多个 from 子句时,这就是交叉连接。除了第一个以外,其他都是对 SelectMany 的调用。为了支持任意数量的输入,您需要遍历它们。但是,一个简单的 foreach 循环不会完全起作用,因为第一个序列需要以不同的方式处理。

有什么不同?两件事情。首先,没有什么可以调用 SelectMany。如果你在一个空列表上调用它,你会得到另一个空列表。因此 loops 中的第一个元素建立了基本列表。我在下面提供的实现 确实 以一个空序列开始,但只有在 loops 中找不到元素的情况下才会存在。当找到第一个元素时,它会被替换。其次,至少在此实现中,您需要从第一个元素发出一个新列表,而后续连接需要发出下一个元素并构建一个新列表。

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

为了做到这一点,我直接使用 IEnumerator<T> 编写了相当于 foreach 循环的代码。对于 loops 中的第一个元素,它为每个要 return.

的结果构造一个 Step 对象列表

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

枚举器公开了一个 Current 属性。这将迭代从绑定到给定索引中解放出来。您会注意到 current 变量被用在 loops[n] 曾经是的地方。

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

这是它的样子:

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> 因为关于该方法如何使用它并没有要求它是一个列表。另一方面,我将 returned 序列中的项目保留为 Lists,因为如前所述,这样可以更轻松地将它们的元素与源列表相关联。

可以进行更多的重构,例如将传递给 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();
    }
}

希望对您有所帮助。