Alexey Romanov
Alexey Romanov

Reputation: 170745

Implementing a bidirectional enumerator in C#

Is there a way to use yield blocks to implement an IEnumerator<T> which can go backward (MoveLast()) as well as forward?

Upvotes: 7

Views: 5223

Answers (7)

Brian Booth
Brian Booth

Reputation: 757

I know this thread is super old but it is relevant to note that

foreach(var item in someCollection)
{
    // Do something
}

... is get compiled into:

var enumerator = someCollection.GetEnumerator()
while (enumerator.MoveNext())
{
    var item = enumerator.Current;
    // Do something
}

So if you don't mind the "MoveNext" syntax, you could easily implement IEnumerator and add a "MovePrevious". You wouldn't be able to reverse direction if you use "foreach" but you'd be able to reverse direction if using a while loop.

Or... if you want to "foreach" a list in reverse direction (not bidirectional) you could take advantage of the yield statement.

public static IEnumerable<TItem> Get<TItem>(IList<TItem> list)
{
    if (list == null)
        yield break;

    for (int i = list.Count - 1; i > -1; i--)
        yield return list[i];
}

Or... if you want to foreach in reverse by going the long route you can implement your own IEnumerable/IEnumerator

public static class ReverseEnumerable
{
    public static IEnumerable<TItem> Get<TItem>(IList<TItem> list)
    {
        return new ReverseEnumerable<TItem>(list);
    }
}

public struct ReverseEnumerable<TItem> : IEnumerable<TItem>
{
    private readonly IList<TItem> _list;

    public ReverseEnumerable(IList<TItem> list)
    {
        this._list = list;
    }

    public IEnumerator<TItem> GetEnumerator()
    {
        if (this._list == null)
            return Enumerable.Empty<TItem>().GetEnumerator();

        return new ReverseEnumator<TItem>(this._list);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

public struct ReverseEnumator<TItem> : IEnumerator<TItem>
{
    private readonly IList<TItem> _list;
    private int _currentIndex;

    public ReverseEnumator(IList<TItem> list)
    {
        this._currentIndex = list.Count;
        this._list = list;
    }

    public bool MoveNext()
    {
        if (--this._currentIndex > -1)
            return true;

        return false;
    }

    public void Reset()
    {
        this._currentIndex = -1;
    }

    public void Dispose() { }

    public TItem Current
    {
        get
        {
            if (this._currentIndex < 0)
                return default(TItem);

            if (this._currentIndex >= this._list.Count)
                return default(TItem);

            return this._list[this._currentIndex];
        }
    }

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

Upvotes: 3

Alexey Romanov
Alexey Romanov

Reputation: 170745

Actually, there seems to be an approach described in Accelerated C# 2008. Unfortunately, two pages are not visible in the preview, and it has to rely on reflection (the results of which can be cached, as usual) but you can get the gist.

Upvotes: 1

Juliet
Juliet

Reputation: 81526

No. One of the limitations of IEnumerator is that it holds its current state, and it doesn't remember its prior state. As a result, IEnumerable is forward-only.

If you need to hold onto prior states, read the IEnumerable into a List or LinkedList and enumerate through those objects instead.

Upvotes: 1

Timur Fanshteyn
Timur Fanshteyn

Reputation: 2296

C5 Collections library (http://www.itu.dk/research/c5/) implements collections and linked list with backwards enumeration. The project is OpenSource so you should be able to find answer there.

Upvotes: 1

Marc Gravell
Marc Gravell

Reputation: 1062975

Not directly from the iterator block, no.

However, the caller can always buffer the results, for example into a List<T>, or just call Reverse() - but this doesn't always apply.

Upvotes: 5

David Schmitt
David Schmitt

Reputation: 59346

No. Using yield results in an IEnumerable which is unidirectional.

Upvotes: 0

Jon Skeet
Jon Skeet

Reputation: 1500893

No, the state machine generated by the C# compiler is strictly forward.

It doesn't even make sense to go backwards in many cases. Imagine an iterator reading from a network stream - to go backwards, it would have to remember everything that it had ever read, because it couldn't rewind time and ask the network for the data again.

(Ditto anything that generated data in some lossy way. Imagine an iterator which returned a new board for Conway's Life on each iteration - there are multiple boards which could all have been the previous one, so to go backwards you again have to remember what you've already returned.)

Upvotes: 5

Related Questions