While introducing nullable reference types to our enterprise application's codebase, I found that we are often using LINQ on sequences of nullable reference types where we filter out the nulls using .Where(item => item is not null)
. The compiler does not see that the resulting sequence should not have nullable items anymore, though, because the Where method has the same type of input and output.
I came up with an extension method that solves the issue:
using System.Collections.Generic;
using System.Linq;
namespace Company.Product.Application.Component.Extensions;
public static class EnumerableOfNullableExtensions
{
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
{
return source.Where(item => item is not null)!;
}
}
It can be applied to sequences of nullable value types, too, but the items in the resulting sequence are sadly still nullable. An extra .Select(item => item.Value)
is needed because after filtering out the nulls, the representation of the remaining values still needs to change, unlike for the reference types1.
The extension can also be applied to non-nullable types.
So I came up with an improved version that handles nullable value types well and even included an extra extension for casting sequences of nullable reference types to sequences of the corresponding non-nullable type:
using System.Collections.Generic;
using System.Linq;
namespace Company.Product.Application.Component.Extensions;
public static class EnumerableOfNullableExtensions
{
public static IEnumerable<T> CastNotNull<T>(this IEnumerable<T?> source)
where T : class
{
return source!;
}
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : class
{
return source.Where(item => item is not null).CastNotNull();
}
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : struct
{
return source.Where(item => item.HasValue).Select(item => item!.Value);
}
}
None of the overloads of the WhereNotNull method accepts non-nullable value types anymore. Non-nullable reference types are still accepted, but that is consistent with the design of nullable reference types. Non-nullable reference type T is assignable to T? and variance rules apply (covariance in the case of IEnumerable).
I noticed that if the null check needs to be performed e.g. on a property of the objects in the sequence, the next use of the property still needs a damnit operator. I believe that this cannot be solved generally.
Is there a way to further improve my improved version of the extensions? Are there any issues with them I do not see?
1: The statement that no Select call is needed for nullable reference types holds only since C# 10 (or since C# 9?). Till then, T?
in the signature of a generic method was treated just like T
even for reference types. There is a related Q&A on Stack Overflow, which is constrained to C# 8.0. We have no such constraint -- currently, we are using .NET 6 with C# 10, but we will upgrade to .NET 7 and C# 11 once they become generally available. Calling .Select(e => e!)
wastes performance -- it wraps the whole enumerable into another enumerable, generates an instance of the lambda, and actually executes it for each element of the collection. Better evade it if possible.
.Select(e => e!)
-- it wraps the whole enumerable into another enumerable, generates an instance of the lambda and actually executes it for each element of the collections. \$\endgroup\$foreach
withyield return item;
instead of usingLinq
.? \$\endgroup\$Linq
optimizations would be very useful for routine coding, this would help to avoid code redundancy and also improve celebrative coding experience. However, when doing extensions or libraries, it would be a good idea to evaluate your code requirements and see ifLinq
would be an advantage or disadvantage. \$\endgroup\$