This is the second technical blog on Solidity from Alex Pinto, a recent addition to our blockchain engineering team. You can read his first post on Working with Strings in Solidity.
There are many occasions when we want to pass to a function a group of similar data that may, or may not, be limited in number. The most basic data type for this situation is an array (and in several cases, this can be used to implement more advanced data structures). We can pass and return arrays without problems, as the following illustrates.
The above uses arrays of
uint, which represents a 256-bit integer, of unlimited size. That means I can pass any array of the correct type into the function. It also means I have to initialise the return array in
getArrayMultipliedByScalar before I can use it, since at the time
outArray_ is declared it does not allocate any memory for its elements (it could have any size).
For comparison; if I used fixed-size arrays, as below, two things happen:
- I no longer need to initialise the outgoing array.
- The compiler returns an error if the function receives an array with any other size but 3.
We can make arrays of other types, like
address – but what about multi-dimensional arrays?
We can pass bi-dimensional arrays of fixed size:
Sadly, things are more difficult with dynamic arrays.
Some languages, like BASIC and Pascal, index bi-dimensional arrays by a tuple of indices. In these languages, arrays are genuine (rectangular) matrices. But in C-derived languages, multi-dimensional arrays are arrays-of-arrays, instead of matrices. That is the case with Solidity as well, and it pays to take some time to understand what this type declaration means:
uint should be read as
(uint), that is, 4 arrays each of size 2.
This is important when we consider dynamic arrays. We could have both of these kinds:
The first example above is a fixed-size array which has 3 elements, each of which is a dynamic array. In the second case, we have a dynamic array outright, but its elements are arrays of fixed size.
I discuss below how to initialise
fixedSizeArray, which is the most interesting case of the two. Regarding
dynamicArray, because it is a dynamic array, we first must allocate memory for it using
new and then we can access the fixed-size elements. The example below works:
Initialisation of multi-dimensional dynamic arrays
Let’s explore an example similar to the above in more detail:
dynamicArray are declared as state variables of the contract, and so are by necessity storage references.
Storage arrays can not be initialised from
newexpressions, as these are of type
memory. Nevertheless, we can initialise each of the arrays inside
fixedSizeArray using memory-array expressions, as shown above.
For comparison, I included also two cases where I try to assign a memory array to an explicit storage one. In the constructor, this works, but not in the second function. Why?
This is because the types of
storageArray and of
localStorageArray are not exactly the same. The former is a state variable of the contract, and when it is referred inside the constructor, its type is
uint256 storage ref (to see this, change the assignment’s right value to something illegal, such as 7, and the error message will show you the types involved). In comparison, the type of
uint256 storage pointer. Subtle difference. In the first case, we have a reference to a location in storage, and the assignment copies the memory array to that storage. In the second case, we try to assign to a local variable which according to the documentation just creates a new reference to a previous pointer:
Assignments to local storage variables only assign a reference though, and this reference always points to the state variable even if the latter is changed in the meantime.
In the above example,
y is a pointer to the same location known as
x, and modifying one causes changes in the other. But in our case, we are trying to assign a memory array to a storage variable which, being of a different type, cannot produce a pointer to that memory location.
On the other hand, when we initialise
fixedSizeArray, we are actually referring to a storage reference. In this case we can assign from a memory array, which has the effect of completely copying the source over the target, erasing all of its previous contents.
Can we pass multi-dimensional arrays to functions?
We can use Solidity’s polymorphism to write four functions with the same name, and different signatures, exploring all combinations of dynamic and fixed-size bi-dimensional arrays.
Two of these functions are illegal, only because their particular array type cannot be passed to a function. Illegal is a bit of a strong word: the error says the type can be used, but only with the new experimental ABI encoder; and that in order to use it, it is necessary to include
pragma experimental ABIEncoderV2;. However, we then would get a warning saying that it should not be used in production code.
This restriction will likely be waived in the future, as new versions of Solidity come along, but for now, I just won’t use these features and will look for workarounds.
The common feature between these two types is that the inner type of the array – that is the type of its elements – is dynamic, of unknown size. These types cannot be passed into nor returned from a function.
I will finalise this post with another example:
The last two functions are illegal. The reason why is very consistent with everything that has been said before:
bytes are dynamic types. Specifically, they are arrays: respectively, of UTF-8 characters, and of bytes. For that reason, the above return types are not really simple uni-dimensional arrays like those of
getAddresses, but are instead bi-dimensional arrays with a dynamic inner type. And because of that, they cannot be passed into nor returned from functions at the current stage of Solidity.