У меня есть последовательность элементов. Последовательность может быть повторена только один раз и может быть "бесконечной".
Каков наилучший способ получить голову и хвост такой последовательности?
У меня есть последовательность элементов. Последовательность может быть повторена только один раз и может быть "бесконечной".
Каков наилучший способ получить голову и хвост такой последовательности?
Разложение IEnumerable<T>
на голову и хвост не особенно хорошо подходит для рекурсивной обработки (в отличие от функциональных списков), потому что, когда вы используете операцию хвоста рекурсивно, вы создадите ряд косвенностей. Однако вы можете написать примерно следующее:
Я игнорирую такие вещи, как проверка аргументов и обработка исключений, но это показывает идею...
Tuple<T, IEnumerable<T>> HeadAndTail<T>(IEnumerable<T> source) {
// Get first element of the 'source' (assuming it is there)
var en = source.GetEnumerator();
en.MoveNext();
// Return first element and Enumerable that iterates over the rest
return Tuple.Create(en.Current, EnumerateTail(en));
}
// Turn remaining (unconsumed) elements of enumerator into enumerable
IEnumerable<T> EnumerateTail<T>(IEnumerator en) {
while(en.MoveNext()) yield return en.Current;
}
Метод HeadAndTail
получает первый элемент и возвращает его как первый элемент кортежа. Второй элемент кортежа IEnumerable<T>
, который генерируется из оставшихся элементов (путем итерации по остальной части перечислителя, которую мы уже создали).
В то время как другие подходы предлагают использовать yield return
для перечислимого tail
, такой подход добавляет ненужные накладные расходы. Лучшим подходом было бы преобразование Enumerator<T>
обратно в то, что можно использовать с foreach
:
public struct WrappedEnumerator<T>
{
T myEnumerator;
public T GetEnumerator() { return myEnumerator; }
public WrappedEnumerator(T theEnumerator) { myEnumerator = theEnumerator; }
}
public static class AsForEachHelper
{
static public WrappedEnumerator<IEnumerator<T>> AsForEach<T>(this IEnumerator<T> theEnumerator) {return new WrappedEnumerator<IEnumerator<T>>(theEnumerator);}
static public WrappedEnumerator<System.Collections.IEnumerator> AsForEach(this System.Collections.IEnumerator theEnumerator)
{ return new WrappedEnumerator<System.Collections.IEnumerator>(theEnumerator); }
}
Если бы использовались отдельные структуры WrappedEnumerator
для общего IEnumerable<T>
и non-generic IEnumerable
, их можно было бы реализовать IEnumerable<T>
и IEnumerable
соответственно; они не будут действительно подчиняться контракту IEnumerable<T>
, хотя, который указывает, что должно быть возможно вызывать GetEnumerator()
несколько раз, причем каждый вызов возвращает независимый счетчик.
Еще одно важное предостережение состоит в том, что если вы используете AsForEach
на IEnumerator<T>
, результирующий WrappedEnumerator
следует перечислить ровно один раз. Если он никогда не перечисляется, базовый IEnumerator<T>
никогда не будет вызывать метод Dispose
.
Применяя приведенные выше методы к данной проблеме, было бы легко вызвать GetEnumerator()
на IEnumerable<T>
, зачитать первые несколько элементов, а затем использовать AsForEach()
для преобразования остатка, чтобы он мог использовать с циклом foreach
(или, возможно, как указано выше), преобразовать его в реализацию IEnumerable<T>
). Важно отметить, однако, что вызов GetEnumerator()
создает обязательство Dispose
результирующего IEnumerator<T>
, а класс, который выполняет разделение head/tail, не имеет никакого способа сделать это, если ничего не называет GetEnumerator()
on хвост.
Очевидно, что каждый вызов HeadAndTail должен повторять последовательность снова (если только не используется какое-то кеширование). Например, рассмотрим следующее:
var a = HeadAndTail(sequence);
Console.WriteLine(HeadAndTail(a.Tail).Tail);
//Element #2; enumerator is at least at #2 now.
var b = HeadAndTail(sequence);
Console.WriteLine(b.Tail);
//Element #1; there is no way to get #1 unless we enumerate the sequence again.
По той же причине HeadAndTail не может быть реализована как отдельный метод Голова и Хвост (если вы не хотите даже первого вызова Хвост, чтобы снова перечислить последовательность, даже если она уже была переименована вызовом Голова).
Кроме того, HeadAndTail не должен возвращать экземпляр IEnumerable (как его можно было бы перечислить несколько раз).
Это оставляет нам единственный вариант: HeadAndTail должен возвращать IEnumerator, и, чтобы сделать вещи более очевидными, он должен принимать IEnumerator как хорошо (мы просто перемещаем вызов GetEnumerator изнутри HeadAndTail вовне, чтобы подчеркнуть, что он используется только один раз).
Теперь, когда мы разработали требования, реализация довольно проста:
class HeadAndTail<T> {
public readonly T Head;
public readonly IEnumerator<T> Tail;
public HeadAndTail(T head, IEnumerator<T> tail) {
Head = head;
Tail = tail;
}
}
static class IEnumeratorExtensions {
public static HeadAndTail<T> HeadAndTail<T>(this IEnumerator<T> enumerator) {
if (!enumerator.MoveNext()) return null;
return new HeadAndTail<T>(enumerator.Current, enumerator);
}
}
И теперь его можно использовать следующим образом:
Console.WriteLine(sequence.GetEnumerator().HeadAndTail().Tail.HeadAndTail().Head);
//Element #2
Или в рекурсивных функциях, подобных этому:
TResult FoldR<TSource, TResult>(
IEnumerator<TSource> sequence,
TResult seed,
Func<TSource, TResult, TResult> f
) {
var headAndTail = sequence.HeadAndTail();
if (headAndTail == null) return seed;
return f(headAndTail.Head, FoldR(headAndTail.Tail, seed, f));
}
int Sum(IEnumerator<int> sequence) {
return FoldR(sequence, 0, (x, y) => x+y);
}
var array = Enumerable.Range(1, 5);
Console.WriteLine(Sum(array.GetEnumerator())); //1+(2+(3+(4+(5+0)))))
вероятно, не лучший способ сделать это, но если вы используете метод .ToList()
, вы можете получить элементы в позиции [0]
и [Count-1]
, если Count > 0.
Но вы должны указать, что вы имеете в виду под "может повторяться только один раз"
Что именно не так с .First()
и .Last()
? Хотя да, я должен согласиться с людьми, которые спросили: "Что означает хвост бесконечного списка"... понятие не имеет смысла, ИМО.