Non-nullability in C# 8.0 and F# 4.1

posted in: Language Features | 0

Once in the past, I was wondering how many times I had written null checks in C# so far. It must have been at least ten thousand times. Later, I discovered F#, which avoids null in most cases. Seeing that in principle, it can be done in .NET languages, I wrote a Visual Studio User Voice suggestion to add compile-time null reference checking to C#. The suggestion quickly rose to the top and stayed there until today.

Finally... Six years and four versions later, non-nullability will be introduced with C# 8.0. And the waiting was worth it! The C# design team found an intuitive, non-intrusive solution, which fits seamlessly into the existing syntax. What's more, the C# 8.0 approach goes even farther, and arguably is safer, than the F# approach!

Feature C# 8.0 F# 4.1
class SafeFoo { } type SafeFoo = class end
- [<AllowNullLiteral>]
type UnsafeFoo = class end
SafeFoo safefoo; val safeFoo : SafeFoo
// New in C# 8: Nullability must be
// explicitly declared with ?
SafeFoo? unsafeFoo;
- val unsafeFoo : UnsafeFoo
unsafeFoo = null; unsafeFoo = null
// Compiler warning
safeFoo = null;
// Compiler error
safeFoo = null
// Compiler warning via flow analysis
safeFoo = potentiallyGetNull();
// No compiler warning
safeFoo = potentiallyGetNull()
// Either check for null with if..., or
// use C# 8 "dammit" operator
// when you're sure it's not null:
safeFoo = potentiallyGetNull()!;
// Compiler warning via flow analysis
// No compiler warning
// Either check for null with if..., or
// use C# 2 null-coalescing operator
unsafeFoo ?? new Foo()
// or use C# 6 null-conditional operator
// or use C# 8 "dammit" operator
// when you're sure it's not null.

Some of the differences between C# 8.0 and F# are:

  • In F#, only types defined in F# are non-nullable. In C# 8.0, all types are non-nullable, regardless of the language in which they have been defined.
  • In F#, you have to decide already at the type definition level whether bindings of the type can be nullable. If a type is declared nullable, then all of its bindings are nullable without exemption. By contrast, in C#, all bindings of all types are non-nullable by default, and nullablilty can only be allowed for one binding at a time.
  • When you introduce a nullable binding in C#, flow analysis will consequencly enforce null checks. Nothing like that exists in F#. (Although, at least with regards to type inference, F#'s static program analysis is more advanced than C#'s).

To be fair, nullability is viewed from a different perspective in C# and F#. In C#, if you want a value to be optional, you typically do this by allowing null. At the same time, null is also used to signal that a variable has not been completely initialized yet. The concerns of optionality and initialization state are not separated at the language level, which is inelegant.

In F#, if a value is meant to be optional, you use the built-in F# type Option<T>. Furthermore, F# is an expression-based language from the ground up. The expressive power enables a programming style where variables (called bindings in F#) can have only one state, which is completely initialized and immutable from the beginning. In such an environment, there is no need for null at all.

The reason null exists in F# is to be compatible with the CLI and other .NET languages. And here things get a little convoluted. Even when you program only in F#, you always have to deal with non-F# types, e.g., System.String. Bindings of non-F# types are allowed to be null in F#, which can lead to bizarre constellations such as let s : string option = Some null.

Ironically, C# 8.0 will give you the foundation to define your own, custom Option type that is even more safe than F#'s built-in one (by avoiding the Some null problem). But there's no reason for doing that. Better keep it simple and stick with the new features of the C# 8.0 compiler.

More information on C# 8.0's planned nullability features can be found here: