In this article, we are going to discuss three popular interfaces in the area of .NET collections. We all use collections every day in .NET, but do we really understand the difference between each of them, and when we should use one or another? Hopefully, at the end of this article, we should have a clearer understanding of these concepts and how to apply them to real-world problems.
Let’s start with some basic theory on .NET collections.
Theory on .NET Collections
.NET Collections are a set of similar types that are grouped together. In .NET, we have a variety of different ways we can do this, but in this article, we’ll discuss the most popular ones. Each of the different collections interfaces in .NET shares similar functionality but usually have one or two key defining features. These features are the ones we need to pay attention to, in order to make decisions on their application. In this article, we are going to focus on the generic implementations of each of the interfaces (e.g IEnumerable<T>, IQueryable<T>, ICollection<T>), as these are the recommended interfaces to achieve type safety since generics were implemented back in .NET 2.0.
Let’s start with IEnumerable.
IEnumerable<T>
IEnumerable<T>
is an interface in the System.Collections.Generics
namespace. It is the base interface for all collections in the System.Collections.Generics
namespace, such as the popular List<T>
class, and it’s likely the one you’ll use most in .NET. Any class that implements IEnumerable<T>
can be enumerated with the foreach statement, so it makes a good choice for cases when you want the consumer to loop over the items and perform some action. Because it is also such a top-level interface, this allows the best flexibility.
Let’s look at a simple example:
using System; using System.Collections.Generic; namespace ConsoleApp1 { class Program { private static void IEnumerableLoopExample(IEnumerable<int> numbers) { foreach (var number in numbers) { if (number % 2 == 0) { Console.WriteLine($"{number} is even"); } } } static void Main(string[] args) { IEnumerableLoopExample(new List<int> { 1, 2, 3, 4, 5 }); Console.ReadKey(); } } }
As mentioned, implementing the IEnumerable<T>
interface allows us to use the foreach operator to loop over each item in a very simple manner than most developers are accustomed to, which makes it a very popular choice. Another special feature of the IEnumerable<T>
is the yield keyword. The yield keyword allows us to return each element of a collection one at a time, without requiring us to allocate the collection.
Let’s look at an example:
static IEnumerable<int> YieldEvenNumbersExample() { for (var i = 1; i <= 5; i++) { if (i % 2 == 0) { yield return i; } } }
We are again implementing an even number functionality. But the interesting thing here is that even though we are returning IEnumerable<int>
, we haven’t declared that anywhere. We simply use the yield
keyword to return each item that we want to be included in the final result set.
If we capture and print the numbers:
var evenNumbers = YieldEvenNumbersExample(); foreach (var number in evenNumbers) { Console.WriteLine(number); }
We’ll see the result is the same as before (2, 4). These use of the foreach and yield operators make IEnumerable<T>
arguably the best collection to use in .NET for most scenarios.
Let’s look at the next collection, IQueryable.
IQueryable<T>
We can find this interface in the System.Linq
namespace, which allows us to evaluate queries against known data sources, for example, LINQ to SQL. IQueryable<T>
extends IEnumerable<T>
, so therefore it inherits all the previous behavior we discussed. IQueryable<T>
works with “expression trees”, which is functionality represented in a way that LINQ can convert to the specific provider that is needed.
Let’s look at a very simple example:
static IQueryable<int> GetNumbersFromDb() { return new[] { 1, 2, 3, 4, 5 }.AsQueryable(); }
Here is a method that returns the numbers 1-5 in memory, but let’s pretend it was a LINQ-SQL or Entity Framework code generated class that returns IQueryable<T>
, as the concept is the same.
Now, let’s use the method:
var numbers = GetNumbersFromDb(); var evenNumbers = numbers.Where(number => number % 2 == 0) .ToList(); foreach (var number in evenNumbers) { Console.WriteLine(number); }
Notice we are filtering the numbers with a Where
clause, and then performing a ToList
operation, before printing out the numbers.
Again, the output will be the same (2,4), but there are two important things to note:
- The
Where
clause will filter the data according to the underlying provider. This method exists on both theIEnumerable<T>
andIQueryable<T>
interfaces, but there is a key difference with theIQueryable<T>
implementation. The filter is executed on the database side (e.g SQL Server), whereasIEnumerable<T>
is executed server-side on the code, meaning all data is sent over the wire. In other words,IQueryable<T>
is a much more performant choice when databases are involved, as it delegates the filtering to the DB itself, making for a more efficient operation. - The
ToList
evaluates the expression, causing execution (for example, on SQL). This allows us to “chain” various filters, and only execute once (deferred execution).
For any scenario that relates to “out of process” operations, IQueryable<T>
is the logical choice as it allows remote filtering.
In the next section, let’s look at the final collection to discuss, ICollection.
ICollection<T>
This interface resides in the System.Collections.Generic
namespace, which again inherits IEnumerable<T>
. However, a key feature of ICollection<T>
is that it allows us to add or remove items to the collection.
Let’s look at an example:
static void ICollectionExample(ICollection<int> numbers) { numbers.Add(100); if (numbers.Contains(1)) { numbers.Remove(1); } }
In this example, we are adding the number 100 to our collection, and removing the number 1. This would not be possible with IEnumerable<T>
or IQueryable<T>,
so it’s a key distinction.
Let’s use our method:
var numbers = new List<int> { 1, 2, 3, 5 }; ICollectionExample(numbers); foreach (var number in numbers) { Console.WriteLine(number); }
As expected, the output is 2, 3, 5, 100. Often when we have items from multiple sources, we need to add items to existing sequences, so having this functionality is a very useful feature.
Now that we’ve discussed each collection, let’s summarise how we can choose each one.
How to Choose Between These .NET Collections
We should have gathered by now, that the choice is pretty simple:
- Do we need to execute LINQ operations on remote data sources? Use IQueryable<T>
- Otherwise, do we need to add or remove items in the collection? Use ICollection<T>
- Else use IEnumerable<T> for all other scenarios
When it comes to writing code in .NET (or any object-orientated language), advice tells us to use the highest interface possible to enable the greatest flexibility. Therefore, if possible we should always try and use IEnumerable<T>
. When it comes to performance, we cannot compare the interfaces against each other, but rather specific operations, so best to research those particular methods as you implement them in your application.
Conclusion
Hopefully, this article gave a very brief introduction to the three most popular interfaces in the .NET Collections area and helps make decisions on when and how to implement each of them.
Happy coding!