DEV Community

Cover image for Annotating Nulls in C#
Matt Eland
Matt Eland Subscriber

Posted on • Edited on • Originally published at killalldefects.com

Annotating Nulls in C#

In my prior post I talked about using functional programming null handling features. While this is a valid approach, it is also one that requires a lot of code changes to achieve. If you don't want to make that drastic of a level of changes, there are a few other options to consider.

One of my favorites since I discovered ReSharper is a freely-available NuGet package called JetBrains.Annotations.

This library contains a few attributes that you can use to annotate your classes, methods, properties, fields, and parameters. While these are intended to be analyzed by the ReSharper Visual Studio add-in or the up-and-coming Rider IDE, they have value in their own right even if you don't use these tools.

NotNull / CanBeNull

Ordinarily you might have a routine that looks something like this:



public AnalysisResult Analyze(ResumeInfo resume, IContainer container)
{
    if (resume == null) throw new ArgumentNullException(nameof(resume));

    IKeywordBonusProvider bonusProvider;
    if (container != null) {
       bonusProvider = container.Resolve<IKeywordBonusProvider>();
    } else {
       bonusProvider = new EntityFrameworkKeywordProvider();
    }

    return CalculateScore(resume, bonusProvider);
}



Enter fullscreen mode Exit fullscreen mode

Looking at this, you'd have to scan the method to determine whether it's okay to pass in a null resume or container either when calling the method or when adding new code to a method.

JetBrains.Annotations has a solution for this via the NotNull and CanBeNull attributes.



[NotNull]
public AnalysisResult Analyze([NotNull] ResumeInfo resume, 
                              [CanBeNull] IContainer container)
{
    if (resume == null) throw new ArgumentNullException(nameof(resume));

    IKeywordBonusProvider bonusProvider;
    if (container != null) {
       bonusProvider = container.Resolve<IKeywordBonusProvider>();
    } else {
       bonusProvider = new EntityFrameworkKeywordProvider();
    }

    return CalculateScore(resume, bonusProvider);
}


Enter fullscreen mode Exit fullscreen mode

Here we added attributes to the two parameters indicating what we expect as far as whether they can or cannot be null. Additionally, we annotated the Analyze method's return type with NotNull to indicate that it does not ever return a null value.

These attributes are not enforced by the compiler or runtime, but they are interpreted via Rider and ReSharper.

ReSharper displaying a nullability warning

Above, ReSharper is warning about potentially passing a potentially null value into a method as a parameter decorated as NotNull.

ItemNotNull / ItemCanBeNull

Similarly to NotNull and CanBeNull, you can annotate collections with ItemNotNull and ItemCanBeNull to indicate whether or not null values are possible inside of a collection.



[NotNull, ItemCanBeNull]
public IEnumerable<string> GetTestingStrings()
{
    return new List<string> {"NUnit", "XUnit", null, "MSTest V2"};
}


Enter fullscreen mode Exit fullscreen mode

Here we note that the collection itself is not null via NotNull, but the collection can have null entries as annotated by ItemCanBeNull.

Pure

Pure methods are methods that do not have side effects outside of the method itself or on the objects passed into the method. As such, pure methods can be called infinitely with the same parameters and get the same output without any modifications being made to other objects or systems.

An extremely simple pure method is below:



[Pure]
public static int AddNumbers(int x, int y) => x + y;


Enter fullscreen mode Exit fullscreen mode

The Pure attribute here is another piece of syntax to help ReSharper and Rider offer warnings if the return value is not being used (meaning the method invoke is meaningless), but it can also be used as documentation for your team to indicate that the method should never be modified to have any observable side effects.

PublicAPI

Public API is another simple one. It essentially marks a method as intended to be consumed by something outside of the project and that the class or method should not be considered unused / dead code.

If you were building a class library, for example, you would often have classes or methods not invoked by your code (though hopefully still called by test code) that are used by external applications.

You annotate those members via the PublicAPI attribute like this:



[PublicAPI]
public void NothingLooksLikeItUsesThis() {
   // Do something important
}


Enter fullscreen mode Exit fullscreen mode

This attribute can also be helpful in restraining yourself when refactoring / restructuring an application. If you were tempted to change the signature of a public method that is invoked by something outside of your project, you might not otherwise realize it unless it had a PublicAPI attribute assigned to it - leading to potential crashes or development headaches later on.

Other Attributes

There are a number of other attributes that JetBrains uses for display and advanced code completion, but these do not necessarily add much value without those tools. See the online documentation for a larger list of available attributes.


While not for everyone and not explicitly checked by the compiler, Jetbrains.Annotations are an interesting hybrid of inline null-safety documentation and advanced IDE support if you happen to use JetBrains products. If this is something that might help you out, I recommend giving it a try as a low-risk way to be explicit about null values.

If you're looking for something with more compile-time enforcement, take a look at my article on functional programming techniques to eliminating nulls in C# code.

Top comments (2)

Collapse
 
saint4eva profile image
saint4eva

=> and return keyword, are they not the same thing? Regarding your AddNumbers method? Otherwise, nice article.

Collapse
 
integerman profile image
Matt Eland

Good catch. I've edited it in light of the expression lambda.