1. Why do collections need interfaces?
Let's be real: while we were writing code with a specific type (List<int>, Dictionary<string, string>), everything was simple and clear. But imagine you suddenly decide to swap List<T> for HashSet<T> because you only need unique values. Or you want to make a method that could work with both types of collections. What do you do?
You need a single entry point — a "universal plug." In C#, that plug is called an interface. They define a set of rules (methods, properties) that a collection must have. Anything that implements the interface supports those operations.
It's like a phone charger: if you have a standard USB-C port, you can charge your phone, a modern vacuum cleaner, and even an electric toothbrush (just don't mix them up in the dark).
Real-life example
Say you want to write a method that sums up all the numbers in any collection of numbers. Here's how it would look without interfaces:
// This method only works with arrays:
static int SumArray(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
// And this one - only with List<int>:
static int SumList(List<int> list)
{
int sum = 0;
for (int i = 0; i < list.Count; i++)
sum += list[i];
return sum;
}
Not cool! The code is almost the same. With the IEnumerable<int> interface, you can make it universal:
static int SumAll(IEnumerable<int> collection)
{
int sum = 0;
foreach (var number in collection)
sum += number;
return sum;
}
Now this method takes any type of collection that implements IEnumerable<int>: array, list, set, whatever.
2. Main collection interfaces
.NET has a few key interfaces for collections. Let's check them out quickly — you'll get the details on each in later lectures.
| Interface | What it's for | Examples of collections implementing the interface |
|---|---|---|
|
Enumerating elements (foreach) | All: , , arrays |
|
Mutable set (Add, Remove, Count) | , , |
|
Index access, modification | , arrays |
|
Working with "key-value" pairs | , |
|
Set (unique elements) | , |
P.S. For simplicity, we're skipping the "multiple levels" of inheritance between these interfaces for now. Who inherits from whom — we'll talk about that later.
Visualization: collection interface tree
IEnumerable<T>
^
|
ICollection<T>
^ ^
| |
IList<T> ISet<T>
| |
List<T> HashSet<T>
For dictionaries:
IEnumerable<KeyValuePair<K,V>>
^
|
ICollection<KeyValuePair<K,V>>
^
|
IDictionary<K,V>
|
Dictionary<K,V>
3. Why use interfaces in real life?
Flexibility
When your method or class works with an interface (IEnumerable<int>) instead of a specific type (List<int>), you can pass in any collection that implements that interface. Your program becomes flexible and extendable. (Instead of "Only iPhone" — now any phone with Bluetooth will do.)
static void PrintAll(IEnumerable<string> collection)
{
foreach (var item in collection)
Console.WriteLine(item);
}
Now you can print elements of any list, array, set, even a LINQ query result!
Unification
If you make your own collection type (like some super-specific collection for a certain task), just implement the right interfaces — and your collection will work with other people's code that doesn't even know about your collection. That's why in .NET you often see: "program to interfaces, not concrete implementations."
Compatibility with standard tools
Thanks to supporting standard interfaces, your collections can work with LINQ, sorting, serialization, and other .NET features.
4. List<T> — it's a list, a collection, and an enumeration
A few words about enumeration
IEnumerable<T> is the main and most basic collection interface in .NET. It defines just one method — GetEnumerator(), which returns an enumerator object for iterating over the collection. That's why you can write:
foreach (var name in names)
{
Console.WriteLine(name);
}
Where names can be a list, a set, or even an array. foreach is a sneaky bit of syntactic sugar that under the hood uses the GetEnumerator() method.
The IEnumerable interface (without <T>) is older and non-generic. These days, always use the generic version.
List class declaration syntax
Let's peek at the List<T> class declaration, just to see a bit of its magic:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, ...
This means a List<T> object can be used as:
- A list with random index access (IList<T>)
- A collection you can add/remove items from (ICollection<T>)
- Just a set of elements you can iterate over (IEnumerable<T>)
Example:
List<string> names = new List<string> { "Anna", "Boris", "Vasya" };
// As IEnumerable - you can loop through it
IEnumerable<string> asEnumerable = names;
foreach (var n in asEnumerable) Console.WriteLine(n);
// As ICollection - you can get Count:
ICollection<string> asCollection = names;
int count = asCollection.Count;
// As IList - you can access by index:
IList<string> asList = names;
string firstItem = asList[0];
5. App example
Let's say we have a simple app for tracking users and their roles (we started building it in previous lectures):
// User class
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
We can store users in any collection that implements IEnumerable<User>:
List<User> listUsers = new List<User> { new User { Name = "Anna", Age = 20 } };
HashSet<User> setUsers = new HashSet<User> { new User { Name = "Boris", Age = 25 } };
// Method that prints all user names
static void PrintUserNames(IEnumerable<User> users)
{
foreach (var user in users)
Console.WriteLine(user.Name);
}
// Use with different collection types:
PrintUserNames(listUsers);
PrintUserNames(setUsers);
Now the app is way more flexible — no magic or custom overloads needed!
6. Typical mistakes and gotchas
One of the most common mistakes is using types that are too specific when an interface would do:
// Bad (hardwired to implementation)
void DoSomethingWithList(List<int> numbers) { ... }
// Better (method works with any collection)
void DoSomethingWithNumbers(IEnumerable<int> numbers) { ... }
Another trap — forgetting that an interface only gives you the operations it explicitly describes. For example, IEnumerable<T> doesn't let you get the number of elements (Count); for that, you need ICollection<T> — or count them manually.
GO TO FULL VERSION