5
\$\begingroup\$

Many of you might have come to the point and wished to have a Peek for IEnumerator and IEnumerator. I tried to implement it by cheating a bit and looking up the next element before the actual MoveNext call. So I ended up with some kind of wrapper.

First of the extensions to convert default enumerators:

public static class PeekableEnumeratorExtension
{
    public static PeekableEnumerator ToPeekable(this IEnumerator enumerator)
    {
        return new PeekableEnumerator(enumerator);
    }

    public static PeekableEnumerator<T> ToPeekable<T>(this IEnumerator<T> enumerator)
    {
        return new PeekableEnumerator<T>(enumerator);
    }
}

And here is the non-generic PeekableEnumerator:

public class PeekableEnumerator : IEnumerator
{
    protected enum Status { Uninitialized, Starting, Started, Ending, Ended }

    protected IEnumerator enumerator;

    protected Status status;

    protected object current;

    protected object peek;

    public PeekableEnumerator(IEnumerator enumerator)
    {
        this.enumerator = enumerator;
        status = Status.Uninitialized;
        MoveNext();
    }

    public object Current
    {
        get
        {
            if (Status.Starting == status)
                throw new InvalidOperationException("Enumeration has not started. Call MoveNext.");
            if (Status.Ended == status)
                throw new InvalidOperationException("Enumeration already finished.");

            return current;
        }
    }

    public object Peek
    {
        get
        {
            if (Status.Ending == status || Status.Ended == status)
                throw new InvalidOperationException("Enumeration already finished.");

            return peek;
        }
    }

    public bool MoveNext()
    {
        current = peek;
        switch (status)
        {
            case Status.Uninitialized:
            case Status.Starting:
                if (enumerator.MoveNext())
                {
                    status++;
                    peek = enumerator.Current;
                }
                else
                    status = Status.Ending;
                break;
            case Status.Started:
                if (enumerator.MoveNext())
                    peek = enumerator.Current;
                else
                    status++;
                break;
            case Status.Ending:
                status++;
                break;
        }

        return Status.Ended != status;
    }

    public void Reset()
    {
        enumerator.Reset();
        status = Status.Uninitialized;
        MoveNext();
    }
}

And the very analog PeekableEnumerator:

public class PeekableEnumerator<T> : IEnumerator<T>
{
    protected enum Status { Uninitialized, Starting, Started, Ending, Ended }

    protected IEnumerator<T> enumerator;

    protected Status status;

    protected T current;

    protected T peek;

    public PeekableEnumerator(IEnumerator<T> enumerator)
    {
        this.enumerator = enumerator;
        status = Status.Uninitialized;
        MoveNext();
    }

    public T Current
    {
        get
        {
            if (Status.Starting == status)
                throw new InvalidOperationException("Enumeration has not started. Call MoveNext.");
            if (Status.Ended == status)
                throw new InvalidOperationException("Enumeration already finished.");

            return current;
        }
    }

    object System.Collections.IEnumerator.Current { get { return Current; } }

    public T Peek
    {
        get
        {
            if (Status.Ending == status || Status.Ended == status)
                throw new InvalidOperationException("Enumeration already finished.");

            return peek;
        }
    }

    public bool MoveNext()
    {
        current = peek;
        switch (status)
        {
            case Status.Uninitialized:
            case Status.Starting:
                if (enumerator.MoveNext())
                {
                    status++;
                    peek = enumerator.Current;
                }
                else
                    status = Status.Ending;
                break;
            case Status.Started:
                if (enumerator.MoveNext())
                    peek = enumerator.Current;
                else
                    status++;
                break;
            case Status.Ending:
                status++;
                break;
        }

        return Status.Ended != status;
    }

    public void Reset()
    {
        enumerator.Reset();
        status = Status.Uninitialized;
        MoveNext();
    }

    public void Dispose()
    {
        enumerator.Dispose();
    }
}

Before you ask: Why are there 5 statuses? It is derived from the lifetime of Current and Peek:

Status        | Current   | Peek      | Comment
--------------+-----------+-----------+-----------------------------------
Uninitialized | n/a       | n/a       | Internal for constructor and Reset
Starting      | Exception | Available | Before first MoveNext
Started       | Available | Available | After first MoveNext
Ending        | Available | Exception | wrapped MoveNext returned false
Ended         | Exception | Exception | After enumeration finished

Example usage:

var a = new[] { 1, 2, 3 }.GetEnumerator().ToPeekable();

a.Current; // InvalidOperationException
a.Peek; // 1
a.MoveNext(); // true
a.Current; // 1
a.Peek; // 2
a.MoveNext(); // true
a.Current; // 2
a.Peek; // 3
a.MoveNext(); // true
a.Current; // 3
a.Peek; // InvalidOperationException
a.MoveNext(); // false
a.Current; // InvalidOperationException
a.Peek; // InvalidOperationException

Update

Thanks to svick here is an alternative version using a Queue. It changes the basic usage from IEnumerator to ICollection as input but I can live with that. I need to keep an copy of the original collection for resetting.

public class PeekableEnumerator : IEnumerator
{
    protected ICollection collection;

    protected Queue queue;

    protected bool current_set;

    protected object current;

    protected bool peek_set;

    protected object peek;

    public object Current
    {
        get
        {
            if (!current_set)
                if (peek_set)
                    throw new InvalidOperationException("Enumeration has not started. Call MoveNext.");
                else
                    throw new InvalidOperationException("Enumeration already finished.");

            return current;
        }
    }

    public object Peek
    {
        get
        {
            if (!peek_set)
                throw new InvalidOperationException("Enumeration already finished.");

            return peek;
        }
    }

    public PeekableEnumerator(ICollection collection)
    {
        this.collection = collection;
        Reset();
    }

    public bool MoveNext()
    {
        current_set = peek_set;
        current = peek;

        if (0 == queue.Count)
        {
            peek_set = false;
            return current_set;
        }
        else
        {
            peek_set = true;
            peek = queue.Dequeue();
            return true;
        }
    }

    public void Reset()
    {
        queue = new Queue(collection);
        MoveNext();
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ While I applaud the effort, I doubt its usage, when and why would you want or need to peek without just iterating? \$\endgroup\$ – Davio Oct 18 '13 at 8:25
  • 1
    \$\begingroup\$ @Davio Peek is very useful when you make decisions based on the next value inside of a loop without consuming the value. \$\endgroup\$ – eisberg Oct 18 '13 at 8:43
  • 1
    \$\begingroup\$ @eisberg Have you considered using Queue<T> instead? I think that's much more natural most of the time. Queue<T> is not lazy, but I never needed that in such cases. \$\endgroup\$ – svick Oct 18 '13 at 8:57
  • \$\begingroup\$ @eisberg, Well, that doesn't really answer my question. I'm looking for a real-world scenario where you would actually want to use it. What do you use it for? \$\endgroup\$ – Davio Oct 18 '13 at 9:09
  • 1
    \$\begingroup\$ @Davio A parser for example. \$\endgroup\$ – eisberg Oct 18 '13 at 11:03
5
\$\begingroup\$

I think your implementation is too complicated, and what nagged me was that you start enumerate in constructor. Here is my implementation which fix that. The state reduced to a boolean telling that the peek value has been fetched from the underlying enumerator or not.

public class PeekEnumerator<T> : IEnumerator<T>
{
    private IEnumerator<T> _enumerator;
    private T _peek;
    private bool _didPeek;

    public PeekEnumerator(IEnumerator<T> enumerator)
    {
        if (enumerator == null)
            throw new ArgumentNullException("enumerator");
        _enumerator = enumerator;
    }

    #region IEnumerator implementation
    public bool MoveNext()
    {
        return _didPeek ? !(_didPeek = false) : _enumerator.MoveNext();
    }

    public void Reset()
    {
        _enumerator.Reset();
        _didPeek = false;
    }

    object IEnumerator.Current { get { return this.Current; } }
    #endregion

    #region IDisposable implementation
    public void Dispose()
    {
        _enumerator.Dispose();
    }
    #endregion

    #region IEnumerator implementation
    public T Current
    {
        get { return _didPeek ? _peek : _enumerator.Current; }
    }
    #endregion

    private void TryFetchPeek() {
        if (!_didPeek && (_didPeek = _enumerator.MoveNext()))
        {
            _peek = _enumerator.Current;
        }
    }

    public T Peek
    {
        get { 
            TryFetchPeek();
            if (!_didPeek)
                throw new InvalidOperationException("Enumeration already finished.");

            return _peek;
        }
    }
}

My test to make sure it complies to your needed behaviour:

var a = new PeekEnumerator<int>(new [] { 1, 2, 3 }.AsEnumerable().GetEnumerator());
Console.WriteLine(a.Peek); // 1
Console.WriteLine(a.MoveNext()); // true
Console.WriteLine(a.Current); // 1
Console.WriteLine(a.Peek); // 2
Console.WriteLine(a.MoveNext()); // true
Console.WriteLine(a.Current); // 2
Console.WriteLine(a.Peek); // 3
Console.WriteLine(a.MoveNext()); // true
Console.WriteLine(a.Current); // 3

try {
    Console.WriteLine(a.Peek); // InvalidOperationException
}
catch (Exception e) {
    Console.WriteLine(e.GetType());
}

Console.WriteLine(a.MoveNext()); // false

try {
    Console.WriteLine(a.Current); // InvalidOperationException
}
catch (Exception e) {
    Console.WriteLine(e.GetType());
}

try {
    Console.WriteLine(a.Peek); // InvalidOperationException
}
catch (Exception e) {
    Console.WriteLine(e.GetType());
}
\$\endgroup\$
  • \$\begingroup\$ This nice but it does not throw an InvalidOperationException before the first use of MoveNext on Current. See my example usage. \$\endgroup\$ – eisberg Oct 22 '13 at 7:03
  • \$\begingroup\$ It does. It relies on the exception thrown by the underlying enumerator in that case. \$\endgroup\$ – tia Oct 22 '13 at 8:06
  • \$\begingroup\$ Sorry. I tested with List<int>(...) which returns default(...) instead of throwing an InvalidOperationException. \$\endgroup\$ – eisberg Oct 22 '13 at 8:40
  • \$\begingroup\$ Seems they are different implementation between List and Array. According to MSDN documentation msdn.microsoft.com/en-us/library/…, the Current property should be undefined on created instead of throwing exception when called. \$\endgroup\$ – tia Oct 22 '13 at 10:26
  • 1
    \$\begingroup\$ Well, also according to .NET Framework Design Guideline : msdn.microsoft.com/en-us/library/ms229006(v=vs.110).aspx, property should avoid raising exception but it's up to you anyway. Actually, the design has an issue that you cannot peek at the end of the enumerator, so a HasPeek method/property should be added to allow consumer to check on it rather than forcing consumer to try/catch the exception. \$\endgroup\$ – tia Oct 23 '13 at 17:06
2
\$\begingroup\$

Not really sure what your question is but I have something you might want to think about, Maybe Peek() shouldn't throw an exception when it's out of bounds or has Status.Ending == status || Status.Ended == status, but instead return a null object so you can check the value of it before you decide to do something.

if(a.Peek != null) { ... }

Food for thought

\$\endgroup\$
  • 1
    \$\begingroup\$ This is code review. So I did not ask to review my code please :) As null is a valid value. This is not an option. \$\endgroup\$ – eisberg Oct 18 '13 at 8:41
  • 3
    \$\begingroup\$ Yes this is codereview, so anything in here will be reviewed :P But it's always a good idea to come up with some specific questions. \$\endgroup\$ – Max Oct 18 '13 at 8:47
  • \$\begingroup\$ I see. I should have asked for improvements. Maybe. \$\endgroup\$ – eisberg Oct 18 '13 at 8:57
1
\$\begingroup\$

I changed svick's implemntation to use generics, and also changed the peek method to return bool rather than throwing an exception... although adding

bool TryPeek(out T value);

to the existing implementation might be more appropriate

public class PeekableEnumerator<T> : IEnumerator<T>
{
    protected IEnumerable<T> collection;

    protected Queue<T> queue;

    protected bool current_set;

    protected T current;

    protected bool peek_set;

    protected T peek;

    public T Current
    {
        get
        {
            if (!current_set)
                if (peek_set)
                    throw new InvalidOperationException("Enumeration has not started. Call MoveNext.");
                else
                    throw new InvalidOperationException("Enumeration already finished.");

            return current;
        }
    }

    public bool Peek(out T value)
    {
        value = default(T);

        if (!peek_set)
            return false;

        value = peek;
        return true;
    }

    public PeekableEnumerator(IEnumerable<T> collection)
    {
        this.collection = collection;
        Reset();
    }

    public bool MoveNext()
    {
        current_set = peek_set;
        current = peek;

        if (0 == queue.Count)
        {
            peek_set = false;
            return current_set;
        }
        else
        {
            peek_set = true;
            peek = queue.Dequeue();
            return true;
        }
    }

    public void Reset()
    {
        queue = new Queue<T>(collection);
        MoveNext();
    }

    public virtual void Dispose()
    {

    }

    object System.Collections.IEnumerator.Current
    {
        get { return Current; }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.