-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
217: Practice Exercise: Building a Custom IEnumerable<T> Collection
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 aforeachloop. - 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.
There are no comments for now.