Skip to Content
Course content

217: Practice Exercise: Building a Custom IEnumerable<T> Collection

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to almost every developer the first time they try to build a custom collection. You've got your logic down, you've implemented IEnumerable<T>, and you're ready to use a foreach loop. Then, the compiler hits you with an error that feels completely contradictory: "SecureVault does not implement interface member IEnumerable.GetEnumerator()".

Wait, what? You did implement GetEnumerator(). You implemented the generic one. Here is exactly what that broken code looks like:

public class SecureVault : IEnumerable<string>
{
    private List<string> _items = new List<string> { "Secret1", "Secret2", "Public1" };

    public IEnumerator<string> GetEnumerator()
    {
        foreach (var item in _items)
        {
            if (!item.StartsWith("Secret")) yield return item;
        }
    }
}

The "Missing Member" Compiler Error

If you try to compile the code above, it fails. The reason is a bit of a historical quirk in .NET. IEnumerable<T> actually inherits from the older, non-generic IEnumerable. Because of that inheritance, your class is technically required to implement both versions of GetEnumerator().

When you write a foreach loop, the C# compiler looks for the generic version. But the interface contract itself demands the non-generic version exists for compatibility with older parts of the framework. If you only provide the generic one, the contract isn't fully satisfied.

Bridging the Gap Between Generic and Non-Generic

The fix is straightforward, but it's a pattern you'll see in almost every professional C# library. You don't want to write the iteration logic twice—that's a maintenance nightmare. Instead, you implement the non-generic method by simply calling your generic method.

I prefer using explicit interface implementation for the non-generic version. This keeps the "ugly" legacy method hidden from the class's public API, so other developers don't accidentally call the wrong one.

public class SecureVault : IEnumerable<string>
{
    private List<string> _items = new List<string> { "Secret1", "Secret2", "Public1" };

    // This is the one the foreach loop actually uses
    public IEnumerator<string> GetEnumerator()
    {
        foreach (var item in _items)
        {
            if (!item.StartsWith("Secret")) yield return item;
        }
    }

    // Explicit implementation: This satisfies the IEnumerable interface
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Now, the compiler is happy. By returning GetEnumerator() (the generic one) inside the non-generic method, we've created a bridge. The IEnumerator<T> interface inherits from IEnumerator, so the types are compatible.

I also used yield return here. If you're building custom collections, stop manually creating "Enumerator" classes. yield return tells the compiler to generate a state machine behind the scenes, handling all the MoveNext() and Current logic for you. It's cleaner, less error-prone, and significantly more readable.




📋 Practical Task

Build a CircularBuffer<T> Collection

Your goal is to create a custom collection called CircularBuffer<T>. This collection should hold a fixed maximum capacity. When the capacity is reached, adding a new item should overwrite the oldest item in the buffer.

Requirements:

  • Implement IEnumerable<T> so the buffer can be iterated using a foreach loop.
  • The Add(T item) method should handle the "wrapping" logic (overwriting the oldest element using a modulo operator or a pointer).
  • The iterator must return the items in the correct chronological order (from the oldest remaining item to the newest), regardless of where they sit in the internal array.
  • Ensure you implement both the generic and non-generic GetEnumerator() methods to avoid compiler errors.

Testing your code: Create a buffer with a capacity of 3, add the numbers 1, 2, 3, 4, and 5. When you iterate over it, the output should be 3, 4, 5.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.