Learning Objectives
- Know the difference between
stringandstring? - Understand the effects of
#nullable enable/<Nullable>enable</Nullable> - Handle null safely with
?.,??,??= - Know when to use
!(null-forgiving) and when to avoid it - See the big picture of attributes such as
[NotNullWhen(true)]
Key concepts
This one line is the key to turning on NRT. #nullable enable / #nullable disable can be used in file units.
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
When on:
string x = null;→ Warning (CS8600)string? x = null;→ Normalstring? x = ...; x.Length;→ Warning (CS8602)
2) string vs string?
| notation | meaning | null assignment | Warning when accessing members |
|---|---|---|---|
string |
non-nullable | warning | None |
string? |
nullable | OK | Warning if null is not checked |
The purpose is to make it possible to immediately know "Can this variable be null?" by looking at the type information.
We've already seen it briefly in Unit 03, but we'll summarize it here.
string? name = null;
string display = name ?? "(none)"; //If null, default value
name ??= "Alice"; //Assignment only if null
int? length = name?.Length; //If null, null
char? first = name?[0]; //Indexing is also possible
This is an expression that says, “I take responsibility and guarantee it, so the compiler turns off warnings.”
string s = Console.ReadLine()!; //Asserts that input is not null.
Person p = null!; //Placeholder indicating that it will be initialized soon.
When overused, NRT loses its meaning. Only when you are really sure, and to the smallest extent possible.
5) is null vs == null
Recommended is is null. The reason is that == can involve user-defined overloading, but is is a language-level check, so it always works the same.
if (x is null) ...
if (x is not null) ...
The compiler's flow analysis is enhanced with attributes such as [NotNullWhen(true)]. The same principle applies to Dictionary.TryGetValue reporting whether the out parameter is null.
bool TryParseName(string raw, [NotNullWhen(true)] out string? value);
//returns true → informs the compiler that value is not null.
See as example
string nonNullable = "hello";
string? nullable = null;
//nonNullable = null; // CS8600 warning
//Console.WriteLine(nullable.Length); // CS8602 warning
if (nullable is not null)
{
Console.WriteLine($"Length: {nullable.Length}");
}
Execution result
non = hello
null = (none)
Note: One line from .csproj to <Nullable>enable</Nullable> makes all the difference. On by default for new .NET 8 projects.
string name = Console.ReadLine()!; //input is not null.
public string Name { get; set; } = null!; //will be filled soon, so turn off the warning.
Execution result (example input "index")
Enter your name: Jisoo
Hello, Jisoo!
Name = Alice
Note: null! often appears in fields that will soon be populated by ORM entities or dependency injection containers. If used excessively in general business code, the meaning of NRT is neutralized.
string? a = null;
if (a is null) Console.WriteLine("a is null");
string display = a ?? "(no name)";
a ??= "default";
string? upper = b?.ToUpper();
Execution result
a is null
b = hello
display = (no name)
a = default
upper = HELLO
first = h
%EB%A9%94%EB%AA%A8:+%EC%9D%B4+%EB%84%A4+%EA%B0%80%EC%A7%80+%EC %97%B0%EC%82%B0%EC%9E%90(is null,+??,+??=,+?. )%EB%A7%8C+%EC%9E%98+%EC%8D%A8%EB%8F%84+NullReferenceException+%EC%9D%98+95%+%EA%B0%80+%EC%82%AC%EB%9D%BC%EC%A7%91%EB%8B%88%EB%8B%A4.
Example 4 — NullableAnnotation : [NotNullWhen(true)]
static bool TryParseName(string raw, [NotNullWhen(true)] out string? value)
{
if (string.IsNullOrWhiteSpace(raw)) { value = null; return false; }
value = raw.Trim();
return true;
}
if (TryParseName("Alice", out string? name))
{
Console.WriteLine(name.Length); //Here, name is inferred as not null.
}
Execution result
Name Length: 5
Failure: empty == null ? True
Note: Thanks to this attribute, the caller does not need to use name! within if. It greatly increases API accuracy when writing libraries.
static Person? FindByName(IEnumerable<Person> people, string name)
{
foreach (Person p in people) if (p.Name == name) return p;
return null;
}
Person? found = FindByName(people, "Alice");
int age = found?.Age ?? -1;
if (found is { Age: > 18 } adult) Console.WriteLine($"{adult.Name} is an adult");
Execution result
Alice Age: 30
Zoe Age:
Zoe Age (default -1): -1
Alice is an adult
Note: This pattern clearly indicates “there may be no result” as a type. The calling side is neat if handled by matching ?./??part/Pattern.
Common Mistakes
- Excessive use of
!to kill warnings — Even if nulls flow in, you cannot catch them. string(non-nullable) member not initialized in constructor — CS8618 warning. Solved withrequired(Part 07) or= "".- Use
Equals(null)/== nullinstead ofis null— Same behavior, butis nullis recommended for consistency. - Assign the value received from an old library with NRT turned off to a non-nullable variable — it may be null at runtime. Receive and inspect with
?. - Use the
outparameter ofDictionary.TryGetValuewithout null checking — the compiler already knows, so it is safe within theif (dict.TryGetValue(...))branch.
Cleanup
- NRT is a function that expresses “nullability of reference types” at the type level.
- Turn on
<Nullable>enable</Nullable>or#nullable enable - Do not include null in reference types without
?(compiler prevents this) - Elegantly handles null with four operators:
?.,??,??=,is null !is a last resort — only when you can really take responsibility.
Try it yourself
cd src/EnableNrt && dotnet run
cd ../NullForgiving && dotnet run
cd ../NullPattern && dotnet run
cd ../NullableAnnotation && dotnet run
cd ../MaybeNullPattern && dotnet run