Optional<T>: A Nullable<T> Alternative
Today we’ll make a new type that addresses some of the deficiencies in Nullable<T>
. We’ll end up with a good tool for dealing with operations that may or may not produce a result or take a parameter, even in Burst-compiled code. Read on to see how it works!
Nullable
Nullable<T>
is built into C# via the syntax T?
. It’s a struct containing a T
value and a bool
that indicates whether the value is usable or not. It overloads the term “null” so that it can be used with T
, which normally can’t be null
because it’s constrained to be a struct, a primitive, or an enum.
This means there’s no way to use Nullable<T>
with a class, delegate, or any other reference type. We could use null
with these types, but that wouldn’t catch accidental usage of the value like Nullable<T>
does. NotNullnull
at all.
To enforce that the T
value is never used when it is “null”, the field is private
and the property that accesses it checks the bool
to make sure access is allowed. When a user of the Nullable<T>
tries to access a “null” value, the property throws an exception. It does this in all build types, including production, which introduces some slowness.
Nullable<T>
also attempts to behave like a T
. It provides functions like Equals
, GetHashCode
, and ToString
. Unfortunately, all of these call virtual functions of object
which causes the T
to be boxed to an object
. This boxing creates garbage for the GC to later collect, possibly causing frame spikes.
Design
Optional<T>
will address these issues, and more, by making the following changes:
T
may be any type, not just structs, primitives, and enums- Getting the
T
value only throws an exception whenUNITY_ASSERTIONS
is defined - Remove all garbage-creating functionality like
Equals
,GetHashCode
, andToString
- Allow setting and resetting the
T
value - Take a default parameter in the constructor
- Add overloaded operators for
bool
,true
, andfalse
Implementation
Immplementing these changes is extremely straightforward, so let’s just look at the full source code. It weighs in at just 73 SLOC plus a whole lot of comments.
using System; using System.Runtime.InteropServices; /// <summary> /// An value which may or may not be usable. /// </summary> /// /// <remarks> /// This type is similar to <see cref="System.Nullable{T}"/> except that /// <typeparamref name="T"/> may be any type, not just structs, it omits all /// garbage-creating functionality like /// <see cref="System.Nullable{T}.Equals(object)"/>, it allows setting and /// resetting the value, its constructor takes a default parameter, getting /// <see cref="Value"/> and casting to <typeparamref name="T"/> only throw an /// exception when <code>UNITY_ASSERTIONS</code> is defined, and it includes /// overloaded operators for bool, true, and false. /// </remarks> /// /// <typeparam name="T"> /// Type of value /// </typeparam> /// /// <author> /// https://JacksonDunstan.com/articles/5372 /// </author> /// /// <license> /// MIT /// </license> [Serializable] [StructLayout(LayoutKind.Sequential)] public struct Optional<T> { /// <summary> /// If a value is contained /// </summary> private bool m_HasValue; /// <summary> /// The contained value /// </summary> private T m_Value; /// <summary> /// Construct with a usable value /// </summary> /// /// <param name="value"> /// Usable value /// </param> public Optional(T value = default(T)) { m_HasValue = true; m_Value = value; } /// <summary> /// If there is a usable value /// </summary> public bool HasValue { get { return m_HasValue; } } /// <summary> /// Get or set the usable value. When assertions are enabled via /// UNITY_ASSERTIONS, getting it when there is no usable value /// results in an exception. /// </summary> public T Value { get { #if UNITY_ASSERTIONS if (!m_HasValue) { throw new InvalidOperationException( "Optional<T> object must have a value."); } #endif return m_Value; } set { m_HasValue = true; m_Value = value; } } /// <summary> /// Get the usable value or the default value if there is no usable value /// </summary> /// /// <returns> /// The usable value or the default value if there is no usable value. /// </returns> public T GetValueOrDefault() { return m_Value; } /// <summary> /// Get the usable value or a default value if there is no usable value /// </summary> /// /// <param name="defaultValue"> /// Value to return if there is no usable value /// </param> /// /// <returns> /// The usable value or <paramref name="defaultValue"/> if there is no /// usable value. /// </returns> public T GetValueOrDefault(T defaultValue) { return m_HasValue ? m_Value : defaultValue; } /// <summary> /// Clear any usable value /// </summary> public void Reset() { m_HasValue = false; m_Value = default(T); } /// <summary> /// Get or set the usable value. When assertions are enabled via /// UNITY_ASSERTIONS and there is no usable value, an exception is thrown. /// </summary> /// /// <param name="optional"> /// Value to convert /// </param> /// /// <returns> /// True if there is a usable value. Otherwise false. /// </returns> public static explicit operator T(Optional<T> optional) { return optional.Value; } /// <summary> /// Construct with a usable value /// </summary> /// /// <param name="value"> /// Usable value /// </param> public static explicit operator Optional<T>(T value) { return new Optional<T>(value); } /// <summary> /// Convert to a bool: (bool)opt /// </summary> /// /// <param name="optional"> /// Value to convert /// </param> /// /// <returns> /// True if there is a usable value. Otherwise false. /// </returns> public static implicit operator bool(Optional<T> optional) { return optional.m_HasValue; } /// <summary> /// Convert to truth: if (opt) /// </summary> /// /// <param name="optional"> /// Value to convert /// </param> /// /// <returns> /// True if there is a usable value. Otherwise false. /// </returns> public static bool operator true(Optional<T> optional) { return optional.m_HasValue; } /// <summary> /// Convert to falsehood: if (!opt) /// </summary> /// /// <param name="optional"> /// Value to convert /// </param> /// /// <returns> /// False if there is a usable value. Otherwise true. /// </returns> public static bool operator false(Optional<T> optional) { return !optional.m_HasValue; } }
Usage
Now let’s take a look at how we can use Optional<T>
to take advantage of some of the changes. Here’s a function that searches for a best target GameObject
and some code to use that function:
// Function clearly advertises that it might not find a target Optional<GameObject> FindBestTarget() { Optional<GameObject> bestTarget = default; // Initially has no value int bestTargetScore = int.MinValue; foreach (GameObject target in Targets) { int score = GetTargetScore(target); if (score > bestTargetScore) { bestTarget.Value = target; // Now it has a value bestTargetScore = score; } } return bestTarget; } // User code // Very clear that the returned value might not be usable // Much less clear when using 'null' // Uses 'operator bool' for terseness Optional<GameObject> bestTarget = FindBestTarget(); if (bestTarget) { Shoot(bestTarget.Value); }
Conclusion
It’s not hard to rewrite even some of the most basic types in C#. If we do, we can customize them to fit our needs at the cost of a little syntax sugar: T?
. In the case of Optional<T>
, we got benefits ranging from increased flexibility to less garbage creation to faster execution to improved usability. Hopefully you’ll find the type useful!
#1 by hessel on September 16th, 2019 ·
Why would you implement the
public static bool operator true
(and false) operators explicitly when you’re already implementing thebool
operator?#2 by jackson on September 17th, 2019 ·
I seem to recall some reason to have both, but some quick searching didn’t turn anything up. It’s probably safe to delete the
true
andfalse
operators.