Arrays
Arrays provide indexed storage with runtime lengths. Rectangular and nested arrays compose freely, while allocation and release remain explicit.
Array types and allocation
T[] is a one-dimensional array type. The element count belongs to the allocation expression and may be computed at runtime.
int count = GetCount();
int[] temporary = int[count];
int[] persistent = new int[count];
T[n] allocates array storage on the stack for the current scope. new T[n] allocates array storage on the heap. Both forms also accept multiple dimensions.
Array equality compares unmanaged handle identity. Two aliases of the same allocation are equal; two different allocations remain unequal even when their lengths and every element value match. Equality never traverses array contents.
Rectangular arrays
Commas inside one suffix specify the rank: T[,] has two dimensions and T[,,] has three. Both heap and stack allocations supply one length for each dimension, and an access must supply exactly that many indices.
int[,] matrix = new int[2, 3];
matrix[1, 2] = 42;
int[,,] volume = new int[2, 3, 4];
Elements use contiguous row-major storage: the last index varies fastest. There is no language-defined maximum rank or nesting depth.
Length, rank and dimensions
All arrays, including rectangular stack arrays, expose int Length, int Rank and int GetLength(int dimension). Length is the total element count, rank is the number of dimensions of this array, and dimension numbers start at zero.
int count = matrix.Length; // 6
int rank = matrix.Rank; // 2
int rows = matrix.GetLength(0); // 2
int columns = matrix.GetLength(1); // 3
These queries do not traverse the elements. A jagged array's rank describes only its outer array; it does not count the dimensions of its children.
Indexing
Use array[index] or array[i, j, ...] to read or write an element. Indices must be integer expressions; bounds are checked separately for each dimension. The receiver and each index expression are evaluated once.
persistent[0] = 10;
persistent[1] = persistent[0] + 5;
int first = persistent[0];
Jagged and mixed arrays
Nested array allocations use the heap. Stack arrays cannot have an array element type: int[2][] is invalid, while a rectangular stack allocation such as int[2, 3] is valid.
A jagged array stores other array values as elements. Allocate each child separately; children may have different lengths. A new outer array initially contains null child values.
int[][] rows = new int[2][];
rows[0] = new int[3];
rows[1] = new int[5];
rows[1][4] = 42;
delete(rows[0]);
delete(rows[1]);
delete(rows);
Suffixes are written from outermost to innermost. This order matters when mixing jagged and rectangular arrays.
// A one-dimensional array of two-dimensional matrices.
int[][,] matrices = new int[4][,];
matrices[0] = new int[2, 3];
matrices[0][1, 2] = 7;
// A two-dimensional array of one-dimensional arrays.
int[,][] grid = new int[2, 3][];
grid[1, 2] = new int[5];
delete(matrices[0]);
delete(matrices);
delete(grid[1, 2]);
delete(grid);
Deleting an outer array does not delete its children. Assigning an array copies its handle and creates an alias, not a deep copy. Release each allocation once, even if multiple elements refer to it.
Stack array lifetime
A stack array normally lives until execution leaves the scope that allocated it. It may be indexed and aliased within that scope or a nested one. An explicit move may relocate it to an enclosing local in the same function, transferring cleanup and extending the backing lifetime to that destination without heap promotion. It can be borrowed by another function through an array reference such as int[]&, subject to ordinary referent lifetime checks. Passing a stack array descriptor by value, storing its backing in an escaping field, or returning it while its backing belongs to the current callee frame remains invalid. A resumable callee that retains the array reference constrains its result to the backing storage lifetime; the caller must complete the operation before destroying that storage.
int SumPair(int left, int right)
{
int[] values = int[2];
values[0] = left;
values[1] = right;
return values[0] + values[1];
}
Stack arrays support arbitrary rectangular rank, with the same metadata, row-major indexing and dimension checks as heap arrays.
int StackMatrix()
{
int[,] values = int[2, 3];
values[1, 2] = 42;
// Length == 6, Rank == 2, GetLength(1) == 3.
return values[1, 2];
}
On scope exit, struct elements with destructors are destroyed in reverse linear order before the stack storage is reclaimed. This happens on ordinary block exit and on return, break or continue when they leave that scope. A return expression is evaluated before cleanup.
Each allocation is cleaned up once. Copying a handle into empty storage creates an alias without transferring lifetime. Replacing a live array local ends its old cleanup responsibility immediately: destructible elements are destroyed in reverse order before the new handle commits, and aliases to those elements must not be used afterward. An explicit move transfers the live cleanup registration to its destination, so the moved source is skipped. Multiple unreplaced allocations in a scope are cleaned up in reverse allocation order; an empty array has no element destructor calls.
int[] result;
{
int[] temporary = int[10];
temporary[0] = 42;
result = move temporary;
}
// result[0] is still 42; temporary is Moved.
A loop body's arrays are cleaned up on each iteration. Arrays allocated in a for initializer live until the entire loop exits. Allocations in expressions follow their containing scope too; for example, repeated allocations in a while condition remain live until the surrounding scope exits.
Do not call free or delete on a stack array or its elements. Cleanup is automatic for the allocation, but it does not infer ownership of pointer fields or recursively destroy their targets. See stack array destructors.
Heap array lifetime
A raw heap array may be passed, returned or stored, and its owner releases the allocation explicitly with delete, which destroys non-trivial elements first. free releases only the backing allocation and is appropriate only when no live element still requires destruction. A unique<T[]> instead tracks one owner, while shared<T[]> allows several strong owners.
move array invalidates that source place and transfers the raw handle or stack cleanup responsibility to the destination. Existing aliases remain aliases. Heap raw arrays still require exactly one explicit delete; ownership-managed unique<T[]> and shared<T[]> handles release automatically.
void Fill(int[] values)
{
values[0] = 42;
}
int[] values = new int[16];
Fill(values);
delete(values);
Elements are zero-initialized. Struct elements also receive field initializers and the dispatch state required by their declared interfaces or virtual members, but user constructors are not called. On release, struct element destructors run in reverse linear order before the storage is released. See array destruction.
return move is valid for heap-backed raw and ownership arrays because their backing survives the callee frame. A stack-backed array cannot be returned because its backing storage belongs to that frame, including when its length is constant. See Returning ownership.
Limits and failed checks
Each length must be a nonnegative int, and the total element count must fit int. Zero-length dimensions are allowed and make the total length zero. Allocation byte sizes must also fit the target address width.
Invalid constant lengths, overflowing constant totals and invalid constant GetLength dimensions are compilation errors when evaluable. Runtime checks cover negative lengths, total or byte-size overflow, null element or length access, out-of-bounds indices, invalid dimension numbers and failed heap allocation.
A failed runtime array check terminates execution through a trap. It does not produce a recoverable language exception or run cleanup code.
Storage and native boundaries
A heap array handle is not a pointer to its first element and its representation is not part of the C ABI. Arrays therefore cannot be passed directly through extern or export signatures.
At a C ABI boundary, pass an element pointer and an explicit length. Obtain the first element only when the array is nonempty; the native function must not retain that pointer after the allocation is freed.
extern void ProcessNative(int* values, nuint length);
void Process(int[] values)
{
if (values.Length > 0)
{
ProcessNative(&values[0], cast<nuint>(values.Length));
}
}
Array references and suspension
int[]& view = values; borrows an array descriptor. In a resumable function, a local fixed-size backing allocation can live inline in the pinned frame together with its views. External storage is never implicitly copied. See suspension lifetimes for liveness, cleanup and external borrow requirements.