04 - Nullable Reference Types
Nullable Reference Types
Classically reference types are nullable (i.e. objects can always be null)
This causes often runtime errors (null reference exception)
Nullable reference types and static code analysis tries to help
Null and non-null values
A reference isn't supposed to be null:
- The variable must be initialized to a non-null value.
- The variable can never be assigned the value null.
A reference may be null:
- The variable may only be dereferenced when the compiler can guarantee that the value isn't null.
- These variables may be initialized with the default null value and may be assigned the value null in other code.
This is purely compile time feature, during execution everything is still nullable. No additional checks or emitted code.
Setup & Requirements
Starting with C# 8.0
Opt in feature:
- Mandatory in this course and next (Distributed App Development)
- Create file named “Directory.Build.props” in solution root folder
- Convert warnings to errors
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>CS8600,CS8602,CS8603,CS8613,CS8618,CS8625</WarningsAsErrors>
</PropertyGroup>
</Project>
Declaration
Declaration – add ?
to the type of variable.
String? foo = null; // ok
String bar = null; // error
Any variable where ?
isn’t appended to type is non-nullable ref type.
Null forgiving operator - !
Foo!.Length;
?: ?. ?[] ?? ??=
?:
- conditional operator (ternary conditional operator)
classify = (input > 0) ? "positive" : "negative";
?.
and?[]
- null-conditional Operators
int? length = customers?.Length;
Customer first = customers?[0];
int? count = customers?[0]?.Orders?.Count();
??
- null-coalescing operator
int count = customers?[0]?.Orders?.Count() ?? 0;
??=
- null-coalescing assignment operator
Instead of:
if (variable is null) { variable = expression; }
variable ??= expression;
Null ref – Late initialization
Solutions:
- Initialize in constructor
- Use backing field with checks
- Initialize property to null! or default!
public class Person
{
public Person()
{
Name2 = "foo";
}
public string Name1 { get; set; } // Error, not initialized
public string Name2 { get; set; } // Ok, initialized in ctor
private string? _name3;
public string Name3
{
get => _name3 ?? throw new NullReferenceException();
set => _name3 = value;
}
public string Name4 { get; set; } = default!; // or = null!}
}
Null ref - descriptive options
More descriptive options can be added via attributes.
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 argument for the specified parameter isn't null.
DoesNotReturn
- A method never returns. In other words, it always throws an exception.
DoesNotReturnIf
- This method never returns if the associated bool parameter has the specified value.
Null ref - attributes
ScreenName
– allow to set value to null, but get never returns null
ReviewComment
– can be null but cannot be set to null
Post-conditions
- Since
T?
is not allowed specify possible null return value - You can pass in null value, but return value is not null
public class Person
{
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();
[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate) { }
public void EnsureCapacity<T>([NotNull] ref T[]? storage, int size) { }
}