LINQ Query проекция из нескольких операторов from - PullRequest
1 голос
/ 12 июня 2019

Я создаю очень большой список объектов, которые представляют собой все возможные комбинации Min, Max и Increment для более чем одного набора коллекций.

Однако это работает, моя проблема в том, что я не могу легко исключить одну или несколько коллекций элементов без переписывания запроса LINQ.Порядок операторов 'from' в запросе определяется по порядку в переданном списке 'loop'.Запрос и окончательная проекция в список «Шаговая модель» в этом примере ожидают обработки трех наборов элементов.Хотя операторы 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 Ответ

1 голос
/ 12 июня 2019

То, что вы пытаетесь сделать здесь, можно описать на высоком уровне как перекрестное соединение из произвольного числа последовательностей .

Когда вы выполняете несколько предложений from в LINQ таким образом, это перекрестное соединение. Все, кроме первого, это звонок на 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 не захватывается лямбдами так же, как переменная диапазона в цикле 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 s, потому что это облегчит корреляцию их элементов с исходным списком, как я упоминал ранее.

Существует больше возможностей рефакторинга, например, извлечение выражения, переданного в 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();
    }
}

Надеюсь, это поможет.

...