Получить доступ к перечислителю в цикле foreach?
-
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);
}
}
}
Вы также можете сделать что-то подобное более функциональным способом, в зависимости от ваших требований.То, о чем вы просите, может быть, хотя и "архивированием" нескольких последовательностей, а затем повторением их всех сразу.Тремя последовательностями для приведенного вами примера будут:
- Последовательность "значение"
- Последовательность "индекс"
- Последовательность "Текущий итог"
Следующим шагом было бы указать каждую из этих последовательностей отдельно:
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.Это действительно небольшой фрагмент простого кода.