Click here to Skip to main content
16,012,082 members
Articles / Programming Languages / C#

(Non-)Nullable Reference Types in C#

Rate me:
Please Sign up or sign in to vote.
3.08/5 (6 votes)
22 Sep 2024CPOL3 min read 3.8K   1   12
This post is mostly a complaint about what could be a great feature that actually became a source of security vulnerabilities.

Background

Before starting, I must say that I am a great supporter of safety-features in programming languages, and I had proposed what I called the "required" keyword for C# more than 10 years ago. Yet, I keep seeing more and more people talking how great nullable reference types are in C# and, well, I am really unpleased on how badly the feature was implemented into the language, and this post is all about it.

(Non-)Nullable Reference (Types)

So, there are three things that I don't like in nullable reference types in C#:

  1. They are about non-nullable variables;
  2. They are not types;
  3. They might make your code vulnerable to attacks.

If those brief descriptions aren't enough, let me expand on each one of them:

1. They are about non-nullable variables

Calling the new feature "nullable reference types" commits two mistakes at once. In C# (and .NET) references were always nullables. What the new feature tried to add was non-nullable references (types). Well, the "types" part is what item number 2 is all about.

2. They are not types

Non-nullable references only exist at variable declarations, be them local variables, method parameters, return "types" (not sure the best word to use here) and fields. Yet, as a type, they are still the full nullable type. Although when writing them they might look the same as nullable vs non-nullable value types, non-nullable references still use the nullable types, while the actual types for nullable and non-nullable value types are different (like, Nullable<int> and just int).

3. They might make your code vulnerable to attacks

The entire purpose of adding non-nullable references is to avoid mistakes where null values enter places where they shouldn't. Yet, the false security provided by (non-)nullable references is doing exactly the opposite. Many developers (be it by themselves, because of the tools they use or by practices enforced by the company where they work) are systematically removing null validations from their code, after all they are using non-nullable references. Unfortunately, non-nullable references can still be null and, if they aren't properly checked, nulls might enter data-structures that shouldn't allow null and cause errors to happen later. This actually introduces security vulnerabilities to components shared among different modules and threads, and makes the overall debugging experience harder, which is quite the opposite of what the feature was supposed to do.

How things could be better?

In concept, very simple. Non-nullable reference types should be their own types. They should be seen as distinct types from their nullable counterparts, possibly following the same pattern of nullable and non-nullable value-types, allowing even overloading between the nullable and non-nullable ones, and guaranteeing that the IL itself would never allow a non-nullable reference type to actually contain null, very similar to how C++ references work, as trying to cast a nullptr to a reference will cause an immediate error.

Conclusion

The conclusion here goes to two different ways. The first is that non-nullable references are just a half-backed feature on its current implementation. I am not saying you should not use it, but be aware of its pitfalls.

This gets to the second conclusion, which is to keep validating your input values, even when they are not-supposed to be null, especially if you write shared libraries that might be used outside of your project, by other developers that might not be using the non-nullable references in their own projects.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions

 
QuestionClarification Pin
Niemand2527-Sep-24 7:03
professionalNiemand2527-Sep-24 7:03 
AnswerRe: Clarification Pin
Paulo Zemek27-Sep-24 9:47
Paulo Zemek27-Sep-24 9:47 
Yes. Old code (where all references were nullable):
C#
private sealed class TestClass
{
  private readonly List<string> _list = new();

  public void Add(string value)
  {
    // Here we check for null, as strings allow null
    // but such a value is invalid for our class.
    if (value == null)
      throw new ArgumentNullException(nameof(value));

    _list.Add(value);
  }

  public int GetTotalLength()
  {
    int result = 0;

    foreach(string value in _list)
    {
      // Here we do not check for null because we never added
      // null to the list, even if references could potentially
      // be null.
      result += value.Length;
    }

    return result;
  }
}

Here is the new code using (non-)nullable references:
C#
private sealed class TestClassNonNullableReferences
{
  private readonly List<string> _list = new();

  public void Add(string value)
  {
    // We do not check for null because value is a non-nullable
    // reference. Yet, an older C# compiler or just a caller
    // ignoring a warning can still pass null.

    _list.Add(value);
  }

  public int GetTotalLength()
  {
    int result = 0;

    foreach(string value in _list)
    {
      // Here we do not check for null because we the list
      // is of non-nullable references. Unfortunately, nulls can
      // still exist in the list and cause a NullReferenceException
      // here, while the real problem lies on the Add method.
      result += value.Length;
    }

    return result;
  }
}

And, copied from Gemini's answer to "Is it still possible to get null values using non-nullable references?"

Answer:
Yes, it's true that null values can still be passed in to non-nullable reference types, even with compiler warnings.

Here are a few ways this can happen:

Caller-side control: If a method or function accepts a non-nullable reference as a parameter, the caller can still explicitly pass a null value. This can lead to a NullReferenceException at runtime.
External libraries or APIs: When using external libraries or APIs, they might not always enforce non-null contracts. This means you could receive a null value from a function that's supposed to return a non-nullable object.
Compiler limitations: In some cases, the compiler might not be able to fully detect all potential null reference scenarios, especially in complex code or when dealing with generics.
To mitigate these risks:

Validate input parameters: Always validate input parameters, even if they are declared as non-nullable. This can help catch potential null values before they cause issues. * I consider this very important.
Use defensive programming techniques: Employ defensive programming practices like null checks and conditional logic to handle potential null values gracefully.
Leverage static analysis tools: Consider using static analysis tools that can help identify potential null reference issues in your code. * I actually disagree with this point, as some of those tools are telling people to remove those null checks.
QuestionExample Pin
star__duster26-Sep-24 18:56
star__duster26-Sep-24 18:56 
AnswerRe: Example Pin
Paulo Zemek27-Sep-24 9:53
Paulo Zemek27-Sep-24 9:53 
PraiseRe: Example Pin
star__duster28-Sep-24 0:14
star__duster28-Sep-24 0:14 
GeneralRe: Example Pin
Paulo Zemek28-Sep-24 8:18
Paulo Zemek28-Sep-24 8:18 
QuestionHow does this create a security vulnerability? Pin
David Pierson23-Sep-24 17:31
David Pierson23-Sep-24 17:31 
AnswerRe: How does this create a security vulnerability? Pin
Paulo Zemek23-Sep-24 17:36
Paulo Zemek23-Sep-24 17:36 
QuestionMarking a reference as not nullable does not make it more vulnerable to attacks Pin
Peter Huber SG23-Sep-24 14:19
mvaPeter Huber SG23-Sep-24 14:19 
AnswerRe: Marking a reference as not nullable does not make it more vulnerable to attacks Pin
Paulo Zemek23-Sep-24 17:32
Paulo Zemek23-Sep-24 17:32 
GeneralComment Pin
Brandon Kothell22-Sep-24 19:44
Brandon Kothell22-Sep-24 19:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.