3

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

为简单起见(这不是确切的业务案例),假设这些属性是CurrentIndexand RunningTotal

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

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

4

3 回答 3

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 对象让您可以控制迭代器的执行方式。我从这个问题中得到的印象是,您需要某种方式来访问迭代器的内部工作,以便您可以在迭代过程中操纵它的迭代方式,但如果不是这种情况,那么这个解决方案可能对你需要的东西太过分了。

于 2009-12-01T21:10:51.250 回答
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);
        }
    }
}
于 2009-12-01T21:03:07.253 回答
0

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

  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 功能的源代码。这确实是一段简单的代码。

于 2009-12-01T22:11:10.433 回答