Remember the array type introduced earlier, able to hold some number of values of the same type? Arrays have a constraint: the number of elements must be known at compile-time. We promised a way to overcome this constraint by using array references. Now that we know how to work with owning and borrowed references, we are ready to talk about array references in detail.
Array references offer the ability to create and work with arrays whose size is determined, and is changeable, at runtime. As much as possible, working with dynamic arrays is very similar to static, fixed-size arrays. However, there are some important differences.
Let's allocate a new dynamic array:
imm nameref = &rc "Jan"
This should look familar. We use the ampersand operator and specify a region (rc) to allocate a new dynamic array. Its contents hold a copy of the 3-byte array given by the string literal above. The allocation returns an owning array reference that points to this value. Unlike regular references that point to a single value of a type, array references point to multiple values of the same type.
The type signature of an array reference reflects this difference. The type signature of nameref is &rc u8. Like a static array, array references use square brackets to indicate the reference points to a dynamic array of bytes. No number is enclosed in the brackets since we won't know the array's size until runtime.
There are several ways to specify the initial values of a dynamic array:
- Copy another array. Specify the array to copy from as part of the allocation:
imm dynarr1 = &so [1,2,3]
The array to copy from can also be a de-referenced array reference:
imm dynarr2 = *dynarr1
- Replicate an element.
Specify a count in square brackets followed by the initial value for all elements:
imm dynarr3 = &gc [4; 0.] // newarr's type: &gc f32
Both the count and the value may be expressions.
- Use an expression.
Specify an expression that calculates each element's initial value:
imm dynarr4 = &rc [4; n: n*n]
The expression will be called for each element, with n incrementing upwards from 0. The expression's returned value becomes the element's initial value and type.
Resizing and Appending
Resizing a dynamic array is only possible when the owning array reference has the uni permission. This restriction ensures memory safety, as it protects against invalidating other references that don't know the new size of the array. A resized array may end up moving to a new memory location.
Resizing uses the <- append operator, along with providing values for the new elements of the larger array:
- Append another array.
This increases the size by the number of elements being added,
then appends the specified array's values:
dynarr1 <- [4,5]
- Shrink or grow via element replication.
Specify the new size in square brackets followed by the fill value for any added elements:
dynarr2 <- [10; 0.] // Grow it. Added elements initialized to zero dynarr2 <- [2; 0.] // Shrink it
Both the count and the value may be expressions.
A reference array may be shrunk by setting a new size that is lower or the same as the current size:
dynarr2.len = 2
Slices (Borrowed References)
A slice is simply a borrowed array reference. It may be borrowed from any array or array reference in the usual way:
imm nbrSlice = &[1, 2, 3] imm textSlice = &"Karen" imm dynSlice = &*dynarr2 // from a owning array reference
These slices effectively point to all elements of the source's array.
If a static array is passed as a call argument where a slice is expected, it is automatically converted into a slice:
print("abc") // equivalent to print(&"abc")
Safety Note: Because a slice is a borrowed reference, its lifetime is limited, ensuring it can never outlive the source array it provides a view into. Depending on permissions, it may temporarily lock out access to the variable it borrows from, thereby ensuring the original array cannot be moved or resized while the slice is active.
It is also possible to create a new slice that points to some subset of the elements in some array or array reference. This is accomplished by indexing using a range operator between two unsigned integers:
- .. excludes the last specified element.
- ... includes the last specified element.
- , specifies how many elements are in the slice.
imm x = &"Abcd"[1..3] // a slice pointing to "bc" imm y = &"Abcd"[1...3] // a slice pointing to "bcd" imm z = &"Abcd"[2, 1] // a slice pointing to "c"
If either index integer is unspecified, 0 and end are assumed.
imm r = &y[1..] // a slice pointing to "cd" (end assumed) imm s = &y[..] // a slice pointing to "bcd" (0 and end assumed)
An index integer may be specified relative to the last element:
imm r = &y[$-2..] // a slice pointing to "cd"
A slice may also be created from a regular reference or a pointer:
imm slice1 = &ref[..] // Slice count from reference is always 1 imm slice2 = &ptr[0...5]
Array references (owning or borrowed) may be indexed, copied, compared, iterated over, resized, and queried for their properties. Many of these operations work the same way they do with static arrays.
Access an Element
A single element within an array may be accessed using an unsigned integer value enclosed in square brackets. This supports both getting and setting the values at a specific index:
c_array = c_array // the 4th element is now the same as the third
Indexing is automatically bounds-checked by its known size.
Borrowed Reference to an Element
One can create a borrowed reference to any element of a dynamic array in the same way as for an static array:
imm nbrRef = &nbrSlice // points to 2 imm charRef = &textSlice // points to 'r'
Borrowing a reference to a single element is not a slice. It is just a regular borrowed reference to some value.
Copy or Fill Elements
Multiple elements of an array may be copied into another array using a range in square brackets. Both content segments must have the same type and size.
arrref[1 by 2] = sliceref[0 by 2] greetings[n by 5] = "Kevin"
Similarly, a single value can be repeatedly filled into an array reference's elements:
arrref[1 .. 3] = 2
The each loop may iterate over an array reference's elements:
mut sum = 0 each x in intslice sum += x
Iteration can also obtain the index number for each value:
each i, x in intslice newslice[i] = x
Two array references may be compared for equality, so long as they have the same element type, even if one is a borrowed reference and the other an owning reference. This comparison checks for equality of both the starting pointer and the element count.
if ownref == borref ....
Comparing for order (e.g., <) is not possible directly. One can of course compare the component parts of an array reference separately.
An array reference is a "fat" reference, as it holds two pieces of information:
- a pointer to the first element of the array
- an unsigned integer specifying how many elements are in the array
The component parts of an array reference can be obtained:
imm ptr = array.ptr // The pointer to the first element imm len = array.len // The number of elements
One can create many different kinds of collection types using a single-owner array reference as a private field within some struct. The struct can define operator methods (such as indexing), so that the resulting type behaves very much like an array reference. These methods can forward these requests, as appropriate, on to the array reference field.