blockchain, cryptocurrency, network

Arrays 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.

pragma solidity ^0.4.24;
contract Arrays {
    
    function getArraySum(uint[] _array) 
        public 
        pure 
        returns (uint sum_) 
    {
        sum_ = 0;
        for (uint i = 0; i < _array.length; i++) {
            sum_ += _array[i];
        }
    }
    
    function getArrayMultipliedByScalar(uint[] _array, uint scalar) 
        public 
        pure 
        returns (uint[] memory outArray_) 
    {
        outArray_ = new uint[](_array.length);
        for (uint i = 0; i < _array.length; i++) {
            outArray_[i] = _array[i] * scalar;
        }
    }
}

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 initialize 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 initialize the outgoing array
  • The compiler returns an error if the function receives an array with any other size but 3
    function getFixedSizeArrayMultipliedByScalar(uint[3] _array, uint scalar) 
        public 
        pure 
        returns (uint[3] memory outArray_) 
    {
        assert(_array.length == 3);
        for (uint i = 0; i < _array.length; i++) {
            outArray_[i] = _array[i] * scalar;
        }
    }

We can make arrays of other types, like bool and address, but what about multi-dimensional arrays?

We can pass bi-dimensional arrays of fixed size:

    function getTableCell(uint[2][4] _table, uint _i, uint _j) 
        public 
        pure 
        returns (uint cell_)
    {
        cell_ = _table[_i][_j];
    }
    
    function getDoubleOfTable(uint[2][4] _array) 
        public 
        pure 
        returns (uint[2][4] outArray_) 
    {
        for (uint i = 0; i < _array.length; i++) {
            for (uint j = 0; j < _array[i].length; j++) {
                outArray_[i][j] = 2 * _array[i][j];
            }
        }
    }

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[2][4] should be read as (uint[2])[4], that is, 4 arrays each of size 2.

This is important when we consider dynamic arrays. We could have both of these kinds:

pragma solidity ^0.4.24;
contract Arrays {	
    uint[][3] fixedSizeArray;
    uint[2][] dynamicArray;
}

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 initialize 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:

pragma solidity ^0.4.24;
contract Arrays {

    uint[][3] fixedSizeArray;
    uint[2][] dynamicArray;

    constructor() public {
        uint[3] memory memArray = [uint(7),8,9];

        fixedSizeArray[0] = memArray;
        fixedSizeArray[1] = new uint[](4);        
        fixedSizeArray[2] = [1,3,5,7,9];
            
        
        dynamicArray = new uint[2][](3);
        dynamicArray[0] = [1,2];
        dynamicArray[1] = [3,4];
        dynamicArray[2] = [5,6];
    }   
}

Initialization of multi-dimensional dynamic arrays

Let’s explore an example similar to the above in somewhat more detail.

pragma solidity ^0.4.24;
contract Arrays {

    uint[][3] fixedSizeArray;
    uint[2][] dynamicArray;
    uint[3] storageArray;

    constructor() public {
        uint[3] memory memArray = [uint(7),8,9];

        fixedSizeArray[0] = memArray;
        fixedSizeArray[1] = new uint[](4);        
        fixedSizeArray[2] = [1,3,5,7,9];
        
        storageArray = memArray;
    }
    
    // THIS FUNCTION DOES NOT WORK
    function assignMemArrayToLocalStorageArray() public {
        uint[3] storage localStorageArray;
        uint[3] memory memArray = [uint(7),8,9];
        localStorageArray = memArray; // TypeError: Type uint256[3] memory is not implicitly convertible to expected type uint256[3] storage pointer.
    }
}

The arrays fixedSizeArray and dynamicArray are declared as state variables of the contract, and so are by necessity storage references. Storage arrays can not be initialized from new expressions, as these are of type memory. Nevertheless, we can initialize 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[3] storage ref (to see this, change the assignment’s right value to something illegal, like 7, and the error message will show you the types involved). In comparison, the type of localStorageArray is uint256[3] 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.

contract C {
    uint[] x; // the data location of x is storage
	
	// the data location of memoryArray is memory
    function f(uint[] memoryArray) public {	
		x = memoryArray; // works, copies the whole array to storage	
        var y = x; // works, assigns a pointer, data location of y is storage	
		[...]

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, and in this case we can assign from a memory array, which has the effect of copying completely the source over the target, erasing all of its previous contents.

Can we pass multi-dimensional arrays to functions?

It depends.

pragma solidity ^0.4.24;
contract Arrays {
	
	[...]
    function getArrayCell(uint[2][2] _array, uint _i, uint _j) {
        
    }    	
	
    // THIS FUNCTION IS ILLEGAL
    function getArrayCell(uint[][2] _array, uint _i, uint _j) {
        
    }
    
    function getArrayCell(uint[2][] _array, uint _i, uint _j) {
        
    }           
	
	// THIS FUNCTION IS ILLEGAL
    function getArrayCell(uint[][] _array, uint _i, uint _j) {
        
    }    	
}

We can use Solidity’s polymorphism to write 4 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 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 finalize this post with another example.

pragma solidity ^0.4.24;
contract Arrays {
 
    function getInts() returns (uint[]) {
        
    }    

    function getAdresses() returns (address[]) {
        
    }    

	// THIS FUNCTION IS ILLEGAL
    function getStrings() returns (string[]) {
        
    }    
    
	// THIS FUNCTION IS ILLEGAL
    function getBytesArray() returns (bytes[]) {
        
    }        
}

The last two functions are illegal. And the reason why is very consistent with everything that has been said before. string and 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 getInts and 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.

Leave a Reply