Constructors

A constructor initializes a struct instance. It receives arguments, sets up the instance's fields and can select a base constructor before its own body runs.

Declaring a constructor

Write the struct's name followed by a parameter list and a body inside the struct. A constructor has no return type. Its body accesses the new instance's fields directly, just like an instance method.

struct Counter
{
    public int Value;

    public Counter(int initialValue)
    {
        Value = initialValue;
    }
}

Use public to allow construction outside the struct. Constructors are private by default, following the same access rules as other struct members. A constructor completes by reaching the end of its body or with return;; it does not return an expression.

The first assignment to a field without an initializer initializes that field. A later assignment on the same reachable path is a replacement: Xenon destroys or releases the previous field value before storing the new one. This distinction also applies to ownership and atomic ownership fields and is tracked across conditional branches. When paths disagree, as in if (condition) Value = first; Value = second;, or when the same assignment can execute repeatedly through a loop backedge, the constructor keeps local initialization state and selects initialization or replacement at runtime. This state is local to the constructor and does not change the struct layout or ABI.

Constructing values and heap objects

Counter(20) constructs a value of type Counter. new Counter(22) allocates storage in the heap through Xenon's target-sized native allocator, invokes the same constructor and returns Counter*.

int UseCounters()
{
    Counter local = Counter(20);
    Counter* heap = new Counter(22);

    int result = local.Value + heap->Value;
    delete(heap);
    return result; // 42
}

The constructor describes initialization; the expression determines whether the result is a value or a pointer to a heap allocation. Value construction does not itself allocate heap storage for the struct, although its constructor may allocate resources for individual fields.

Heap allocations require explicit release. delete invokes the struct's destructor before releasing the allocation; free is the storage-only operation and must not be used to skip a live object's cleanup. See manual memory for ownership and lifetime rules.

Constructor overloads

A struct may declare several constructors with different parameter lists. The compiler selects a constructor using argument count, types and allowed implicit conversions. Exact type matches are preferred to conversions; a call must have one best match.

struct Range
{
    public int Start;
    public int End;

    public Range(int end)
    {
        Start = 0;
        End = end;
    }

    public Range(int start, int end)
    {
        Start = start;
        End = end;
    }
}
Range fromZero = Range(10);       // Start = 0, End = 10
Range selected = Range(2, 10);     // Start = 2, End = 10
Range* heap = new Range(2, 10);
delete(heap);

Parameter names alone do not distinguish overloads. Once a struct declares constructors, parenthesized construction must match one of them. To provide Range() as well, declare a parameterless constructor explicitly.

Default construction

For a struct with no base and no declared constructors, T() and new T() perform default construction: initialize storage to zero, prepare dynamic dispatch when needed, then run instance field initializers.

struct Options
{
    public int Count;
    public bool Enabled;
    public int Capacity = 16;
}
Options options = Options();
// Count = 0, Enabled = false, Capacity = 16

Options* heap = new Options();
delete(heap);

A derived struct without declared constructors receives an implicit parameterless constructor that follows the base-construction rules. Base constructors are selected through that initialization path; their parameter lists do not become overloads of the derived type.

For a declared constructor, use field initializers or assignments in the constructor to give fields values before reading them. An empty declared constructor is a constructor body, not the zero-initializing default form above.

Field initialization order

For parenthesized construction, storage and any required dynamic dispatch state are prepared first. Initialization then proceeds in this order:

  1. Initialize the base part of the object, including its selected constructor.
  2. Run this struct's instance field initializers in field declaration order.
  3. Run this struct's constructor body.

This lets a derived field initializer read values established by the base constructor. The derived body sees the results of its own field initializers:

struct Base
{
    public int Stage;

    public Base(int stage)
    {
        Stage = stage;
    }
}

struct Derived : Base
{
    public int Result = Stage + 2;
    public int Marker = 7;

    public Derived(int stage) : base(stage)
    {
        Marker += 1;
    }
}
Derived value = Derived(40);
// Stage = 40, Result = 42, Marker = 8

Field initializers belong to the instance, not to the constructor's parameter scope. Use constructor parameters in the constructor body or pass them to base(...). Static fields belong to the type and are separate from this per-instance sequence.

Chaining to another constructor

Write : this(arguments) to delegate initialization to another constructor of the same struct before the current body runs. The target is selected with the normal overload rules. Its base construction and instance field initializers run once through the selected constructor; the delegating constructor then executes only its own body.

struct Value
{
    public int Data;
    public Value() { Data = 42; }
    public Value(int ignored) : this() { }
}

Initializer arguments are evaluated before the new instance is available, so they cannot read the constructor's this, fields or instance methods. A constructor cannot delegate directly to itself.

Selecting a base constructor

Write : base(arguments) between the parameter list and the body of a derived constructor. In the example above, Derived(int stage) : base(stage) passes the incoming parameter to Base(int stage).

If the base declares constructors and you omit : base(...), the compiler selects its parameterless constructor. The selected base constructor must be public and match the supplied arguments. If the base has no declared constructor, its field initializers run as part of base initialization.

Base arguments are evaluated before the derived fields and body are initialized. Use constructor parameters and expressions independent of the new instance; the derived this, its fields and its instance methods cannot be used in those arguments.

These rules also apply to an implicit parameterless constructor of a derived struct. If the base requires arguments, declare a derived constructor and forward them explicitly. See inheritance for base members and layout.

Private constructors and readonly fields

A private constructor can be called by code inside its own struct. A public static factory can expose a named way to create instances:

struct Identifier
{
    public readonly int Value;

    private Identifier(int value)
    {
        Value = value;
    }

    public static Identifier Create(int value)
    {
        return Identifier(value);
    }
}
Identifier id = Identifier.Create(42);
int value = id.Value;

A constructor may initialize a readonly instance field declared in its own struct through the new instance. The field can also have a declaration initializer. Ordinary code can read id.Value, but cannot assign it after construction. Inherited readonly fields are initialized by their declaring base type.

Brace construction

T { values } creates a struct value directly from accessible fields. Arguments follow field declaration order; the brace expression itself does not allocate heap storage for the struct.

struct Vector3
{
    public float X;
    public float Y;
    public float Z;

    public Vector3(float x, float y, float z)
    {
        X = x;
        Y = y;
        Z = z;
    }
}
float x = 1.0f;
float y = 2.0f;
float z = 3.0f;

Vector3 position = Vector3 { x, y, z };
// X = 1.0f, Y = 2.0f, Z = 3.0f

The values map to X, Y and Z by position, not by the argument variables' names. Instance field initializers run first, then the supplied values are assigned to the fields.

Parenthesized construction invokes the declared Vector3 constructor instead. Its body assigns the same field values in this example:

Vector3 position = Vector3(1.0f, 2.0f, 3.0f);

Unlike T(arguments), brace construction does not call a constructor body, even when the struct declares constructors:

struct Pair
{
    public int Left;
    public int Right;

    public Pair(int value)
    {
        Left = value;
        Right = value;
    }
}
Pair repeated = Pair(4);       // constructor: Left = 4, Right = 4
Pair separate = Pair { 2, 3 }; // fields: Left = 2, Right = 3

The same distinction applies to new T(arguments) and new T { values }; both allocate heap storage and return a pointer. For example, new Vector3 { x, y, z } returns Vector3*, which must later be released with delete.

Vector3* heap = new Vector3(1.0f, 2.0f, 3.0f);

heap->Z = 9.0f;
delete(heap);

Positional construction obeys field access rules. A private field cannot be initialized this way from outside its declaring struct. Use private fields with a public constructor or factory when callers should go through initialization logic rather than populate fields directly.

Copies and array elements

Assigning a struct value, passing it by value or returning an existing value copies its fields; it does not invoke a constructor again. Pointer and array fields retain their referenced allocations when copied. See struct value semantics.

Array creation initializes element storage to zero, applies struct field initializers and prepares any required dynamic dispatch. It does not invoke user constructors for each element. An explicit expression such as items[0] = Counter(20); constructs a value and assigns it to the selected element. See arrays for allocation and array destruction for cleanup.

Construction inside readonly code

A readonly function may construct a local instance. The compiler checks the constructor, its base calls and field initializers using the actual argument origins. Initializing the new object is permitted; hidden writes to static or global state remain forbidden.

int readonly ReadCounter(int initialValue)
{
    Counter counter = Counter(initialValue);
    return counter.Value;
}

The constructor does not need a readonly modifier for this use. Pointer and reference arguments retain their mutability rules and origins throughout the call. See constructors, destructors and accessors in readonly code.