C# > Advanced C# > Collections and Generics > IEnumerable<T> and IEnumerator<T>

Custom Collection with IEnumerable and IEnumerator

This snippet demonstrates how to create a custom collection in C# that implements IEnumerable<T> and provides its own IEnumerator<T> implementation. This allows you to iterate over the collection using foreach or other mechanisms that rely on the IEnumerable interface.

The example creates a simple CustomList<T> that stores a fixed-size array of items. It provides a custom enumerator called CustomListEnumerator<T> that handles the iteration logic.

Code Example

The CustomList<T> class implements IEnumerable<T>, which requires the implementation of GetEnumerator(). This method returns an instance of the custom enumerator, CustomListEnumerator<T>.

The CustomListEnumerator<T> class implements IEnumerator<T>. It maintains the current index and the current item during iteration. The MoveNext() method advances the enumerator to the next item in the collection, and the Current property returns the current item.

The Example class demonstrates how to use the CustomList<T> class in a foreach loop. The loop automatically calls GetEnumerator() and uses the MoveNext() and Current properties to iterate through the collection.

using System;
using System.Collections;
using System.Collections.Generic;

public class CustomList<T> : IEnumerable<T>
{
    private T[] _items;
    private int _currentIndex = -1;

    public CustomList(int size)
    {
        _items = new T[size];
    }

    public void Add(T item, int index)
    {
        if (index >= 0 && index < _items.Length)
        {
            _items[index] = item;
        }
    }

    public T GetItem(int index)
    {
        if (index >= 0 && index < _items.Length)
        {
            return _items[index];
        }
        return default(T);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new CustomListEnumerator<T>(this);
    }

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

    private class CustomListEnumerator<T> : IEnumerator<T>
    {
        private CustomList<T> _list;
        private int _index;
        private T _current;

        public CustomListEnumerator(CustomList<T> list)
        {
            _list = list;
            _index = -1;
            _current = default(T);
        }

        public T Current
        {
            get { return _current; }
        }

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

        public void Dispose() { }

        public bool MoveNext()
        {
            _index++;
            if (_index >= 0 && _index < _list._items.Length)
            {
                _current = _list._items[_index];
                return true;
            }
            else
            {
                return false;
            }
        }

        public void Reset()
        {
            _index = -1;
            _current = default(T);
        }
    }
}

public class Example
{
    public static void Main(string[] args)
    {
        CustomList<string> myList = new CustomList<string>(3);
        myList.Add("Item 1", 0);
        myList.Add("Item 2", 1);
        myList.Add("Item 3", 2);

        foreach (string item in myList)
        {
            Console.WriteLine(item);
        }
    }
}

Concepts Behind the Snippet

IEnumerable<T>: Represents a sequence of objects that can be iterated over.

IEnumerator<T>: Provides the functionality to iterate through a collection, providing access to each element one at a time.

Implementing these interfaces allows your custom collections to be used with foreach loops and LINQ queries.

Real-Life Use Case

Consider a scenario where you have a data structure that doesn't directly inherit from standard collections like List<T> or Array, but you still need to provide a way to iterate over its elements. For example, you might have a custom tree structure, a graph, or a collection that loads data lazily from a file. Implementing IEnumerable<T> and IEnumerator<T> allows you to expose the data in a way that can be easily consumed by other parts of your application, using standard iteration patterns.

Best Practices

Dispose Resources: If your enumerator uses any resources (e.g., file handles, network connections), implement the IDisposable interface to release those resources when the enumeration is complete.

Thread Safety: Be aware of thread safety when implementing IEnumerator. If the underlying collection can be modified by multiple threads, you'll need to implement appropriate locking mechanisms to prevent race conditions.

Consider Yield Return: For simple iterations, using the yield return statement within a method that returns IEnumerable<T> can greatly simplify the implementation of the enumerator.

Interview Tip

Be prepared to explain the difference between IEnumerable and IEnumerator. IEnumerable represents the collection, while IEnumerator provides the mechanism for traversing that collection. Also, understand how foreach internally uses these interfaces.

When to use them

Use IEnumerable<T> and IEnumerator<T> when you need to create custom collections with specific iteration logic, or when you need to expose an existing data structure in a way that allows for easy iteration using foreach loops.

Memory footprint

The memory footprint depends on the underlying data structure. The IEnumerator itself generally has a small memory footprint, as it only needs to track the current position. However, if the enumeration involves loading large amounts of data, the overall memory usage will be higher.

Alternatives

Using yield return: For simple scenarios, using yield return is a more concise way to implement IEnumerable<T>. This avoids the need to create a separate enumerator class.

Using Existing Collections: Whenever possible, leverage existing collection classes (e.g., List<T>, Array) instead of creating custom collections from scratch.

Pros

Customizable Iteration: Provides full control over how the collection is iterated.

Integration with foreach: Allows your custom collections to be used seamlessly with foreach loops.

Lazy Evaluation: Enables lazy loading and processing of data during iteration.

Cons

Increased Complexity: Implementing IEnumerator manually can be more complex than using yield return or existing collections.

Potential for Errors: Incorrectly implemented enumerators can lead to unexpected behavior, such as infinite loops or incorrect data access.

FAQ

  • What is the difference between IEnumerable and IEnumerator?

    IEnumerable represents a collection that can be iterated, while IEnumerator provides the mechanism to iterate through the collection, keeping track of the current position.
  • Why should I implement IEnumerable and IEnumerator?

    Implementing these interfaces allows your custom collections to be used with 'foreach' loops and LINQ queries, providing a standard way to iterate over your data.
  • Can I use 'yield return' instead of implementing IEnumerator?

    Yes, for simple scenarios, 'yield return' provides a more concise way to implement IEnumerable. However, for more complex iteration logic, implementing IEnumerator might be necessary.