Получить доступ к перечислителю в цикле foreach?

StackOverflow https://stackoverflow.com/questions/1828881

  •  11-09-2019
  •  | 
  •  

Вопрос

У меня есть класс List, и я хотел бы переопределить GetEnumerator() чтобы вернуть мой собственный класс Enumerator.Этот класс Enumerator будет иметь два дополнительных свойства, которые будут обновляться по мере использования Enumerator.

Для простоты (это не точный бизнес-пример), предположим, что эти свойства были CurrentIndex и RunningTotal.

Я мог бы управлять этими свойствами в цикле foreach вручную, но я бы предпочел инкапсулировать эту функциональность для повторного использования, и перечислитель, похоже, является подходящим местом.

Проблема: предначертание скрывает весь бизнес перечислителя, так есть ли способ в операторе foreach получить доступ к текущему перечислителю, чтобы я мог получить свои свойства?Или мне пришлось бы использовать foreach, использовать неприятный старый цикл while и самому манипулировать перечислителем?

Это было полезно?

Решение

Строго говоря, я бы сказал, что если вы хотите сделать именно то, что говорите, то да, вам нужно было бы вызвать GetEnumerator и самостоятельно управлять перечислителем с помощью цикла while.

Не зная слишком много о требованиях вашего бизнеса, вы могли бы воспользоваться преимуществами функции итератора, например, чем-то вроде этого:

    public static IEnumerable<decimal> IgnoreSmallValues(List<decimal> list)
    {
        decimal runningTotal = 0M;
        foreach (decimal value in list)
        {
            // if the value is less than 1% of the running total, then ignore it
            if (runningTotal == 0M || value >= 0.01M * runningTotal)
            {
                runningTotal += value;
                yield return value;
            }
        }
    }

Тогда вы можете сделать это:

        List<decimal> payments = new List<decimal>() {
            123.45M,
            234.56M,
            .01M,
            345.67M,
            1.23M,
            456.78M
        };

        foreach (decimal largePayment in IgnoreSmallValues(payments))
        {
            // handle the large payments so that I can divert all the small payments to my own bank account.  Mwahaha!
        }

Обновленный:

Итак, вот продолжение того, что я назвал своим решением "рыболовный крючок".Теперь позвольте мне добавить оговорку о том, что я действительно не могу придумать веской причины для того, чтобы сделать что-то таким образом, но ваша ситуация может отличаться.

Идея заключается в том, что вы просто создаете объект "рыболовный крючок" (ссылочный тип), который вы передаете своей функции-итератору.Функция iterator манипулирует вашим объектом fishing hook , и поскольку у вас все еще есть ссылка на него в вашем внешнем коде, вы можете видеть, что происходит:

    public class FishingHook
    {
        public int Index { get; set; }
        public decimal RunningTotal { get; set; }
        public Func<decimal, bool> Criteria { get; set; }
    }

    public static IEnumerable<decimal> FishingHookIteration(IEnumerable<decimal> list, FishingHook hook)
    {
        hook.Index = 0;
        hook.RunningTotal = 0;
        foreach(decimal value in list)
        {
            // the hook object may define a Criteria delegate that
            // determines whether to skip the current value
            if (hook.Criteria == null || hook.Criteria(value))
            {
                hook.RunningTotal += value;
                yield return value;
                hook.Index++;
            }
        }
    }

Вы бы использовали это следующим образом:

        List<decimal> payments = new List<decimal>() {
            123.45M,
            .01M,
            345.67M,
            234.56M,
            1.23M,
            456.78M
        };

        FishingHook hook = new FishingHook();

        decimal min = 0;
        hook.Criteria = x => x > min; // exclude any values that are less than/equal to the defined minimum
        foreach (decimal value in FishingHookIteration(payments, hook))
        {
            // update the minimum
            if (value > min) min = value;

            Console.WriteLine("Index: {0}, Value: {1}, Running Total: {2}", hook.Index, value, hook.RunningTotal);
        }
        // Resultint output is:
        //Index: 0, Value: 123.45, Running Total: 123.45
        //Index: 1, Value: 345.67, Running Total: 469.12
        //Index: 2, Value: 456.78, Running Total: 925.90
        // we've skipped the values .01, 234.56, and 1.23

По сути, объект FishingHook дает вам некоторый контроль над тем, как выполняется итератор.Впечатление, которое у меня сложилось из этого вопроса, состояло в том, что вам нужен был какой-то способ получить доступ к внутренней работе итератора, чтобы вы могли манипулировать тем, как он выполняет итерации, пока вы находитесь в середине итерации, но если это не так, то это решение может быть излишним для того, что вам нужно.

Другие советы

С foreach вы действительно не можете получить перечислитель - однако вы могли бы вернуть перечислитель (yield) кортеж , который включает в себя эти данные;на самом деле, вы, вероятно, могли бы использовать LINQ, чтобы сделать это за вас...

(Я не мог чисто получить индекс с помощью LINQ - можно получить общее и текущее значение с помощью Aggregate, хотя;итак, вот подход к кортежу)

using System.Collections;
using System.Collections.Generic;
using System;
class MyTuple
{
    public int Value {get;private set;}
    public int Index { get; private set; }
    public int RunningTotal { get; private set; }
    public MyTuple(int value, int index, int runningTotal)
    {
        Value = value; Index = index; RunningTotal = runningTotal;
    }
    static IEnumerable<MyTuple> SomeMethod(IEnumerable<int> data)
    {
        int index = 0, total = 0;
        foreach (int value in data)
        {
            yield return new MyTuple(value, index++,
                total = total + value);
        }
    }
    static void Main()
    {
        int[] data = { 1, 2, 3 };
        foreach (var tuple in SomeMethod(data))
        {
            Console.WriteLine("{0}: {1} ; {2}", tuple.Index,
                tuple.Value, tuple.RunningTotal);
        }
    }
}

Вы также можете сделать что-то подобное более функциональным способом, в зависимости от ваших требований.То, о чем вы просите, может быть, хотя и "архивированием" нескольких последовательностей, а затем повторением их всех сразу.Тремя последовательностями для приведенного вами примера будут:

  1. Последовательность "значение"
  2. Последовательность "индекс"
  3. Последовательность "Текущий итог"

Следующим шагом было бы указать каждую из этих последовательностей отдельно:

List<decimal> ValueList
var Indexes = Enumerable.Range(0, ValueList.Count)

Последний вариант более веселый...два метода, которые я могу придумать, заключаются либо во временной переменной, используемой для суммирования последовательности, либо для пересчета суммы для каждого элемента.Второй, очевидно, гораздо менее эффективен, я бы предпочел использовать временный:

decimal Sum = 0;
var RunningTotals = ValueList.Select(v => Sum = Sum + v);

Последним шагом было бы собрать все это вместе..Net 4 будет иметь Встроенный Zip-оператор, и в этом случае это будет выглядеть примерно так:

var ZippedSequence = ValueList.Zip(Indexes, (value, index) => new {value, index}).Zip(RunningTotals, (temp, total) => new {temp.value, temp.index, total});

Очевидно, что это становится шумнее, чем больше вещей вы пытаетесь застегнуть вместе.

В последней ссылке есть исходный код для самостоятельной реализации функции Zip.Это действительно небольшой фрагмент простого кода.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top