Avoiding null: The Case for Returning Empty Lists or Arrays in C#

Programmers around the world have all experienced the dread that comes with encountering a NullReferenceException
. These errors can occur when you’re working with complex applications, and if not handled properly, can result in serious headaches and hours of debugging. In C#, developers face the dilemma of whether to return null
or an empty list or array when a method is not able to return values. Often times, returning an empty list or array is often a better choice than returning null
.
The Problem with Null
The main issue with null
is that it can lead to the infamous NullReferenceException
. The exception arises because there’s nothing there to call upon — it’s like trying to open a door in a house that doesn’t exist!
Example:
List<string> myList = GetList("Susan");
// throws NullReferenceException if GetList returns null
int count = myList.Count;
public static List<string>? GetList(string filter)
{
List<string> strings = new List<string> { "Jane", "Bob" };
List<string> result = strings.Where(s => s.Contains(filter)).ToList();
return result.Count > 0 ? result : null;
}
The Solution: Empty Collections
To avoid the potential pitfalls of returning null
, it’s often preferable to return an empty list or array. When the client code receives an empty list or array, it can operate on it just like it would with a populated list or array, without the fear of a NullReferenceException
. This makes it easier to write safe, robust code. It also makes the intent of your code clearer and eliminates the need for the client code to check for null
before using the returned value.
Consider the following code:
List<string> myList = GetList("mike");
// safe, even if GetList returns an empty list
int count = myList.Count;
public static List<string>? GetList(string filter)
{
List<string> strings = new List<string> { "Jane", "Bob" };
List<string> result = strings.Where(s => s.Contains(filter)).ToList();
return result.Count > 0 ? result : Enumerable.Empty<string>().ToList();
}
With this approach, GetList
can return an empty list when there are no strings to return, and the client code can safely call the Count
property without needing to check for null
.
Beside not having to do a null check, by using an Empty Collection, you simplify the conditional checks, and simplify your code’s flow. Like in the below example, you don’t need a lot of conditional checking in your code, slimming it down and making it easier to read.
// Simplified flow:
List<string> myList = GetList("Alice");
foreach (var item in myList)
{
Console.WriteLine(item); // Works even if the list is empty
}
Am I going to Incur a Cost using an Empty Collection?
One of the arguments against returning an empty list or array is that creating an empty collection can have a cost in terms of memory and performance. However, in many cases, this cost is minimal. Moreover, C# offers a way to return an empty list or array without creating a new one each time.
The Array.Empty<T>
method returns a singleton instance of an empty array. This method doesn’t allocate memory for an empty array; instead, it always returns the same array instance. This way, you can return an empty array without worrying about the performance implications.
public int[] GetNumbers()
{
// Some logic here…
if (/* no numbers to return */) return Array.Empty<int>();
}
Similarly, for a list, you can return Enumerable.Empty<T>().ToList()
, which also doesn’t allocate memory for an empty list.
Wrapping it up
Returning empty lists or arrays instead of null
can help you write code that’s safer and easier to understand. It eliminates the risk of NullReferenceException
when the returned value is used and makes it clear that the method didn’t find anything to return. The potential performance cost of creating empty collections can be avoided by returning singleton instances of empty arrays or lists.
This approach can save you a lot of time and frustration in the long run. No more unnecessary null checks or unexpected NullReferenceException
! Instead, you get clear, robust code that behaves consistently, whether it’s working with a full list, an empty one, or anything in between.