在foreach循环中访问枚举器?

时间:2009-12-01 20:58:10

标签: c# foreach ienumerator

我有一个List类,我想覆盖GetEnumerator()以返回我自己的Enumerator类。此Enumerator类将具有两个附加属性,这些属性将在使用Enumerator时更新。

为简单起见(这不是确切的商业案例),我们假设这些属性为CurrentIndexRunningTotal

我可以手动管理foreach循环中的这些属性,但我宁愿封装此功能以便重用,而Enumerator似乎是正确的位置。

问题: foreach 隐藏了所有Enumerator业务,所以有没有办法在foreach语句中访问当前的Enumerator,以便我可以检索我的属性?或者我是否必须使用一个令人讨厌的旧while循环,并自己操纵Enumerator?

3 个答案:

答案 0 :(得分:4)

严格地说,如果你想完全按照你所说的去做,那么是的,你需要调用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 :(得分:1)

使用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);
        }
    }
}

答案 2 :(得分:0)

根据您的要求,您也可以采用更加实用的方式执行此类操作。你问的问题可能是将多个序列“压缩”在一起,然后一次性迭代它们。您给出的示例的三个序列将是:

  1. “价值”序列
  2. “索引”序列
  3. “运行总计”序列
  4. 下一步是单独指定每个序列:

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

    最后一个更有趣......我能想到的两种方法是要么有一个临时变量用于总结序列,要么重新计算每个项目的总和。第二个显然要低得多,我宁愿使用临时的:

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

    最后一步是将这些拼接在一起。 .Net 4将有Zip operator built in,在这种情况下它将如下所示:

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

    你尝试拉链的东西越多越明显越吵。

    在最后一个链接中,有自己实现Zip功能的源代码。它真的只是一点点代码。