﻿# Help the compiler, and the compiler will help you\. Subtleties of working with nullable reference types in C\#

Nullable reference types appeared in C\# 3 years ago\. By this time, they found their audience\. But even those who work with this "beast" may not know all its capabilities\. Let's figure out how to work with these types more efficiently\.

![1017_NullableReferenceTypes/image1.png](https://import.viva64.com/docx/blog/1017_NullableReferenceTypes/image1.png)

## Introduction

Nullable reference types are designed to help create a better and safer application architecture\. At the code writing stage, it is necessary to understand whether this or that reference variable can be _null_ or not, whether the method can return _null_, and so on\.

It is safe to say that every developer has encountered NRE \(_NullReferenceException_\)\. And the fact that this exception can be generated at the development stage is a good scenario, because you can fix the problem immediately\. It is much worse when the user finds the problem when working with the product\. Nullable reference types help protect against NRE\.

In this article I will talk about a number of non\-obvious features related to nullable reference types\. But it's worth starting with a brief description of these types\.

## A word about nullable reference

In terms of program execution logic, a nullable reference type is no different from a reference type\. The difference between them is only in the specific annotation that the first one has\. The annotation allows the compiler to conclude whether a particular variable or expression can be _null_\. To use nullable reference types, you need to make sure that the nullable context is enabled for the project or file \(I will describe later how to do this\)\.

To declare a nullable reference variable, add '?' at the end of the type name\.

Example:

```cpp
string? str = null;
```

Now the variable _str_ can be _null_, and the compiler will not issue a warning for this code\. If you don't add '?' when declaring a variable and assigning it with _null_, a warning will be issued\.

It is possible to suppress compiler warnings about possible writing _null_ to a reference variable that is not marked as nullable\.

Example:

```cpp
object? GetPotentialNull(bool flag)
{
  return flag ? null : new object();
}

void Foo()
{
  object obj = GetPotentialNull(false);
}
```

The _obj_ variable will never be assigned with _null_, but the compiler does not always understand this\. You can suppress the warning as follows:

```cpp
object obj = GetPotentialNull(false)!;
```

Using the '\!' operator, we "tell" the compiler that the method will definitely not return _null_\. Therefore, there will be no warnings for this code fragment\.

The functionality available when working with nullable reference types is not limited to declaring variables of that type \(using '?'\) and suppressing warnings with '\!'\. Below I'll look at the most interesting features when working with nullable reference types\.

## Working with a nullable context

There are a number of mechanisms for more flexible work with nullable reference types\. Let's look at some of them\.

### Working with attributes

Attributes can be used to tell the compiler the null\-state of various elements\. Let's look at the most interesting ones\. Check out the [documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis) to find the full list of attributes\.

To make it easier, let's introduce the term — null\-state\. The null\-state is information about whether a variable or expression can be _null_ at a given time\.

#### AllowNull

Let's look how the attribute work\. Here is an example:

```cpp
public string Name
{
  get => _name;
  set => _name = value ?? "defaultName";
}

private string _name;
```

If you write the _null_ value to the _Name_ property, the compiler will issue a warning: _Cannot convert null literal to non\-nullable reference type_\. But you can see from the implementation of the property that it can be _null_\. In this case, the "defaultName" string is assigned to the _\_name_ field\.

If you add '?' to the property type, the compiler will assume that:

* the set accessor can accept _null_ \(this is correct\);
* the get accessor can return _null_ \(this is an error\)\.

For correct implementation, it is worth adding the _AllowNull_ attribute to the property:

```cpp
[AllowNull]
public string Name
```

After that, the compiler will assume that _Name_ may be assigned with _null_, although the property's type is not marked as nullable\. If you assign the value of this property to a variable that should never be _null_, then there will be no warnings\.

#### NotNullWhen

Suppose we have a method that checks a variable for _null_\. Depending on the result of this check, the method returns a value of the _bool_ type\. This method informs us about the null\-state of the variable\.

Here's a synthetic code example:

```cpp
bool CheckNotNull(object? obj)
{
  return obj != null;
}
```

This method checks the _obj_ parameter for _null_ and returns a value of the _bool_ type depending on the check result\.

Let's use the result of this method in the condition:

```cpp
public void Foo(object? obj1)
{
  object obj2 = new object();

  if (CheckNotNull(obj1))
    obj2 = obj1;
}
```

The compiler will issue a warning to code above: _Converting null literal or possibly null value to non\-nullable type_\. But such a scenario is impossible, since the condition guarantees that _obj1_ is not _null_ in the then branch\. The problem is that the compiler doesn't understand this, so we have to help it\.

Let's change the signature of the _CheckNotNull_ method by adding the _NotNullWhen_ attribute:

```cpp
bool CheckNotNull([NotNullWhen(true)]object? obj)
```

This attribute takes a value of the _bool_ type as the first argument\. With _NotNullWhen_, we link the null\-state of the argument with the return value of the method\. In this case, we "tell" the compiler that if the method returns _true_, the argument has a value other than _null_\.

There is a peculiarity associated with this attribute\.

Here are some examples:

**Using the _out_ modifier**

```cpp
bool GetValidOrDefaultName([NotNullWhen(true)] out string? validOrDefaultName, 
                           string name)
{
  if (name == null)
  {
    validOrDefaultName = name;
    return true;
  }
  else
  {
    validOrDefaultName = "defaultName";
    return false;
  }
}
```

Here, the compiler will issue a warning: _Parameter 'validOrDefaultName' must have a non\-null value when exiting with 'true'_\. It is quite reasonable, since '\=\=' is used in the condition instead of the '\!\=' operator\. In this implementation, the method returns _true_ when _validOrDefaultName_ is _null_\.

**Using the _ref_ modifier**

```cpp
bool SetDefaultIfNotValid([NotNullWhen(true)] ref string? name)
{
  if (name == null)
    return true;

  name = "defaultName";
  return false;
}
```

We will also get a warning for this code fragment: _Parameter 'name' must have a non\-null value when exiting with 'true'_\. Similarly to the previous example, the warning is reasonable\. '\=\=' is used instead of the '\!\=' operator\.

**Without using a modifier**

```cpp
bool CheckingForNull([NotNullWhen(true)] string? name)
{
  if (name == null)
    return true;

  Console.WriteLine("name is null");
  return false;
}
```

The situation here is similar to previous cases\. If _name_ equals _null_, the method returns _true_\. Following the logic of previous examples, a warning should also be issued here: _Parameter 'name' must have a non\-null value when exiting with 'true'_\. However, there is no warning\. It's hard to say what's caused this, but it looks strange\.

#### NotNullIfNotNull

This attribute allows you to establish a relationship between the argument and the return value of the method\. If the argument is not _null_, the return value is also not _null_, and vice versa\.

Example:

```cpp
public string? GetString(object? obj)
{
  return obj == null ? null : string.Empty;
}
```

The _GetString_ method returns _null_ or an empty string, depending on the null\-state of the argument\.

Usage of this method: 

```cpp
public void Foo(object? obj)
{
  string str = string.Empty;

  if(obj != null)
    str = GetString(obj);
}
```

Compiler's warning for this code: _Converting null literal or possibly null value to non\-nullable type_\. In this case, the compiler is lying\. Assignment is performed in the body of _if_, the condition of which guarantees that _GetString_ will not return _null_\. To help the compiler, let's add the _NotNullIfNotNull_ attribute for the return value of the method:

```cpp
[return: NotNullIfNotNull("obj")]
public string? GetString(object? obj)
```

**Note\.** Starting with C\#11, you can get the parameter name using the _nameof_ expression_\._ In this case, it would be _nameof\(obj\)_\.

The _NotNullIfNotNull_ attribute takes the value of the _string_ type as the first argument — the name of the parameter, based on which the null\-state of the return value is set\. Now the compiler has information about the relationship between _obj_ and the return value of the method: if _obj_ is not _null_, the return value of the method will not be _null,_ and vice versa\.

#### MemberNotNull

Let's start with an example:

```cpp
class Person
{
  private string _name;

  public Person()
  {
    SetDefaultName();
  }

  private void SetDefaultName()
  {
    _name = "Bob";
  }
}
```

The compiler will issue a warning to this code fragment:_ Non\-nullable field '\_name' must contain a non\-null value when exiting constructor\. Consider declaring the field as nullable_\. However, the _SetDefaultName_ method is called in the constructor's body, which initializes the only field of the class\. This means that the compiler's message is false\. The _MemberNotNull_ attribute allows you to solve the problem:

```cpp
[MemberNotNull(nameof(_name))]
private void SetDefaultName()
```

This attribute takes an argument of the _string\[\]_ type with the _params_ keyword\. The strings need to match the names of the members that are initialized in the method\.

Thus, we are indicating that the value of the_ \_name_ field will not be _null_ after this method is called\. Now the compiler can understand that the field is initialized in the constructor\.

#### MemberNotNullWhen

Let's look at the example: 

```cpp
class Person
{
  static readonly Regex _nameReg = new Regex(@"^I'm \w*");

  private string _name;

  public Person(string name)
  {
    if (!TryInitialize(name))
      _name = "invalid name";
  }

  private bool TryInitialize(string name)
  {
    if (_nameReg.IsMatch(name))
    {
      _name = name;
      return true;
    }
    else
      return false;
  }
}
```

_TryInitialize_ will initialize _\_name_ if the argument's value matches some pattern\. The method returns _true_ when the field has been initialized, otherwise it returns _false_\. Depending on the result of executing _TryInitialize_, a value is assigned to the _\_name_ field in the constructor\. In this implementation, _\_name_ **cannot** be not initialized in the constructor\. However, the compiler will issue a warning: _Non\-nullable field '\_name' must contain a non\-null value when exiting constructor\. Consider declaring the field as nullable_\.

To fix the situation, you need to add the _MemberNotNullWhen_ attribute:

```cpp
[MemberNotNullWhen(true, nameof(_name))]
private bool TryInitialize(string name)
```

The type of the first argument is_ bool_, the second argument's type is _string\[\]_ \(with the _params_ keyword\)\. The attribute is used for methods with a return value of the_ bool _type\. The logic is simple: if the method returns a value that corresponds to the first argument of the attribute, the class members passed to _params_ will be considered initialized\.

#### DoesNotReturn and DoesNotReturnIf

It is not uncommon to have to create methods that throw out exceptions if something has not gone according to plan\. Unfortunately, the compiler cannot always understand that program execution will be terminated after such a method is called\.

Example: 

```cpp
private void ThrowException()
{
  throw new Exception();
}

void Foo(string? str)
{
  if (str == null)
    ThrowException();

  string notNullStr = str;
}
```

For code above, the compiler will issue a warning: _Converting null literal or possibly null value to non\-nullable type_\. However, if _str_ is _null_, the execution of the method will not reach the code fragment with the assignment, as an exception will be thrown\. Thus, at the time of assignment, the _str_ variable cannot be _null_\.

The _DoesNotReturn_ attribute allows you to tell the compiler that after executing the method marked with the attribute, the execution of the calling method stops\.

Let's add the attribute for the _throwException_:

```cpp
[DoesNotReturn]
private void ThrowException()
```

Now the compiler knows that after this method is called, control will not be returned to the calling method\. Therefore, _null_ will never be written to _notNullStr_\.

The _DoesNotReturnIf_ attribute works similarly to _DoesNotReturn_, except for checking an additional condition\.

Example: 

```cpp
private void ThrowException([DoesNotReturnIf(true)] bool flag)
{
  if(flag)
    throw new Exception();
}
```

The compiler will assume that _throwException_ will not return control to the calling method if the _flag_ parameter is set to _true_\.

### Specifying context at the project level

To change the nullable context at the project level, you need to open the project properties and select the context in the "Build" section\.

![1017_NullableReferenceTypes/image2.png](https://import.viva64.com/docx/blog/1017_NullableReferenceTypes/image2.png)

You can set the nullable context in the project file \(\.csproj\)\. You need to open this file and write the value to the _Nullable_ property:

```cpp
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>disable</Nullable>             // <=
  </PropertyGroup>
</Project>
```

It is likely that many people know that you can turn on or turn off the nullable context\. However, there are two more context options\.

#### Warnings

Behavior in the nullable warning context:

* the '?' sign does not affect the analysis in any way;
* from the point of view of the compiler, all values of the reference type can be _null_ by default;
* if you write the '?' sign, the compiler will issue a warning that it should not be used in this context;
* the compiler will issue a warning only for those parts of code where the null reference is dereferenced;
* you can indicate that the expression is not _null_ using the '\!' operator\.

This mode helps protect against exceptions of _NullReferenceException_ type\. The mode informs about the dereference of null reference\.

#### Annotations

Behavior in the nullable annotation context:

* there are no warnings related to dereference of null references and errors when working with nullable reference;
* the compiler does not issue warnings when '?' and '\!' are used\.

This mode helps make a smooth entry into the use of nullable reference types in the project\. It allows you to markup variables that can and cannot be _null_\.

### Working with preprocessor directives

Preprocessor directives are used at the file level with the \.cs extension and allow you to change the states of the nullable context for fragments of code in this file\. The way it works is similar to that described in the previous section\. Each directive starts with '\#'\.

Let's look at all possible directives:

* \#nullable disable – disables the nullable context;
* \#nullable enable – enables nullable context;
* \#nullable restore – restores the nullable context to its value at the project level;
* \#nullable disable annotations – disables annotation context;
* \#nullable enable annotations – enables annotation context;
* \#nullable restore – restores the nullable context to its value at the project level;
* \#nullable disable warnings – disables the warning context;
* \#nullable enable warnings – enables the warning context;
* \#nullable restore warnings – restores the warning context to its value at the project level\.

In fact, the _enable_ value represents the enabled context of annotations and the context of warnings, and _disable_ – on the contrary, these same contexts are in the disabled state\. So the '\#nullable enable' directive would be equivalent to writing '\#nullable enable annotations' and '\#nullable enable warnings' together\.

You can use multiple directives in one file at once\. This allows you to set a different nullable context for different code fragments\. 

Let's look at an example of such usage \(at the project level, nullable\-context is disabled\):

```cpp
.... // nullable-context is disabled in this code fragment 
#nullable enable warnings
.... // the warning context is enabled in this code fragment
#nullable enable annotations
.... // the context of warnings and annotations is enabled 
     // in this code fragment
#nullable disable annotations
.... // only the warning context is enabled in this code fragment
#nullable restore
.... // nullable-context is disabled in this code fragment  
     // (since the Nullable property – disable)
```

## Conclusion

In conclusion, being able to use nullable reference types should be of great benefit to developers\. These types allow you to make the application more secure and correct from the point of view of architecture\.

This mechanism is not without its drawbacks either\. About drawbacks, and in general about nullable reference types, my colleagues told in articles: [one](https://pvs-studio.com/en/blog/posts/csharp/0631/), [two](https://pvs-studio.com/en/blog/posts/csharp/0764/)\. The ability to add attributes makes sense largely because of the imperfection of the static analyzer\. Therefore, it is necessary to add annotations to methods, fields, etc\. manually, because the analyzer cannot understand some relationships\. For example, the relationship between the return value of a method and the null state of a variable\.

A number of drawbacks are the result of insufficient in\-depth analysis\. Such analysis cannot be done on the fly\. On the other hand, it is not required\. nullable\-context is a good help in the code\-writing process\. When part of the functionality is ready and it needs to be tested, we recommend using tools for deeper analysis – for example, PVS\-Studio\.