C# tutorials > Modern C# Features > C# 6.0 and Later > What are non-nullable reference types (NRTs) and how do they improve null safety?

What are non-nullable reference types (NRTs) and how do they improve null safety?

Non-nullable reference types (NRTs) are a feature introduced in C# 8.0 that aim to eliminate the most common source of errors in C# code: null reference exceptions. By default, reference types are now considered non-nullable, meaning they cannot hold a null value unless explicitly marked as nullable. This allows the compiler to perform static analysis and warn you about potential null dereferences at compile time, significantly improving the robustness of your code.

Basic Introduction to NRTs

In C# 8.0 and later, reference types are non-nullable by default. This means that a variable of type string, for instance, cannot be assigned the value null without explicit indication that it can be nullable. To allow a reference type to hold null, you must use the ? suffix, making it a nullable reference type (e.g., string?). The compiler will issue warnings if you attempt to assign null to a non-nullable reference or dereference a nullable reference without checking for null first.

// Non-nullable string (cannot be null)
string name = "John";

// Nullable string (can be null)
string? nullableName = null;

// Attempting to assign null to a non-nullable string will produce a warning
// name = null; // Compiler warning: CS8600 Converting null literal or possible null value to non-nullable type.

Enabling NRTs in Your Project

To enable NRTs in your project, you need to add the <Nullable>enable</Nullable> property to your project's .csproj file. This tells the compiler to treat reference types as non-nullable by default and to perform nullability analysis. You can also set it to 'disable', 'warnings' or 'annotations' for different levels of strictness. ImplicitUsings allow for global using statements to be declared.

<!-- .csproj file -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

Dealing with Nullable References

When dealing with nullable reference types, you need to explicitly check for null before dereferencing them. The compiler will warn you if you attempt to use a nullable reference without a null check. The above code demonstrates a simple null check using an if statement. You can also use the null-conditional operator (?.) or the null-coalescing operator (??) for more concise null handling.

string? nullableName = GetNameFromDatabase();

if (nullableName != null)
{
    Console.WriteLine("Hello, " + nullableName.ToUpper());
}
else
{
    Console.WriteLine("Name not found.");
}

Using Null-Conditional and Null-Coalescing Operators

The null-conditional operator (?.) allows you to access a member of a nullable reference only if the reference is not null. If the reference is null, the expression evaluates to null. The null-coalescing operator (??) provides a default value if the left-hand operand is null. This makes your code more concise and readable while still handling null values safely.

string? nullableName = GetNameFromDatabase();

// Null-conditional operator
string? upperCaseName = nullableName?.ToUpper();

// Null-coalescing operator
string displayName = nullableName ?? "Guest";

Console.WriteLine($"Upper Case Name: {upperCaseName ?? "(Null)"}");
Console.WriteLine($"Display Name: {displayName}");

Attributes for Fine-Grained Control

C# provides attributes like [AllowNull], [DisallowNull], [NotNull], and [MaybeNull] to provide more precise control over nullability. These attributes can be used to annotate parameters, return values, and properties to indicate whether they can be null. They help the compiler perform more accurate nullability analysis and provide more informative warnings.

public class MyClass
{
    // Indicates that the method will always return a non-null value
    [return: NotNull]
    public string GetName() => "Example";

    // Indicates that the parameter cannot be null
    public void ProcessName([NotNull] string name)
    {
        Console.WriteLine(name.ToUpper());
    }

    //Indicates that the parameter can be null
     public void ProcessNullableName([AllowNull] string? name)
    {

    }
}

Concepts Behind the Snippet

The core concept behind NRTs is to treat null as an exceptional case rather than the default. This forces developers to be more explicit about when a reference can be null, leading to more robust and less error-prone code. By enabling NRTs, the compiler can help you catch potential null reference exceptions at compile time, preventing runtime errors. The use of annotations like ? and attributes helps to provide even more fine-grained control and context to the compiler.

Real-Life Use Case Section

Consider a web application where user data is retrieved from a database. Without NRTs, you might receive user data (like the user's name) as a string without knowing if it's potentially null. If you then try to use this name without checking for null, you might get a NullReferenceException. With NRTs, the database access method would return a string?, clearly indicating that the name could be null. This forces you to handle the potential null case, such as displaying a default name or logging an error. This is crucial in preventing unexpected application crashes. NRTs are valuable when working with external data sources where nullability is uncertain.

Best Practices

  • Enable NRTs in all new projects.
  • Carefully analyze existing code and migrate it to use NRTs.
  • Use nullable reference types (string?) only when a value is truly optional or could be null.
  • Always check for null when working with nullable reference types using if statements, the null-conditional operator (?.), or the null-coalescing operator (??).
  • Use attributes like [AllowNull], [DisallowNull], [NotNull], and [MaybeNull] to provide more precise nullability information.
  • Consider using code analysis tools to help identify potential null reference issues.

Interview Tip

When discussing NRTs in an interview, emphasize that they are a powerful tool for improving code quality and preventing runtime errors. Explain how they work, how to enable them, and how to use them effectively. Be prepared to discuss the trade-offs and challenges of migrating existing code to use NRTs. It's also useful to discuss the attributes used for fine-grained control. Demonstrate that you understand the benefits of NRTs and how they can contribute to building more robust and maintainable applications.

When to Use Them

Use NRTs whenever you want to improve the null safety of your C# code. They are particularly useful in large projects with complex codebases, where the potential for null reference exceptions is higher. Also use them when interfacing with external libraries or APIs where the nullability of data might be unclear. NRTs add value to any C# project seeking to minimize runtime errors and improve overall code quality.

Memory Footprint

Non-nullable reference types themselves do not directly impact memory footprint. The memory footprint is primarily determined by the object that the reference points to. However, using nullable reference types (string?) does introduce a slight overhead, as the compiler needs to track whether the reference is null or not. This overhead is typically negligible, especially compared to the benefits of improved null safety. Consider that handling the possibility of nulls with checks and exception management may have a higher overhead than the small cost of nullable tracking.

Alternatives

Before C# 8.0, developers had to rely on defensive programming techniques to handle potential null reference exceptions. This involved manually checking for null before dereferencing a reference. While this approach can be effective, it can also be tedious and error-prone. Other alternatives include using code analysis tools to identify potential null reference issues. However, NRTs provide a more comprehensive and automated approach to null safety, making them the preferred choice in modern C# development. There are also design by contract tools or approaches using validation libraries.

Pros

  • Improved null safety: NRTs help prevent null reference exceptions at compile time.
  • Enhanced code quality: NRTs encourage developers to write more robust and maintainable code.
  • Reduced runtime errors: By catching potential null reference issues early, NRTs can significantly reduce runtime errors.
  • Better code readability: NRTs make it clearer which references can be null and which cannot.
  • Compiler assistance: The compiler provides warnings and errors to help you identify and fix potential null reference issues.

Cons

  • Migration effort: Migrating existing code to use NRTs can require significant effort, especially in large projects.
  • Increased code complexity: Dealing with nullable reference types can add some complexity to your code.
  • Potential for false positives: The compiler's nullability analysis is not perfect and may sometimes produce false positive warnings.
  • Learning curve: Developers need to learn how to use NRTs effectively.
  • Requires C# 8.0 or later: Older versions of C# do not support NRTs.

FAQ

  • What happens if I don't enable NRTs in my project?

    If you don't enable NRTs, reference types will behave as they always have in previous versions of C#. They will be implicitly nullable, and the compiler will not perform nullability analysis. You will not receive any warnings about potential null reference exceptions.
  • Can I disable NRTs for specific files or regions of code?

    Yes, you can disable NRTs for specific files or regions of code using preprocessor directives. For example, you can use #nullable disable to disable NRTs for a specific file. However, it's generally recommended to enable NRTs for your entire project to get the most benefit from them.
  • How do NRTs interact with legacy code?

    When working with legacy code that doesn't use NRTs, the compiler will treat reference types as implicitly nullable. This means that you might need to add null checks or use attributes to suppress warnings. It's generally a good idea to gradually migrate legacy code to use NRTs to improve its null safety.
  • Are NRTs a replacement for unit tests?

    No, NRTs are not a replacement for unit tests. They are a complementary tool that can help you catch potential null reference exceptions at compile time. However, unit tests are still essential for verifying the correctness of your code and ensuring that it behaves as expected.