CleanCoder
CleanCoder

Reputation: 2867

Is it useful in a nullable enabled c# environment still to use NotNull attribute? Or does it makes no difference then?

Is it useful in a nullable enabled c# environment still to use NotNull attribute? Or does it make no difference then?

Let's say I have a parameter

case a

public static SomeExtensionMethod<T>([NotNull] this IList<T> source) 
{
    // stuff
}

case b

public static SomeExtensionMethod<T>([NotNull] this T source) where T: notnull
{
    // stuff
}

Upvotes: 4

Views: 595

Answers (2)

Theraot
Theraot

Reputation: 40180

NotNull will serve as a hint for the caller code. Not about what you can pass, but about whatever or not what you pass there will be null after the call.

If you mark a parameter as nullable and use the NotNull attribute, Roslyn will let you pass null, and assume that after that point the reference is not null. We can satisfy this contract by throwing if null is passed. This can be useful to make assertions.

public static void NotNullList<T>([NotNull] this IList<T>? source) 
{
    if (source == null)
    {
         throw new ArgumentNullException(nameof(source));
    }
    // stuff
}

Perhaps you are thinking that we do not need such assertions. However, when a reference can be null, and we need to discard the null case. Well, of course, you may take advantage of NotNullIfNotNull instead.

Sadly, last time I checked, comparing a reference with null works as a hint to Roslyn that the reference can be null. So a simple if statement to discard null may not work as expected.


Another use of NotNull is for ref or out parameters. To guarantee to the caller that the reference will not be null.

In this example, passing null is allowed, and it is also guaranteed that the reference will not be null after the execution of the method. That is, we are saying that if you pass null, we will either throw or initialize it.

public static void DoSomethingWithRef<T>([NotNull] ref IList<T>? source) 
{
    if (source == null)
    {
         source = new List<T>();
    }
    // stuff
}

You can, of course use [return: NotNull] to communicate to the caller that return value is not null.


Now, how about generics?

If you do not constraint the generic, you can specify either a nullable or a not nullable reference type as the generic argument.

var x = new List<string?>();
var y = new List<string>();

In our generic code, without the constraint notnull, we do not know if the generic type is set to a nullable or not nullable type. Thus, the NotNull attribute remains useful to communicate to the caller when we can guarantee that something is not null.


Please keep in mind that class or struct constraint do not mix well with notnull. Code with those constraints will not be able to call cleanly into code with the notnull. In fact, notnull will tend to propagate. Futhermore, T with a notnull constraint does not allow to use T?.

Because of these reasons, I avoid the notnull constraint when possible. Sometimes even repeating myself:

    public static IEnumerable<T> ExploreSequenceUntilNull<T>(T? initial, Func<T, T?> next)
        where T : class
    {
        // ...
    }

    public static IEnumerable<T> ExploreSequenceUntilNull<T>(T? initial, Func<T, T?> next)
        where T : struct
    {
        // ...
    }

See there I'm saying that the input of next will not be null, but it is OK for it to return null. It is also OK for the initial parameter to be null. However, the returned IEnumerable will not have nulls. Yes, overload resolution can figure out which one to call. And yes, they are identical in source, except for the generic constraint. The actual code is different, as you know, for the struct T? means Nullable<T>.


What about the opposite (MaybeNull)? It remains useful with unconstrained generics, to communicate that a returned reference may be null (if the generic type allows for null). It is also similarly useful for parameters and fields in generic types.

Of course, we can just use ? when we are not dealing with a generic type.

You may also be interested in MaybeNullWhen.

Upvotes: 2

CleanCoder
CleanCoder

Reputation: 2867

sadly the documentation does not help much

AllowNull: // A non-nullable input argument may be null.
DisallowNull: // A nullable input argument should never be null.
MaybeNull: // A non-nullable return value may be null.
NotNull: // A nullable return value will never be null.
MaybeNullWhen: // A non-nullable input argument may be null when the method returns the specified bool value.
NotNullWhen: // A nullable input argument will not be null when the method returns the specified bool value.
NotNullIfNotNull: // A return value isn't null if the input argument for the specified parameter isn't null.

Upvotes: 2

Related Questions