Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
C# 10.0 improves handling of nullable references in generic types - no more AllowNull

A couple of years ago I blogged about how C# lets us transcend the type system with the AllowNull attribute. At the time I noted that in one of these case—generic code that wants to allow nullability when using type parameters—that the obvious solution turned out to work. You'd think that you could just write, say T?, but the compiler would reject that. Well in C# 10.0, it does work!

(Strictly speaking this was fixed in C# 9.0 and .NET 5.0. However, that wasn't an LTS release. For most of my work, non-LTS releases aren't really a viable target, so for me, this feature effectively became available with C# 10.0 and .NET 6.0.)

The non-generic case

To explain the scenario at hand, let's first look at some non-generic code to understand what we're looking to express. Here's an interface from the .NET runtime libraries:

public interface IComparable
{
    int CompareTo(object? obj);
}

This expresses ordered comparison, enabling you to determine whether one value is less then, equal to, or greater than some other value.

The Introduction to Rx.NET (v6.1) 3rd Edition (2025) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

CompareTo's only argument is of type object?, indicating that implementations of this interface are required to be able to cope with null inputs. Before nullable reference types were introduced in C# 8.0, this interface had to be declared with just object here but it still worked the same way: implementations were required to accept null, it's just that this wasn't previously formally captured in the interface definition.

The generic case, before nullable reference types

IComparable was introduced before generics, which is why the argument is of type object. This caused a problem for value types. For example, all the built-in numeric types implement IComparable, but if you pass an int to this version of CompareTo, the compiler would generate code to box the value. That enabled an int to be passed as an object but it was inefficient. Also, the use of object failed to convey the idea that this interface is intended for comparing like with like. Generics enabled both of these problems to be addressed:

public interface IComparable<in T>
{
    int CompareTo(T other);
}

What does this say about nulls? Nothing much. If you supply a value type as the type argument, e.g., IComparable<int>, then it's not possible to pass a null to CompareTo because null is not a valid value for int. But if you use a reference type, this definition doesn't tell us what to expect; we had to look at the documentation to know that implementations for which T was a reference type were expected to accept a null input. Back before nullable reference types were introduced in C# 8.0, there was no way to express this formally in a method signature.

C# 8.0 era nullability

As I showed in my blog from a couple of years back, in C# 8.0 and .NET Core 3.1, the interface definition was modified with the addition of an AllowNull attribute:

public interface IComparable<in T>
{
    int CompareTo([AllowNull] T other);
}

This states explicitly that in cases where the type argument supplied for T is a reference type, nulls are allowed. For example, we might write either IComparable<string?> or IComparable<string>. If it weren't for that [AllowNull], the CompareTo method would accept nulls only in the first case (because the type argument is the nullable string?). But because of that [AllowNull], if the type argument is a non-nullable reference type we can pass null to CompareTo. So we're allowed to do that even with IComparable<string>.

In that old blog I explained why the more obvious approach of writing T? wouldn't work. That would mean quite different things in cases where the type parameter was a value type. Take IComparable<int> for example. The type int? is a very different sort of a thing from string?. The types string and string? have identical runtime representations, the only difference being that the C# compiler tries its best to stop you from assigning a null value into a string?. But int and int? have completely different runtime representations.

Programming C# 12 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

A "nullable" value type is really a syntactic shorthand for Nullable<T>, which is actually a value type. The C# compiler can end up generating quite different code for Nullable<T> compared to what it produces for otherwise identical-looking source code that uses some nullable reference type (because Nullable<T> gets special recognition from the compiler), so if we were allowed to write T?, the compiler wouldn't always know what to do when compiling generic code.

And yet...

C# 10.0 era nullability

If you look at the definition of IComparable<T> in a C# 10 project, you'll see it looks like this:

public interface IComparable<in T>
{
    int CompareTo(T? other);
}

That seems like a much more natural thing to write. But what about the problem I described with value types? The design for this feature (officially called Unconstrained type parameter annotations) states that when a method argument using a type parameter is declared as nullable in this way, it is equivalent to the older way of using [AllowNull].

The effect is that while this does what you'd expect for reference types, it is essentially ignored when the type argument is a value type. This becomes clear when you try to implement this interface for specific value and reference types, as these examples show:

class CI : IComparable<int>
{
    public int CompareTo(int other)
    {
        return 42.CompareTo(other);
    }
}

class CS : IComparable<string>
{
    public int CompareTo(string? other)
    {
        return 42.CompareTo(other);
    }
}

This seems a little surprising in the IComparable<int> case—it just looks like the ? has gone missing. But in practice this aligns with how IComparable<int> has always worked since it was introduced back in 2005. For it to suddenly require an implementation to make CompareTo take an int? would be a major breaking change. Given that nullability works completely differently for value types and reference types, there really are only two options for what T? could mean in generic definitions like this: either it is defined to have no meaning (which is how it was in C# 8.0), or it can be apply only when the type argument is a reference type. That latter seems more useful, which is presumably why it is now supported.

FAQs

Why couldn't you write T? in generic code before C# 9.0/10.0? The types int? and string? work completely differently at runtime. For reference types like string, the nullable and non-nullable versions have identical runtime representations - the difference is only in compiler checks. But int? is actually Nullable, a completely different value type with a different runtime representation. The compiler generates quite different code for Nullable, so if T? were allowed in generic code, the compiler wouldn't always know what to generate.
What does T? mean in C# 10.0 generic code when T is a value type? When a method argument using a type parameter is declared as T?, then unless T is constrained to be a struct, the question mark is essentially ignored when the type argument is a value type—it is as though we had written just T. This aligns with how interfaces like IComparable have always worked since 2005 - for it to suddenly require CompareTo to take an int? would be a major breaking change. The T? annotation only applies when the type argument is a reference type (or if the type parameter is constrained always to be a value type).
What was the AllowNull attribute used for in C# 8.0? In C# 8.0, the [AllowNull] attribute was applied to generic method parameters to indicate that nulls were allowed when the type argument was a reference type. For example, IComparable's CompareTo method used [AllowNull] T to indicate that even with IComparable (non-nullable), you could pass null to CompareTo.
How does IComparable<T> demonstrate the C# 10.0 nullability improvement? In C# 10.0, IComparable's CompareTo method is declared with T? instead of [AllowNull] T. This is more natural to write and read. When implementing IComparable, the parameter is just int (not int?), while implementing IComparable uses string? - matching how these types have always behaved.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked across an extraordinary breadth of computing - from embedded real-time systems and broadcast television to medical imaging and cloud-scale architectures. As Technical Fellow at endjin, he brings this deep cross-domain experience to bear on the hardest technical problems.

A 17-time Microsoft MVP in Developer Technologies, Ian is the author of O'Reilly's Programming C# 12.0 and one of the foremost authorities on the C# language and high-performance .NET development. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects.

Ian has created Pluralsight courses on WPF fundamentals, WPF advanced topics, WPF v4, and the TPL, and has given over 20 talks at conferences worldwide. Technology brings him joy.