In Solidity, arrays take on a similar structure and style as in JavaScript, but there are still some tricky language-specific details. This post serves as a primer on Solidity arrays, and attempts to summarize some of these features. As always, please let me know if I’ve gotten anything wrong.
Intro
Arrays in Solidity are a reference type, meaning that they reference existing data. This contrasts with a value type, which passes an independent copy of that value to be used. Therefore, this means that reference types can be modified through multiple different names (ie each of their references). This is similar to JavaScript reference types.
In the example below, z
and y
both reference x
and therefore either can alter (the third element in) x
when f()
is called:
In Solidity, reference types are comprised of structs, arrays, and mappings, and are more complicated to use than basic value types (ints, bools, etc) because the data location must also be explicitly declared: either memory
, storage
, or calldata
. The only exception is state variables, which are automatically assumed and can only be storage
. A previous post explores some of these differences. We will only be exploring arrays here.
In general:
memory
— has a lifetime limited to an external function call, is mutable, and scoped within a function (non-persistent, modifiable)storage
— has a lifetime limited to the lifetime of a contract it is contained in, is mutable, and is where all state variables are stored (persistent, mutable)calldata
— is similar tomemory
, is immutable, and is a special data location containing function arguments (non-persistent, non-modifiable)
New to trading? Try crypto trading bots or copy trading
Data Location Effects
The data location of an array (and other reference types) has implications on assignment behavior:
- Assignments between
storage
andmemory
(or fromcalldata
) always create an independent copy
In the example below, g(x)
can be called internally (by calling f()
externally) even though x
is a storage
array, and g
accepts memory
arrays — because the g(x)
invocation hands over a separate, independent copy of x
in memory
.
- Assignments from
memory
tomemory
only create references. This means that changes to one memory variable are also visible in all other memory variables that refer to the same data
In the example below, because both z
and zz
memory
arrays reference the same underlying memory
array (y
), changes to one memory
variable affect all others referencing the same data.
- Assignments from
storage
to a localstorage
variable also only assign a reference
In the example below, a local storage
variable z
references x
and is therefore able to update x
’s value in the 0
th index by calling f()
. The private function h(y)
can also update x
, because again a reference to x
is passed rather than a copy.
*Note that this example would not even compile if h
were neither private
nor internal
, because in that case h
’s function parameters would be required to be either memory
or calldata
— local storage
variables can only be passed in a function as a reference to an existing storage variable, and not available for public functions. They are particularly useful 1) for manipulation in-place of a reference variable or 2) within library functions to access storage data.
- All other assignments to
storage
always copy — including assignments to state variables, even if the local variable itself is just a reference
In the example below, assigning the state variable x
to either memory
or calldata
arrays is allowed because the whole array is copied into storage
. They are shown to be copies (rather than reference) because updating x[0]
has no effect on either y
or z
.
Dynamic Size vs Fixed Size Arrays
Arrays can have a compile-time fixed size, or can have a dynamic size. With a fixed size k
(ie 5 elements) and element type T
(ie uint256
), fixed size arrays are represented with T[k]
(ie uint256[5]
), and dynamic size arrays with T[]
(ie uint256[]
).
All memory
arrays have fixed size — however, dynamic arrays can also depend on runtime parameters.
Arrays can have elements of any type, including mappings or structs, although the same type restrictions apply for these reference types — mappings can only be stored in storage
data location.
Arrays can also be multi-dimensional. For example, uint[][5] memory x
would create a memory
array of 5 dynamic arrays of uint
. Indices are zero-based as in JS, and access is in the opposite direction of the declaration, meaning that to access the third uint
in the second dynamic array, use x[1][2]
.
As with other state variable types, assigning a state array as public
automatically creates a getter in Solidity. However, a numeric index must also be included as a required parameter in that getter (otherwise, it would be unknown which element to “get”, as the whole array is not returned so as to avoid high gas costs):
- therefore for an array
uint[] public x
, the third element would be accessed by calling:x(2)
Note that trying to access an array past its length causes a failing assertion.
Initializing Arrays
Dynamic memory arrays can be initialized using the new
operator. However, although dynamic, memory
arrays can not be resized — required sizes must be determined in advance, or completely copied into new memory
arrays to make updates. Newly allocated arrays are always initialized with the type’s default value (ie 0
for a uint256
):
To update these values, elements must be assigned individually:
Statically-sized (fixed size) memory arrays can be initialized by an array literal — a comma-separated list of one or more expressions, in the form[…]
. An array literal is interpreted as a statically-sized memory array with length equal to the number of expressions. The base type of the array is the type of the first expression in the list such that the other expressions can be implicitly converted to it. Otherwise it throws a type error: Unable to deduce common type for array elements
. Note that one of the elements in the list must be explicitly that type — it’s not enough to have a type that all elements can be implicitly converted to.
For example, the following is a uint8[3] memory
because the type of 1
is explicitly uint8
and the remaining values in the list (2
, 3
) can be implicitly converted to uint8
.
[1, 2, 3]
To create a uint[3] memory
instead, one of the elements must be explicitly converted to uint
:
The following would not be allowed, because bool
is not implicitly convertible to uint
.
Fixed-size memory arrays cannot be assigned to dynamically-sized memory arrays either:
Array Members
Arrays have members in similar ways to JavaScript, with a few exceptions:
length
: array length, available to all array types
push()
andpush(x)
: both only available to storage arrays
With no parameters, it appends a zero-initialized element at the end of the array and returns a reference to that element.
With parameters, it appends to the end of the array and returns nothing.
pop()
: also only available to storage arrays. It implicitly calls adelete
on the removed element. Unlike JS, it does not return that element.
Array Slices
Connected portions of arrays can also be accessed as slices, using x[start:end]
. The array values will be returned between x[start]
to x[end-1]
inclusive. Indexes must be uint256
type or implicitly convertible to it. Index access is not relative to the underlying array, but to the start of the slice (and is only available to dynamic calldata
arrays).
Other Array Details
- avoid dangling references: avoid leaving references to storage items in arrays that have been deleted or moved, for example popped off elements.
- bytes and string as arrays: they are both special arrays (
bytes
is similar tobytes1[]
, but it is packed tightly incalldata
andmemory
);string
is equal tobytes
but does not allowlength
or index access.
Hopefully that gave some good insight into Solidity arrays and some key differences with JavaScript. Thanks for reading!