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. NotNull is a another option, but it doesn’t allow for null 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 when UNITY_ASSERTIONS is defined
  • Remove all garbage-creating functionality like Equals, GetHashCode, and ToString
  • Allow setting and resetting the T value
  • Take a default parameter in the constructor
  • Add overloaded operators for bool, true, and false
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!