Skip to content

Null and references

SyntaxDescription
string s = null;Assigning to a reference type
new string[4]Array elements right after creation
new int[4][]Inner arrays right after creation
buffer == nullComparing with null
s is nullMatching against null
other != nullComparing interface values
s ?? "x"Another value if null
s ??= "x"Assign if null
target?.nameDon’t call if null
target?.Rotate(...)Don’t call if null (no return value)
#nullable enableEnabling the check
string? sTypes that can be null
#nullable disableDisabling the check
s!Treating as not null
if (s != null) { s.Length }Handling after a check
where T : notnullRestricting to types that cannot be null
SyntaxDescriptionErrorReasonInstead
c as TransformBecome null if the conversion failsTUKI0001not yetReceive it with a type pattern (c is Transform t)
a?[0]Don’t read the element if the array is nullTUKI0001runtimeWrap it in if (a != null)
IShape s = new Box(); s == nullComparing an interface implemented on a value type with nullTUKI0001by designValue types can’t be null. You can compare it if the interface is on a Behaviour.
Point p = new Point(); p == nullComparing a struct with nullCS0019by designValue types can’t be null

Calling a member on a null reference, or reading an element of a null array, stops the event there. The runtime raises the error. There is no exception mechanism, so you can’t catch it and keep running.

Reference type fields and array elements are null until you assign a value. Without an initializer, they start out null.

using UnityEngine;
using Tsukimi;
public class CvNullLiteral : TsukimiBehaviour
{
void Start()
{
string s = null;
Debug.Log(s == null); // => true
}
}
using UnityEngine;
using Tsukimi;
public class ArraysElementReferenceType : TsukimiBehaviour
{
private string[] names;
void Start()
{
names = new string[4];
Debug.Log(names[0] == null); // => true
names[0] = "hello";
Debug.Log(names[0]); // => "hello"
}
}
using UnityEngine;
using Tsukimi;
public class ArraysDeclareJagged : TsukimiBehaviour
{
private int[][] grid;
void Start()
{
grid = new int[4][];
Debug.Log(grid[0] == null); // => true
grid[0] = new int[4];
grid[0][0] = 1;
Debug.Log(grid[0][0]); // => 1
}
}

You can only check for null on reference type values. Value types can’t be null, so you can’t write the comparison at all.

using UnityEngine;
using Tsukimi;
public class ArraysCompareToNull : TsukimiBehaviour
{
private int[] buffer;
void Start()
{
Debug.Log(buffer == null); // => true
if (buffer == null)
{
buffer = new int[4];
}
Debug.Log(buffer.Length); // => 4
}
}

A form that writes the same check as == null, as a pattern (patterns).

using UnityEngine;
using Tsukimi;
public class PtConstNull : TsukimiBehaviour
{
void Start()
{
string s = null;
if (s is null) { Debug.Log(1); } // => 1
}
}

Only interfaces implemented on a Behaviour can be compared with null. Interfaces implemented on a value type can’t, because the value type itself can’t be null.

using UnityEngine;
using Tsukimi;
public interface IGadget
{
void Tick();
}
public class IfaceCompareNull : TsukimiBehaviour, IGadget
{
public IGadget other;
public void Tick() { }
void Start()
{
Debug.Log(other == null); // => true
if (other != null)
{
other.Tick();
}
}
}

A form that writes the behaviour for null inside an expression, without wrapping it in if.

using UnityEngine;
using Tsukimi;
public class ExpressionsNullCoalesce : TsukimiBehaviour
{
void Start()
{
string s = null;
string r = s ?? "x";
Debug.Log(r); // => "x"
}
}
using UnityEngine;
using Tsukimi;
public class ExpressionsNullCoalesceAssign : TsukimiBehaviour
{
void Start()
{
string s = null;
s ??= "x";
Debug.Log(s); // => "x"
}
}

When the left side is null, the call after it doesn’t run, and the whole expression becomes null. A public field not wired up in the inspector is null, so s is null in this example.

using UnityEngine;
using Tsukimi;
public class ExpressionsNullConditionalReference : TsukimiBehaviour
{
public Transform target;
void Start()
{
string s = target?.name;
Debug.Log(s); // => null
}
}
using UnityEngine;
using Tsukimi;
public class ExpressionsNullConditionalVoid : TsukimiBehaviour
{
public Transform target;
void Start()
{
target?.Rotate(Vector3.up);
}
}

Null safety divides reference types into a type that can be null (string?) and a type that cannot (string), and the compiler tracks the flow of values. If you use a possibly-null value without checking it, the compiler reports TUKI0201 and stops there. The C# compiler reports the same form as a warning, but in this language it’s an error, and the code doesn’t compile.

The check is off by default. It only applies from the line where you write #nullable enable.

The check applies only at compile time. It doesn’t remain in the built program, so it doesn’t prevent execution from stopping on null at runtime.

#nullable enable
using UnityEngine;
using Tsukimi;
public class PreNullableEnable : TsukimiBehaviour
{
void Start()
{
string s = "a";
Debug.Log(s.Length); // => 1
}
}
#nullable enable
using UnityEngine;
using Tsukimi;
public class R_nrt_assign : TsukimiBehaviour
{
void Start()
{
string s = null;
Debug.Log(s);
}
}

Adding ? to a type treats it as a value that can be null. Without it, the value is treated as one that can’t be null.

using UnityEngine;
using Tsukimi;
public class NrAnnot : TsukimiBehaviour
{
void Start()
{
#nullable enable
string? s = null;
Debug.Log(s == null); // => true
}
}
#nullable enable
using UnityEngine;
using Tsukimi;
public class R_nrt_deref : TsukimiBehaviour
{
void Start()
{
string? s = null;
Debug.Log(s.Length);
}
}
#nullable enable
using UnityEngine;
using Tsukimi;
public class NrDisable : TsukimiBehaviour
{
void Start()
{
#nullable disable
string s = null;
Debug.Log(s == null); // => true
}
}
#nullable enable
using UnityEngine;
using Tsukimi;
public class R_nrt_no_disable : TsukimiBehaviour
{
void Start()
{
string s = null;
Debug.Log(s == null);
}
}

Adding ! lets you pass a possibly-null value directly to a type without ?. Only the check is satisfied, not the value, so calling a member on an actually-null value still stops there at runtime.

#nullable enable
using UnityEngine;
using Tsukimi;
public class PeNullForgiving : TsukimiBehaviour
{
void Start()
{
string? s = Get();
string t = s!;
Debug.Log(t); // => "a"
}
private string? Get() { return "a"; }
}
#nullable enable
using UnityEngine;
using Tsukimi;
public class R_nrt_no_bang : TsukimiBehaviour
{
void Start()
{
string? s = Get();
string t = s;
Debug.Log(t);
}
private string? Get() { return "a"; }
}
#nullable enable
using UnityEngine;
using Tsukimi;
public class NullNarrowing : TsukimiBehaviour
{
void Start()
{
string? s = Get();
if (s != null) { Debug.Log(s.Length); } // => 2
}
private string? Get() { return "ab"; }
}
#nullable enable
using UnityEngine;
using Tsukimi;
public class R_nrt_no_check : TsukimiBehaviour
{
void Start()
{
string? s = Get();
Debug.Log(s.Length);
}
private string? Get() { return "ab"; }
}
using UnityEngine;
using Tsukimi;
public class GcNotNull : TsukimiBehaviour
{
void Start()
{
Debug.Log(Take<int>(1)); // => 1
}
private int Take<T>(T v) where T : notnull { return 1; }
}