Documentation

Unity Engine


User Manual

Script Reference

Unity Engine


Introduction to unmanaged collections

Introduction to unmanaged collections.
Read time 3 minutesLast updated 7 days ago

The
Unity.Collections
namespace provides unmanaged C# containers that you can use in jobs and Burst-compiled code. The collections in this namespace fall into the following categories:
  • Collection types with safety checks to ensure they're used in a thread-safe way and properly disposed of. These types are in the
    Unity.Collections
    namespace and their names start with
    Native
    .
  • Collection types without safety checks. These types are in the
    Unity.Collections.LowLevel.Unsafe
    namespace and their names start with
    Unsafe
    .
The remaining collection types that don't fit into these categories aren't allocated and don't contain any pointers. These types only contain small amounts of data, and their disposal and thread safety aren't a concern.

Safety handles and checks

Native
collection types carry Unity's
AtomicSafetyHandle
, which performs validation for access, disposal, and job dependencies. The
Unsafe
equivalents don't carry these safety handles. These safety handles are separate from index bounds checking, and several
Unsafe
types, such as
UnsafeList
and
UnsafePtrList
, bounds-check their indexers via
CollectionHelper.CheckIndexInRange
. This works in the same way as the equivalent conditional checks in
Native
types.
Checks are always enabled in the Editor, and in Player builds with the Checked managed code variant. The only way to make these checks fully zero-cost is to run in a Player build that doesn't include safety checks.
It's important to understand that disabling safety checks in the Editor doesn't compile out these checks:
  • The Burst menu option to disable safety checks has a specific and limited effect: when a subset of checks are called from Burst-compiled code, they're compiled out. If Burst is disabled, or if a method runs outside of Burst for any other reason, this option has no effect on that code.
  • Disabling the jobs debugger disables a subset of jobs debugger threading checks, but doesn't reduce the overall cost of checks to zero.
Neither of these options reduces the cost of checks overall to zero.

Native versus Unsafe collections

It's best practice to use
Native
collections over the
Unsafe
equivalents, because the safety handles catch access, disposal, and job-dependency errors. However, it's sometimes necessary to use the
Unsafe
equivalents, because
Native
collection types can't contain other
Native
collections. This limitation comes from how Unity implements the
Native
safety handles. For example, if you want a list of lists, you can use either
NativeList<UnsafeList<T>>
or
UnsafeList<UnsafeList<T>>
, but you can't use
NativeList<NativeList<T>>
.
Most
Native
collections are implemented as wrappers of their
Unsafe
counterparts. For example,
NativeList
is made up of an
UnsafeList
plus the handles that the safety checks use. Because of this wrapping, a
Native
type is not free even when checks are compiled out: the internal
Unsafe
collection introduces an extra pointer indirection, which adds cost by itself even in unchecked Player builds.
Several
Native
types have
Unsafe
equivalents, for example,
NativeList
has
UnsafeList
, and
NativeHashMap
has
UnsafeHashMap
.
The following table summarizes the comparative safety features of
Native
and
Unsafe
collection types:

Behavior

Native

Unsafe

Job safety checksYesNo
Index bound checksYesPartially (when safety checks are enabled)
Containing behaviorCan only contain
Unsafe
collections
Can contain either

Passing collections by value

The way changes are reflected when you pass a collection by value differs between collection types:
  • If you pass a
    NativeList
    by value and either write into the data or resize the list, the changes are reflected in the caller's copy of the list.
  • If you pass an
    UnsafeList
    by value and write into the data, those writes are reflected in the caller's version. However, if you resize the list, that change is not reflected in the caller's copy, and moreover all subsequent writes are not reflected either.
  • If you pass a
    FixedList
    by value, nothing is reflected in the caller's copy, because the entire contents are copied when you pass it.

Additional resources