NotNull and Owner
The Guidelines Support Library is a small collection of utilities for C++. Today we’ll look at how two of them can make our C# code safer and cleaner.
NotNull
The C++ GSL has a not_null<T>
type. Today’s C# version will be named in the standard C# way: NotNull<T>
. The idea behind it is to wrap a value that is not a null
reference. This allows code using a NotNull<T>
variable to be sure it’s not dealing with null
.
So how do we construct such a wrapper? It can be instructive to look at the design of the opposite type: Nullable<T>
. That is a struct
that contains a private T
field accessed with the Value
property. Let’s start out with that:
public struct NotNull<T> { private readonly T value; public NotNull(T value) { this.value = value; } public T Value { get { return value; } } }
This wrapper is very straightforward and applies no overhead to the wrapped value. However, it isn’t enforcing that the value is not null. To do that, let’s add a where
constraint so that T
is a class type that could be null. Then we can add an assertion to the constructor.
public struct NotNull<T> where T : class { private readonly T value; public NotNull(T value) { UnityEngine.Assertions.Assert.IsFalse( object.ReferenceEquals(value, null), "Can't create a NotNull from a null value"); this.value = value; } public T Value { get { return value; } } }
If a user of NotNull<T>
tries to pass null
to the constructor then the assertion will fail in debug builds. The call to Assert.IsFalse
and the string literal will be removed in release builds because Assert.IsFalse
has a [Conditional("UNITY_ASSERTIONS")]
attribute on it.
There’s still one issue remaining to address. Every struct
has an implicit default constructor. This can be accessed with either new NotNull<T>()
or default(T)
to bypass our assertion. Since we can’t override this implicit default constructor, we can’t enforce a non-null
value
field. Instead, we’ll have to enforce that the Value
property will never return null
by adding an assertion there. This brings us to the final version of the type:
/// <summary> /// Wrapper for a class object that is not a null reference. Conversions from /// and to T are null-checked with Unity assertions. /// </summary> /// /// <author> /// Jackson Dunstan, https://jacksondunstan.com/articles/4953 /// </author> /// /// <license> /// MIT /// </license> public struct NotNull<T> where T : class { /// <summary> /// The held value. Can be null if this is default-constructed. Otherwise /// this is not null. /// </summary> private readonly T value; /// <summary> /// Wrap a non-null value. /// </summary> /// /// <param name="value"> /// Value to wrap. Must be a non-null reference. /// </param> public NotNull(T value) { UnityEngine.Assertions.Assert.IsFalse( object.ReferenceEquals(value, null), "Can't create a NotNull from a null value"); this.value = value; } /// <summary> /// Get the wrapped value. Asserts that the returned value is not a null /// reference. /// </summary> /// /// <value> /// The wrapped value. /// </value> public T Value { get { UnityEngine.Assertions.Assert.IsFalse( object.ReferenceEquals(value, null), "Can't get the value of a NotNull with a null value. " + "Pass a value to the constructor to create the NotNull."); return value; } } }
Now let’s look at how to use NotNull<T>
:
// Define a function that guarantees to its callers // that a non-null value will be returned. // // Instead of returning just 'string', return 'NotNull<string>' NotNull<string> GetName() { // Wrap the string in a NotNull<string> and return the wrapper to the caller return new NotNull<string>("Jackson"); } // Define a function that requires a non-null value // // Instead of passing just 'string', pass 'NotNull<string>' void PrintNameLength(NotNull<string> name) { // Use the wrapped value Debug.Log(name.Value.Length); }
There are a few ramifications of this design. First, using a NotNull<T>
instead of just a T
indicates to users of the code that the wrapped value will never be null
. In the above example, GetName
tells its callers that a non-null
name will always be returned. Likewise, PrintNameLength
tells its callers that it requires them to pass a non-null
value. This is a boon to readability and usability.
Second, using NotNull<T>
usually moves the point at which an exception is thrown. Normally, the exception would occur at the point where the null
value is used. If PrintNameLength
took just a string
then its call to the Length
property would throw a NullReferenceException
if name
were null
. Instead, whichever code that constructs the NotNull<string>
that is eventually passed to PrintNameLength
will receive the AssertionException
. This helps track down the problem when it happens, not when its consequences eventually occur.
Third, there’s no need to add null checks when using a NotNull<T>
because the check is already present when wrapping a value or retrieving the wrapped value. This cleans up the code so readers aren’t distracted by null checks that are unrelated to the overall purpose of the code. Since the null checks are removed from release builds, this also improves performance.
Owner
The next utility from the C++ GSL is owner<T>
, translated into the C# naming convention as Owner<T>
. This is also a struct
that wraps a value. In this case, it doesn’t enforce that the value is not a null
reference. It doesn’t even enforce that T
is any particular type. This makes it tremendously easy to implement!
/// <summary> /// Wrapper for an object to indicate that ownership is being transfered. The /// new owner is responsible for either transfering ownership again or disposing /// the wrapped value. /// </summary> /// /// <author> /// Jackson Dunstan, https://jacksondunstan.com/articles/4953 /// </author> /// /// <license> /// MIT /// </license> public class Owner<T> { /// <summary> /// The wrapped value /// </summary> private readonly T value; /// <summary> /// Wrap a value. /// </summary> /// /// <param name="value"> /// Value to wrap /// </param> public Owner(T value) { this.value = value; } /// <summary> /// Get the wrapped value /// </summary> /// /// <value> /// The wrapped value. /// </value> public T Value { get { return value; } } }
So what good is Owner<T>
if all it does is wrap a value? Well, it has two related purposes. First, Owner<T>
adds the name Owner
to parameters and return values. This expresses the intent of the function more clearly than just passing an unwrapped value. For example, if a function takes an IDisposable
then should the caller or the function call Dispose
on it? The ownership of the object is unclear.
By passing a parameter as an Owner<IDisposable>
, it’s clear that the caller is handing off ownership of the wrapped IDisposable
to the function. The caller is stating that it will not call Dispose
on the parameter. It is now the responsibility of the function to either call Dispose
on the parameter or hand off the Owner<IDisposable>
object for another function to own.
Likewise, by returning an Owner<IDisposable>
, the function is handing off ownership of the wrapped IDisposable
to the caller. The function states to the caller that it will not call Dispose
on the return value. Normally this is possible because the function may be a method of a class that has that IDisposable
as a field. It is the responsibility of the caller to either call Dispose
or hand off ownership again.
The second purpose is that this transfer of ownership is explicit. Function signatures are sometimes not read by their callers, but Owner<T>
requires the caller to directly acknowledge that ownership is being transfered to or from the function. This reduces the chance of errors by requiring clear, deliberate action in an error-prone section of the code.
Now let’s see Owner<T>
in action by looking at an example of passing ownership to a function via a parameter:
byte[] ReadFile(Owner<FileStream> file) { // Read until we have the whole file int len = file.Value.Length; byte[] bytes = new byte[len]; while (len > 0) { len -= file.Value.Read(bytes, bytes.Length-len, len); } // We own the stream, so dispose it file.Value.Dispose(); // Return the file's contents return bytes; } // Open the file FileStream file = File.OpenRead("/path/to/file"); // Pass ownership to the function byte[] contents = ReadFile(new Owner<FileStream>(file));
In this example the ReadFile
function is much clearer about its intent to call Dispose
on the FileStream
than if it simply took a FileStream
parameter. By taking an Owner<FileStream>
, it tells the caller that it’s taking ownership and forces them to explicitly type new Owner<FileStream>
to pass ownership. The caller will not be surprised when ReadFile
calls Dispose
on their parameter!
Now let’s take a look at an example of a function that hands off ownership to the caller via a return value:
Owner<FileStream> OpenFile(string dir, string fileName) { // Build the full path string path = File.Combine(dir, fileName); // Open the file FileStream file = File.OpenRead(path); // Hand off ownership to the caller return new Owner<FileStream>(file); } // Acquire ownership from the function Owner<FileStream> file = OpenFile("/path/to/files", "myfile.dat"); // Pass ownership to a function byte[] contents = ReadFile(file);
Here we’ve written OpenFile
in a way that clearly states that the caller is responsible for calling Dispose
on the returned FileStream
. The caller is forced to deal with this fact by declaring their return value as Owner<FileStream>
and explicitly extracting the FileStream
out of it by typing .Value
. It’s very unlikely that the caller will forget that they now own this FileStream
and forget to call Dispose
on it.
In this particular case, the caller doesn’t use the Value
property or call Dispose
on it. That’s because they opt to instead transfer ownership of the FileStream
by calling a function that takes an Owner<FileStream>
parameter. This absolves them of the responsibility to call Dispose
because the new owner, ReadFile
in this case, is now the owner and therefore must call Dispose
.
Conclusion
Today we’ve seen two small examples of utilities from the C++ GSL. Both are easy ways to improve our C# code that imply little or even negative performance overhead. Feel free to incorporate one or both of them into your projects.
#1 by Doobius on October 29th, 2018 ·
I know there are some pretty good arguments to be made against this, but it seems to me that if a function had a NonNull parameter (with implicit type conversion), a null exception would be thrown at the call site when null is passed, without having to actually change any code at the call site (thinking lazy refactoring here).
#2 by jackson on October 31st, 2018 ·
That’s true. There are arguments to be made for both implicit and explicit conversion. I prefer the caller to explicitly acknowledge that they’re not passing null, but that does mean refactoring will take more work. If you prefer implicit conversion, it should be very easy to add it to your copy of
NotNull
.#3 by Noam on August 24th, 2019 ·
A bit late to the game, but I feel like the NotNull sort of breaks when it comes to anything derived from UnityEngine.Object, since they can in fact become null later even if they were not initially, if the object was destroyed with Destroy().
It doesn’t technically break the functionality, but I think in that use case it just wouldn’t add as much value as expected.
#4 by jackson on August 24th, 2019 ·
Note that
NotNull<T>
checks for a null reference and therefore doesn’t trigger any overloaded operators such as used byUnityEngine.Object
:So if a
UnityEngine.Object
“becomes null” later, meaning its underlying object has been destroyed, then theNotNull<UnityEngine.Object>
will still not trigger the assertion when callingget Value
because the reference is still notnull
.As for value and whether this is expected behavior, it’s really subjective. One person might think it’s strange that this code wouldn’t assert when a
GameObject
is destroyed:Another person might think it’s strange that they can’t write this code:
If you’re in the first camp, consider adding a tweaked version of
NotNull<T>
that uses overloaded operators when dealing withUnityEngine.Object
:#5 by Noam on August 26th, 2019 ·
Yes, you’re right. I guess it does really boil down to personal preference :)
#6 by Noam on August 26th, 2019 ·
Yes, you’re right. I guess it does really boil down to personal preference :)
#7 by Noam on August 26th, 2019 ·
Eek, replied to the wrong thing!