Nullable Reference Types: How to Specify "T" Type Without Constraining to Class or Struct

Receiving error about nullable type parameter even when parameter has notnull constraint

I think this issue is very similar to what is happening in this post.

Note that a T? where T : class and a T? where T : struct are represented very differently in the CLR. The former is just the CLR type T. There are not separate types in the CLR to differentiate between T and T?. T? in C# just adds extra compile time checking by the C# compiler. On the other hand, The latter is represented by the CLR type Nullable<T>.

So let's consider your method:

T? Read (Guid id);

How should this be represented in the CLR? What is the return type? The compiler don't know whether T is a reference type or a value type, so the compiler cannot decide whether the method signature should be:

T Read (Guid id);

or:

Nullable<T> Read (Guid id);

How to solve The issue with T?/nullable constraint on type parameter?

Starting with C# 9, the interface could be declared like this:

public interface ICache<TVersionIdentifier> where TVersionIdentifier : notnull
{
Task<Stream> GetAsync(string file, TVersionIdentifier version, CancellationToken cancellationToken = default);
Task<TVersionIdentifier?> GetVersionAsync(string file, CancellationToken cancellationToken = default);
// ...
}

It doesn't achieve what I try to achieve though.

An implementation of ICache<string> will correctly have the members:

public class StringVersionedCache : ICache<string>
{
public Task<Stream> GetAsync(string file, string version, CancellationToken cancellationToken = default)
{
// ...
}

public Task<string?> GetVersionAsync(string file, CancellationToken cancellationToken = default)
{
// ...
}

// ...
}

An implementation of ICache<int> will incorrectly have these members though:

public class IntVersionedCache : ICache<int>
{
public Task<Stream> GetAsync(string file, int version, CancellationToken cancellationToken = default)
{
// ...
}

// WRONG: needs to be Task<int?>
public Task<int> GetVersionAsync(string file, CancellationToken cancellationToken = default)
{
// ...
}

// ...
}

@rikki-gibson mentioned Nullable reference types: How to specify "T?" type without constraining to class or struct in the comments, thanks. It is realted and further helps understanding the issue, but doesn't solve the problem though (see above).

The imho cleanest solution I was able to come up with is described in Returning nullable and null in single C# generic method?.

Declare the interface this way:

public interface ICache<TVersionIdentifier> where TVersionIdentifier : notnull
{
Task<Stream> GetAsync(string file, [DisallowNull] TVersionIdentifier version, CancellationToken cancellationToken = default);
Task<bool> TryGetVersionAsync(string file, [NotNullWhen(true)] out TVersionIdentifier? result, CancellationToken cancellationToken = default);
// ...
}

Also note the [NotNullWhen(true)] attribute and see the corresponding docs. Thanks to C# 9 it is possible to have a nullable out parameter out TVersionIdentifier? result. If TVersionIdentifier is int, this will be int not int? (as described above). Given how Try* methods are typically used in .Net, the intention and semantics of the function are easily understandable though. Using the NotNullWhen attribute we get proper compiler null checks too. Thus, this solution meets my design goals.

A problem with Nullable types and Generics in C# 8

T? can only be used when the type parameter is known to be of a reference type or of a value type. Otherwise, we don't know whether to see it as a System.Nullable<T> or as a nullable reference type T.

Instead you can express this scenario in C# 8 by using the [MaybeNull] attribute.

#nullable enable
using System.Diagnostics.CodeAnalysis;

public class C
{
[return: MaybeNull]
public T GetDefault<T>()
{
return default!; // ! just removes warning
}
}

This attribute is only included in .NET Core 3.0+, but it is possible to declare and use the attribute internal to your project (although this is not officially supported, there's no reason to assume the behavior will break down the line). To do so, you can just add a namespace+class declaration to your code similar to the following:

namespace System.Diagnostics.CodeAnalysis
{
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class MaybeNullAttribute : Attribute { }
}

How do you specify that a generic reference type is nullable when a subclass constraint is used in c# 8?

I'm posting this so that it is easier for people to find. In my case, it wasn't actually a compilation issue, it was just Resharper's error highlighting that confused me.

To troubleshoot, just try building. If there are no errors and you use Resharper, then Resharper is the culprit. You can safely ignore it.

I'll file a report to Resharper so they can fix this soon.

Nullability and generics in .NET 6

For unconstrained (neither to struct nor to class) generic type parameter T - T? is handled differently for value types and reference types. From the docs:

  • If the type argument for T is a reference type, T? references the corresponding nullable reference type. For example, if T is a string, then T? is a string?.
  • If the type argument for T is a value type, T? references the same value type, T. For example, if T is an int, the T? is also an int.
  • If the type argument for T is a nullable reference type, T? references that same nullable reference type. For example, if T is a string?, then T? is also a string?.
  • If the type argument for T is a nullable value type, T? references that same nullable value type. For example, if T is a int?, then T? is also a int?.

This happens partially due to the differences in how nullable reference and nullable value types are handled, cause nullable value types are actually separate types - Nullable<T>.

For TestConstraints let's imagine that T is int then according to the rules the signature becomes TestConstraints<int>(int key, int nullableKey) which obviously will not compile for the TestConstraints<int>(id, nullableId) call due to the type mismatch (TestConstraints<int>(id, nullableId.Value) will compile but throw at the runtime).

For int? (Nullable<int>) the signature becomes TestConstraints<int?>(int? key, int? nullableKey) which will compile (due to the implicit conversion T -> Nullable<T>) but obviously will fail the generic constraint with the warning.

The workaround can be to introduce two overloads, one for struct, one for class and let the compiler figure it out:

private void TestConstraints<TKey>(TKey key, TKey? nullableKey)
where TKey : struct
{ }

private void TestConstraints<TKey>(TKey key, TKey? nullableKey)
where TKey : class
{ }

Inconsistent behavior in C# 8 nullable reference type handling with generics

The generic type constraint where T : IDisposable means "T must be non-nullable and must implement IDisposable". Where you have multiple generic type constraints of differing nullabilities, the constraint overall is only nullable if all constraints are nullable.

So the fact that class? is nullable gets overridden by the fact that IDisposable is not.

You need where T : class?, IDisposable?.



Related Topics



Leave a reply



Submit