-
-
Save franzalex/ea57f2d07b8b3fbfb8c23b08d242172c to your computer and use it in GitHub Desktop.
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Linq; | |
/// <summary> | |
/// Extends the default <see cref="IEnumerator{T}" /> implementation to allow accessing the next | |
/// element of the collection without advancing the position of the enumerator | |
/// </summary> | |
/// <typeparam name="T">The type of objects to enumerate.</typeparam> | |
/// <seealso cref="System.Collections.Generic.IEnumerator{T}" /> | |
public struct PeekEnumerator<T> : IEnumerator<T> | |
{ | |
private const string wasReset_ExceptionMessage = "Enumeration has not started. Call MoveNext."; | |
private IEnumerator<T> _enumerator; | |
private Queue<T> cache; | |
private bool wasReset; | |
/// <summary>Initializes a new instance of the <see cref="PeekEnumerator{T}" /> struct.</summary> | |
/// <param name="collection"> | |
/// The collection for which a <see cref="PeekEnumerator{T}" /> is to be created. | |
/// </param> | |
public PeekEnumerator(IEnumerable<T> collection) : this(collection.GetEnumerator()) { } | |
/// <summary>Initializes a new instance of the <see cref="PeekEnumerator{T}" /> class.</summary> | |
/// <param name="enumerator">The enumerator.</param> | |
/// <exception cref="ArgumentNullException">enumerator</exception> | |
public PeekEnumerator(IEnumerator<T> enumerator) | |
{ | |
_enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator)); | |
cache = new Queue<T>(); | |
wasReset = true; | |
} | |
/// <summary>Gets the element in the collection at the current position of the enumerator.</summary> | |
/// <exception cref="InvalidOperationException">Enumeration has not started. Call MoveNext.</exception> | |
public T Current | |
{ | |
get | |
{ | |
if (wasReset) throw new InvalidOperationException(wasReset_ExceptionMessage); | |
if (cache.Count == 0) throw new InvalidOperationException("Enumeration already finished."); | |
return cache.Peek(); | |
} | |
} | |
/// <summary>Gets the element in the collection at the current position of the enumerator.</summary> | |
object IEnumerator.Current => this.Current; | |
/// <summary>Gets a value indicating whether this instance has next.</summary> | |
/// <value><c>true</c> if this instance has next; otherwise, <c>false</c>.</value> | |
public bool HasNext => cache.Count >= 2 || TryFetchAndCache(2); | |
/// <summary> | |
/// Performs application-defined tasks associated with freeing, releasing, or resetting | |
/// unmanaged resources. | |
/// </summary> | |
public void Dispose() | |
{ | |
_enumerator.Dispose(); | |
} | |
/// <summary>Advances the enumerator to the next element of the collection.</summary> | |
/// <returns> | |
/// <see langword="true" /> if the enumerator was successfully advanced to the next element; | |
/// <see langword="false" /> if the enumerator has passed the end of the collection. | |
/// </returns> | |
public bool MoveNext() | |
{ | |
wasReset = false; | |
////// remove the previous Current value | |
////if (_cache.Count != 0) _cache.Dequeue(); | |
//// | |
////return _cache.Count > 0 || TryFetchAndCache(1); | |
if (cache.Any()) cache.Dequeue(); | |
var success = TryFetchAndCache(1); //TODO: Conduct tests and remove unused variable | |
return cache.Any(); | |
} | |
/// <summary>Gets the next item without advancing the position of the enumerator.</summary> | |
/// <returns>The next item in the collection.</returns> | |
/// <exception cref="InvalidOperationException">Cannot peek beyond end of enumeration.</exception> | |
public T PeekNext() | |
{ | |
if (wasReset) throw new InvalidOperationException(wasReset_ExceptionMessage); | |
if (cache.Count < 2 && !TryFetchAndCache(2)) | |
throw new InvalidOperationException("Cannot peek beyond end of enumeration."); | |
return cache.ElementAt(1); | |
} | |
/// <summary> | |
/// Sets the enumerator to its initial position, which is before the first element in the collection. | |
/// </summary> | |
public void Reset() | |
{ | |
_enumerator.Reset(); | |
cache.Clear(); | |
wasReset = true; | |
} | |
/// <summary>Tries to get the next element without advancing the position of the enumerator.</summary> | |
/// <param name="result"> | |
/// When this method returns, contains the next element in the collection if there are any | |
/// elements after the current position of the enumerator, or the default value for | |
/// <typeparamref name="T" /> if there are none. | |
/// </param> | |
/// <returns><c>true</c> if the next element was successfully retrieved; else <c>false</c>.</returns> | |
public bool TryPeekNext(out T result) | |
{ | |
try | |
{ | |
// check prevent peeking if enumerator has been previously reset | |
if (!wasReset && this.TryFetchAndCache(2)) | |
{ | |
result = this.PeekNext(); | |
return true; | |
} | |
} | |
catch (Exception ex) when (ex is InvalidOperationException || /* from TryFetchAndCache and PeekNext */ | |
ex is ArgumentOutOfRangeException /* from Enumerable.ElementAt() */) | |
{ | |
/* Empty exception handler to catch known exceptions. */ | |
} | |
result = default(T); | |
return false; | |
} | |
/// <summary> | |
/// Try to fetch the specified number of elements from the collection and cache the result. | |
/// </summary> | |
/// <param name="count">The number of elements to fetch and cache.</param> | |
/// <returns> | |
/// <c>true</c> if at least <paramref name="count" /> elements were successfully fetched; else <c>false</c>. | |
/// </returns> | |
/// <exception cref="InvalidOperationException">count</exception> | |
private bool TryFetchAndCache(int count) | |
{ | |
if (count <= 0) throw new InvalidOperationException(nameof(count) + " must be greater than 0"); | |
while (cache.Count < count && _enumerator.MoveNext()) | |
cache.Enqueue(_enumerator.Current); | |
return cache.Count >= count; | |
} | |
} |
using System.Collections.Generic; | |
public static class PeekEnumeratorExtensions | |
{ | |
/// <summary>Returns a <see cref="PeekEnumerator{T}" /> that iterates through the collection.</summary> | |
/// <typeparam name="T">The element type of objects to enumerate.</typeparam> | |
/// <param name="collection">The collection to be enumerated.</param> | |
/// <returns>A <see cref="PeekEnumerator{T}" /> that can be used to iterate through the collection.</returns> | |
public static PeekEnumerator<T> GetPeekEnumerator<T>(this IEnumerable<T> collection) | |
{ | |
return new PeekEnumerator<T>(collection.GetEnumerator()); | |
} | |
} |
Why prevent peeking a reset enumerator? It breaks most algorithms using this kind of an enumerator.
Why prevent peeking a reset enumerator? It breaks most algorithms using this kind of an enumerator.
@JoniHelen I don't really understand what you have written. If you're asking a question, could you please rephrase it?
Why prevent peeking a reset enumerator? It breaks most algorithms using this kind of an enumerator.
@JoniHelen I don't really understand what you have written. If you're asking a question, could you please rephrase it?
I'll rephrase: Why is using PeekNext
and TryPeekNext
unallowed in a reset enumerator?
If, for example, I have a collection with only a single element, and I create an enumerator for said collection, by the definition of IEnumerator, the next element would be the first element. However, I can't use Peek on this enumerator. Why? Why should I work around this restriction by always calling MoveNext
and checking the first element?
This class was implemented to be as close to the IEnumerator class as possible. When initialized, the MoveNext()
function must be called before accessing the first element (or peeking). This is in conformance with the description given in the documentation,
Initially, the enumerator is positioned before the first element in the collection. At this position, Current is undefined. Therefore, you must call MoveNext to advance the enumerator to the first element of the collection before reading the value of Current.Source: IEnumerator<T>§Remarks
This is just as I demonstrated in the usage sample and snippet posted below
int[] nums = new[] {0, 1, 2, 3, 4, 5};
var pe = new PeekEnumerator<int>(nums);
// alternative using extension methods
var pe = nums.GetPeekEnumerator();
while (pe.MoveNext()) // <-- this is required before calling any other properties
{
Console.WriteLine("{0,-10}: {1,5}", "Current", pe.Current);
Console.WriteLine("{0,-10}: {1,5}", "Has Next", pe.HasNext);
Console.WriteLine("{0,-10}: {1,5}", "Peek Next", pe.HasNext ? pe.PeekNext().ToString() : "<None>");
Console.WriteLine("---");
}
pe.Dispose();
Doesn't it kind of go against the idea of a peek enumerator to require calling MoveNext
before anything else? If I specifically want a peek enumerator, why would I always call MoveNext
if I know I'm using a peek enumerator that peeks to the next element (that in all cases except empty collections should be valid for a reset enumerator)? With this approach, I always have to make an exception in the case of the first element in the collection instead of using the same line of code for all elements.
In the process of designing this class, some fundamental decisions were made. Amongst these was the decision to stay as true to the IEnumerator<T>
in the .NET Standard Library as possible.
The explanation of your requirement is at variance with this specific decision that was taken.
The Standard Library requires calling MoveNext
before consuming the properties of the class instance so this implementation also has that same requirement.
The primary intention in posting the code here was to help anyone needing a similar solution. The code is free to use as-is, or to modify if you need a different behaviour.
If, therefore, if your use case requires a different behaviour, you can freely modify the relevant sections to achieve it.
Description
An extension of
System.Collections.Generic.IEnumerator<T>
to allow accessing the next element of the collection without advancing the position of the enumerator.Usage