Here's a fact about two .NET types:
A
Nullable<T>
is equivalent to anIEnumerable<T>
with either zero or one elements.
That is, suppose you have a variable x
of type Nullable<T>
for some value
type T
(sometimes written T?
). Then:
- If
x.HasValue
isfalse
, then it's equivalent to anIEnumerable<T>
with zero elements. - If
x.HasValue
istrue
, then it's equivalent to anIEnumerable<T>
with one element.
Note that this only works for value types, as Nullable<T>
is only defined if
T
is a value type. However, you could apply the same principle to reference
types, especially if you're using nullable reference
types
in C# 8 or newer.
This realisation came in handy recently when I was refactoring some code. Here's a trimmed down example to explain how this is useful.
Example
Suppose your code has a function which returns a nullable:
int? GetNullableNumber(string text)
{
// The implementation of this method is less important than the return type
if (int.TryParse(text, out int value))
{
return value;
}
else
{
return null;
}
}
Then when you call it, it only makes sense to perform an action (e.g. saving to a database) if the nullable has a value:
// Assume GetText() is defined elsewhere
var text = GetText();
var nullableNumber = GetNullableNumber(text);
// This condition could equivalently be written nullableNumber != null
if (nullableNumber.HasValue)
{
var number = nullableNumber.Value;
SaveNumberToDatabase(number);
}
Suppose you now want to support a text
value that could contain multiple
numbers (perhaps comma-separated). To make this new functionality easier, we can
do an initial refactor like so:
IEnumerable<int> GetNumbers(string text)
{
if (int.TryParse(text, out int value))
{
return new List<int>() { value };
}
else
{
return Enumerable.Empty<int>();
}
}
And then the call site becomes:
var text = GetText();
var numbers = GetNumbers(text);
foreach (int number in numbers)
{
SaveNumberToDatabase(number);
}
From here, we can go on to add the new parsing functionality to GetNumbers()
.
Conclusion
This works because of the fact we started with:
A
Nullable<T>
is equivalent to anIEnumerable<T>
with either zero or one elements.
To refactor from nullable to enumerable, replace any HasValue
(or != null
)
checks with a foreach
.
It's worth pointing out that I don't necessarily think that the foreach
is
more readable; in fact, in the above example I think it's less readable! The
point I'm trying to make is that you can perform this refactor, often with a
view to then supporting a collection of any number of elements.
If you really want to get nerdy about this, the reason that this works is that
Nullable<T>
and IEnumerable<T>
are both functors. But perhaps that's a
topic for another time ...