Доступ к Enumerator в цикле foreach? - PullRequest
2 голосов
/ 01 декабря 2009

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

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

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

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

Ответы [ 3 ]

4 голосов
/ 02 декабря 2009

Строго говоря, я бы сказал, что если вы хотите сделать именно то, что говорите, то да, вам нужно будет вызвать 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!
        }

Обновлено:

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

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

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

1 голос
/ 02 декабря 2009

С 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);
        }
    }
}
0 голосов
/ 02 декабря 2009

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

  1. Последовательность "значение"
  2. последовательность "index"
  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. Это действительно немного кода.

...