Implementing Generic Interfaces in C# Data Structures
Implementing generic interfaces for custom data structures in C#
enables strong type safety, eliminates the performance overhead of
boxing and unboxing, and integrates seamlessly with the standard .NET
ecosystem, including LINQ. This guide covers how to define custom
generic interfaces, implement standard .NET collection interfaces like
IEnumerable<T>, apply generic type constraints, and
build a fully functional custom generic collection.
Defining a Custom Generic Interface
Creating a custom generic interface establishes a contract for how data can be added, removed, or accessed in your collection, independent of the underlying storage mechanism.
public interface ICustomStack<T>
{
void Push(T item);
T Pop();
T Peek();
int Count { get; }
bool IsEmpty { get; }
}The type parameter T acts as a placeholder for the
actual data type that the consumer specifies upon instantiation.
Implementing Common .NET Interfaces
To make a custom data structure idiomatic in C#, you should implement
standard framework interfaces. The most crucial is
IEnumerable<T>, which enables the
foreach loop and LINQ queries.
Implementing IEnumerable<T> requires satisfying
two methods:
IEnumerator<T> GetEnumerator(): The generic enumerator.IEnumerator IEnumerable.GetEnumerator(): The non-generic explicit implementation required for backward compatibility.
Complete Implementation: Custom Generic Stack
The following example demonstrates a custom linked stack that
implements both a custom interface (ICustomStack<T>)
and the standard IEnumerable<T>.
using System;
using System.Collections;
using System.Collections.Generic;
public class Node<T>
{
public T Value { get; set; }
public Node<T>? Next { get; set; }
public Node(T value)
{
Value = value;
}
}
public class LinkedStack<T> : ICustomStack<T>, IEnumerable<T>
{
private Node<T>? _top;
public int Count { get; private set; }
public bool IsEmpty => _top == null;
public void Push(T item)
{
var newNode = new Node<T>(item)
{
Next = _top
};
_top = newNode;
Count++;
}
public T Pop()
{
if (IsEmpty)
{
throw new InvalidOperationException("The stack is empty.");
}
T value = _top!.Value;
_top = _top.Next;
Count--;
return value;
}
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException("The stack is empty.");
}
return _top!.Value;
}
// Generic enumerator using an iterator block
public IEnumerator<T> GetEnumerator()
{
var current = _top;
while (current != null)
{
yield return current.Value;
current = current.Next;
}
}
// Explicit non-generic implementation
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}Applying Generic Type Constraints
If your data structure relies on specific operations—such as
comparison, serialization, or object construction—you can restrict
generic types using the where keyword on both the interface
and the implementation.
// Constraining T to types that implement IComparable<T>
public interface IPriorityQueue<T> where T : IComparable<T>
{
void Enqueue(T item);
T Dequeue();
}
public class PriorityQueue<T> : IPriorityQueue<T> where T : IComparable<T>
{
public void Enqueue(T item)
{
// Comparison logic using item.CompareTo(...)
}
public T Dequeue()
{
// Retrieval logic based on priority
throw new NotImplementedException();
}
}Common constraints include:
where T : struct: Value types only.where T : class: Reference types only.where T : notnull: Non-nullable types.where T : new(): Must have a public parameterless constructor.where T : InterfaceName: Must implement the specified interface.
Usage Example
Once implemented, the custom structure functions just like built-in .NET collections:
var stack = new LinkedStack<int>();
stack.Push(10);
stack.Push(20);
stack.Push(30);
// Using LINQ via IEnumerable<T>
bool containsTwenty = stack.Any(x => x == 20);
// Iterating using foreach
foreach (var item in stack)
{
Console.WriteLine(item);
}