C#idiomatic C#
null-safetyidioms

Idioms

Null safety

Nullability written into the types, so the compiler can tell a present value from a missing one.

C# null safety is a contract between the code and the compiler. Annotations say what may be missing, the analysis follows each value through every branch, and the operators keep the checks short. These are the decisions that keep that contract honest.

Turn nullable reference types on and keep them on

The <Nullable>enable</Nullable> project setting makes every reference type non-nullable unless it carries a ?, and starts the compiler tracking null state through the whole project. New projects get the setting from the template. A project that predates the feature can adopt it file by file with #nullable enable, working from the deepest types outward, then drop the directives once each file has migrated.

Preferred

csharp
#nullable enable

string? middle = ReadMiddleName();

Avoid

csharp
string? middle = ReadMiddleName();

Corpus: Nullable reference types · Nullable migration strategies

Put the nullability in the signature

A reference type without ? is a promise that the value is never null; string? says the value may be missing. Writing the annotation down puts the question in the signature, where the compiler checks every caller and every dereference against it, instead of leaving each reader to guess.

Preferred

csharp
string? nickname = ReadNickname();
if (nickname is not null)
{
    Console.WriteLine(nickname.Length);
}

Avoid

csharp
string nickname = ReadNickname();
Console.WriteLine(nickname.Length);

Corpus: Tutorial: Express your design intent with nullable and non-nullable reference types · Nullable reference types

Make required members part of the contract

A non-nullable member that no constructor assigns makes the compiler report CS8618. The required modifier moves that duty to the caller: the object initializer must set the member, or the code does not compile. An = null! initializer does the opposite, silencing the warning while leaving a half-built object behind.

Preferred

csharp
var order = new Order { Customer = "Ada" };

Avoid

csharp
var order = new Order();

Corpus: required modifier · Resolve nullable warnings

Use the null-forgiving operator sparingly

The ! operator changes the null state the compiler tracks for one expression and does nothing at run time. Where a check or an annotation can carry the guarantee, ! is a warning switched off. It is earned only when the guarantee lives outside the code the compiler can see: a test that passes null on purpose, or an API whose contract the compiler cannot read.

Preferred

csharp
Assert.Throws<ArgumentNullException>(() => Greet(null!));

Avoid

csharp
var name = FindName()!;
Console.WriteLine(name.Length);

Corpus: ! (null-forgiving) operator · Resolve nullable warnings

Test for null with is null and is not null

A null test is how the analysis learns a value is safe to use: inside the if (value is not null) branch every dereference of value is allowed, and the state carries on into the code that follows a guard clause. A type can overload ==, so value != null is a method call whose answer the type controls, while the patterns is null and is not null always test the actual null reference.

Preferred

csharp
if (value is not null)
{
    Use(value);
}

Avoid

csharp
if (value != null)
{
    Use(value);
}

Corpus: Null operators in C# · Patterns

Give a fallback with ?? and fill lazily with ??=

?? evaluates its right operand only when the left one is null, so the fallback work is skipped when a value already exists, and a ?? b ?? c returns the first value that is present. ??= is the assignment form: the field is built on first use and left alone afterwards.

Preferred

csharp
_index ??= BuildIndex();
return _index;

Avoid

csharp
if (_index is null)
{
    _index = BuildIndex();
}
return _index;

Corpus: ?? and ??= operators - null-coalescing operators · Null operators in C#

Traverse nullable chains with ?. and ?[]

The null-conditional operators evaluate the receiver once, skip the access when it is null, and short-circuit the rest of the chain, so order?.Customer?.Name is safe where order.Customer.Name throws. The same property makes Changed?.Invoke(...) the safe way to raise an event. A value member read this way comes back as its nullable type, so apply ?? when a plain value is needed.

Preferred

csharp
string city = order?.Customer?.Address?.City ?? "unknown";
Changed?.Invoke(this, args);

Avoid

csharp
string city = order.Customer.Address.City;

Corpus: Member access operators and expressions · Null operators in C#

Ask with TryGetValue and TryParse, do not catchCA1854

The Try pattern reports failure through a return value and hands the value over in the same call, so a case the caller expects does not throw. TryGetValue also avoids the double lookup of ContainsKey followed by the indexer, and TryParse returns false for input that Parse would reject with an exception.

Preferred

csharp
if (_scores.TryGetValue(name, out int score))
{
    return score;
}
return 0;

Avoid

csharp
if (_scores.ContainsKey(name))
{
    return _scores[name];
}

Corpus: CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method · Common collection types in C# · How to convert a string to a number

Reject a null argument at the top of the methodCA1510

A public entry point checks what it cannot work without before doing anything else. ArgumentNullException.ThrowIfNull is the compact form of the guard. Give it a value that can actually be null: a non-nullable struct, a nameof expression, or a new expression never throws there, so the call is dead code and boxes the struct.

Preferred

csharp
ArgumentNullException.ThrowIfNull(message);

Avoid

csharp
if (message is null)
{
    throw new ArgumentNullException(nameof(message));
}

Corpus: CA1510: Use ArgumentNullException throw helper · CA2264: Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'

A null string and an empty string both mean no textCA1820

An empty string is a real object that answers method calls; null is not, and any member access on it throws. To a caller both mean nothing was supplied. String.IsNullOrEmpty tests the pair in one call, where a comparison against "" misses the null case. When white space is as meaningless as absence, as in a name, String.IsNullOrWhiteSpace rejects that too instead of storing blank input.

Preferred

csharp
if (string.IsNullOrWhiteSpace(userName))
{
    throw new ArgumentException("Name is blank", nameof(userName));
}

Avoid

csharp
if (userName == "")
{
    throw new ArgumentException("Name is blank", nameof(userName));
}

Corpus: Strings · CA1820: Test for empty strings using string length · Automatically implemented properties

Record what a helper method initializes

Null analysis reads a method body on its own and does not follow calls, so a constructor that delegates to an initializer helper still reports CS8618 for the fields the helper assigns. [MemberNotNull(nameof(_window))] states the postcondition on the helper: after it returns, the named members are not null. The warning goes away without changing the constructor.

Preferred

csharp
[MemberNotNull(nameof(_window))]
void Initialize()
{
    _window = OpenWindow();
}

Avoid

csharp
void Initialize()
{
    _window = OpenWindow();
}

Corpus: Attributes interpreted by the compiler: Nullable static analysis · Resolve nullable warnings

Annotate Try methods and custom null checks

The compiler does not see inside a method it calls, so it treats an out parameter as maybe-null whatever the body proves, and it does not recognize a helper that checks for null. [NotNullWhen(true)] supplies that missing fact: when the method returns true, the argument or out value is not null. The runtime annotates TryGetValue and IsNullOrEmpty this way, and the attribute works on a custom test of your own.

Preferred

csharp
bool TryGet(string key, [NotNullWhen(true)] out string? value)

Avoid

csharp
static bool TryGet(string key, out string value)
{
    value = null;
    return false;
}

Corpus: Attributes interpreted by the compiler: Nullable static analysis · ! (null-forgiving) operator

Do not read T? as always nullable

For an unconstrained type parameter, T? means the nullable reference form only when the caller supplies a reference type; Box<int> still gets int. The constraints settle the meaning: where T : class requires a non-nullable reference type and class? allows a nullable one. A generic method that returns default for a missing result should say so with [return: MaybeNull] instead of letting callers assume the value is present.

Preferred

csharp
[return: MaybeNull]
T Find<T>(string key) => default;

Avoid

csharp
T Find<T>(string key) => default;

Corpus: Nullable reference types · Attributes interpreted by the compiler: Nullable static analysis

Extract a nullable value type with a pattern

T? over a value type is a value plus a flag: the default is null, not zero, and an operator applied to a null operand lifts the whole result to null. The type pattern is int degrees checks and extracts in one step. Value throws when the flag is false, so keep it behind HasValue or replace it with GetValueOrDefault or ??.

Preferred

csharp
if (temperature is int degrees)
{
    Console.WriteLine(degrees * 2);
}

Avoid

csharp
Console.WriteLine(temperature.Value * 2);

Corpus: Nullable value types in C# · Nullable value types