Back to Blog
C#

C# Null Check: Syntax, Patterns, and Pitfalls

c# null check: Learn how to perform null checks in C# using equality operators, pattern matching, nullable types, and null-conditional operators. Avoid common pitfalls.

C#Null CheckingNullable TypesPattern MatchingNull-Conditional Operators
A C# code snippet with a guard symbol representing null check patterns and nullable types.

c# null check requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Checking for null is one of the the most frequent operations in C#. The language offers several ways to perform a null check, and the right choice depends on the context, the the type, and the behavior you want when the value is missing. This article covers the main syntaxes, explains how they behave at runtime, and highlights subtle differences that can lead to bugs if overlooked.

The Basic Null Check with Equality Operators

The most straightforward null check uses the equality operator:

if (value == null) { // handle null }

For reference types, this checks whether the reference points to no object. For nullable value types, it compares against the underlying value's default, which is effectively the same as checking HasValue is false. However, for value types that are not nullable, the compiler will reject the comparison because they cannot be null.

int number = 42; if (number == null) // Compile error: operator cannot be applied { }

To compare a value type with null, you must use a nullable type:

int? maybeNumber = GetNumber(); if (maybeNumber == null) { n // maybeNumber has no value }

nThe equality operator works, but it is not always the most expressive or safest choice, especially when overloaded operators are involved. A custom type can overload == to perform a custom comparison, which might not produce the intended null check. In that case, the is pattern is safer.

Nullable Value Types and HasValue

Nullable value types, declared with ? or Nullable<T>, have a HasValue property that indicates whether a value is present. You can check it directly:

int? count = GetCount(); if (count.HasValue) { int actual = count.Value; } else { // handle absence }

This is equivalent to count != null, but it makes the intent explicit. Accessing .Value when HasValue is false throws an InvalidOperationException. The null-coalescing operator ?? provides a safer way to extract a value:

int result = count ?? 0;

For nullable value types, the underlying type's default value is used when the nullable has no value. This is useful when you want to treat a missing value as a sensible default without an explicit check.

Null-Conditional Operators (?.) and (?[])

The null-conditional operators allow you to access members or elements only when the receiver is not null. If the receiver is null, the entire expression evaluates to null, and the rest of the chain is skipped.

string? name = person?.Name; int? length = name?.Length;

Here, if person is null, name becomes null, and length also becomes null. This avoids writing multiple nested null checks. The same works for indexers:

string? first = list?[0];

If list is null, first is null. If list is not null but the index is out of range, an exception is thrown. The null-conditional operator only guards against a null receiver, not against other runtime errors.

These operators are particularly useful in expression chains where intermediate results can be null. They also work with value types, returning a nullable result:

int? age = person?.Age;

Because Age is an int, the result is int? to represent the possibility of a missing value.

Pattern Matching for Null Checks

C# 7 introduced pattern matching, and C# 9 added the is not pattern. These provide a clear and safe way to check for null:

if (obj is null) { // handle null } if (obj is not null) { // handle non-null }

Unlike the == operator, is null and is not null always perform a reference equality check. They cannot be overloaded, so they are immune to custom equality logic. This makes them the recommended way to check for null in most modern C# code.

You can also use property patterns to check nested properties without null-conditional operators:

if (person is { Address: { City: "London" } }) { // person is not null and has an Address with City "London" }

This pattern implicitly checks that person and person.Address are not null before comparing City. It is a concise alternative to a chain of null checks.

Null Coalescing and Null Coalescing Assignment

The null-coalescing operator ?? returns the left operand if it is not null; otherwise, it returns the right operand.

string displayName = name ?? "Unknown";

If name is null, displayName gets the string "Unknown". This works for both reference and nullable value types. The right operand is evaluated only when the left operand is null, which avoids unnecessary work.

The null-coalescing assignment operator ??= assigns the right operand to the left operand only if the left operand is null:

name ??= "Default";

This is equivalent to:

if (name is null) { name = "Default"; }

Both operators reduce the amount of boilerplate code needed for common null handling patterns. They are especially useful when initializing fields or properties lazily.

Null-Forgiving Operator (Postfix !)

The null-forgiving operator ! is a compile-time hint that tells the compiler a nullable expression is actually not null. It does not perform any runtime check. It is used to suppress nullable warnings when you are certain about the value.

string name = GetName()!;

If GetName() returns string?, the ! tells the compiler to treat it as string. If the value is actually null at runtime, you will get a NullReferenceException later when you use it. The operator is a promise to the compiler, not a safeguard.

Use it sparingly. Overusing ! can hide real nullability issues and make the code less safe. Prefer explicit null checks or null-conditional operators when the value might genuinely be null.

Performance and Maintainability Considerations

From a runtime perspective, == null, is null, and is not null generate the same IL for reference types. The choice is a readability and safety decision, not a performance one. Pattern matching is preferred because it avoids overloaded equality operators and makes the intent clear.

Null-conditional operators add a small overhead because they insert a null check and a conditional branch. In most applications, this is negligible. However, in tight loops where the receiver is known to be non-null, a direct access may be faster. Profile before optimizing; the readability benefit usually outweighs micro-optimizations.

Nullable reference types are a compile-time feature. They do not affect runtime behavior. They help you catch potential null dereferences during development, but they do not enforce null safety. A value declared as string? can still be null at runtime, and a value declared as string can be null if you use ! or ignore warnings.

When designing APIs, consider whether a method can return null and document it with nullable annotations. This makes the contract explicit and helps callers decide which null check to use.

Common Pitfalls and Edge Cases

One subtle issue is the difference between == and is when a type overloads the equality operator. For example, a custom class might implement operator == to compare contents. In that case, obj == null could return false for a null object if the overload is not careful. The is null pattern always checks reference identity, so it is safer.

For nullable value types, == is translated to a check on HasValue and a comparison of the underlying value. If the underlying type overloads ==, that overload is used. This can lead to unexpected behavior if the overload has side effects.

Another edge case is using ?? with a nullable value type and a default value that is not a constant. The right operand is evaluated lazily, which is useful for expensive computations. But be careful with side effects: they only run when the left operand is null.

Finally, remember that the null-forgiving operator does not change runtime behavior. It only suppresses warnings. If you use it to silence a warning without verifying the value, you may introduce a null dereference that is hard to trace. Always ensure the value is actually non-null at that point.

When working with collections, ?[] only guards against a null collection, not an index that is out of range. If you need to both, combine it with a bounds check or use TryGetValue on dictionaries.

Understanding these nuances helps you write null checks that are correct, readable, and maintainable. Choose the syntax that best matches the intent: is null for reference checks, HasValue for nullable value types, ?. for safe member access, and ?? for providing defaults. Each has a specific role, and using them appropriately reduces bugs and makes the code easier to reason about.

c# null check: Practical Usage and Code Examples | RYUSLOG DEV