Solidity中的数组

在许多情况下,我们想要向一个函数传递一组可能受或可能不受数量限制的类似数据。这种情况最基本的数据类型是一个数组(在某些情况下,这可以用来实现更高级的数据结构)。我们可以毫无问题地传递和返回数组,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
}
}

以上使用uint数组,表示256位整数,大小不限。这意味着我可以将任何正确类型的数组传递给函数。这也意味着我必须先在getArrayMultipliedByScalar初始化返回数组才能使用它,因为在声明outArray_它没有为其元素分配任何内存(它可以有任何大小)。

为了比较,如果我使用固定大小的数组,如下所示,会发生两件事:

  • 我不再需要初始化传出的数组。
  • 如果函数接收到除3之外的任何其他大小的数组,编译器将返回错误。
1
2
3
4
5
6
7
8
9
10
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;
}
}

我们可以创建其他类型的数组,比如booladdress,但是多维数组呢?

我们可以传递固定大小的二维数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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];
}
}
}

遗憾的是,动态数组的情况更加困难。

一些语言,如BASIC和Pascal,通过索引元组索引二维数组。在这些语言中,数组是真正的(矩形)矩阵。但在C语言中,多维数组是数组数组,而不是矩阵。这也是Solidity的情况,需要一些时间来理解这种类型声明的含义:uint[2][4]应该被理解为(uint[2])[4] ,即4个数组每个长度2。

当我们考虑动态数组时,这很重要。我们可以有这两种:

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

上面的第一个例子是一个固定大小的数组,它有3个元素,每个元素都是一个动态数组。在第二种情况下,我们有一个动态数组,但它的元素是固定大小的数组。

我在下面讨论如何初始化fixedSizeArray,这是两者中最有趣的案例。关于dynamicArray,因为它是一个动态数组,我们首先必须使用new为它分配内存然后我们可以访问固定大小的元素。以下示例有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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];
}
}

多维动态数组的初始化

让我们更详细地探索一个类似于上面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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.
// TypeError: 类型uint256[3]内存不能隐式转换为预期类型uint256[3]存储指针。
}
}

数组fixedSizeArraydynamicArray被声明为契约的状态变量,因此必须存储引用。无法通过new表达式初始化Storage阵列,因为它们是类型memory。不过,我们可以使用内存数组表达式初始化fixedSizeArray每个数组,如上所示。

为了比较,我还包括两种情况,我尝试将内存数组分配给显式存储数据。在构造函数中,这可以工作,但不在第二个函数中。为什么?

这是因为storageArraylocalStorageArray的类型并不完全相同。前者是合约的状态变量,当它在构造函数中被引用时,它的类型是uint256[3] storage ref(要看到这一点,将赋值的权限值更改为非法的,如7,并且错误消息将告诉你所涉及的类型)。相比之下,localStorageArray的类型是uint256[3] storage pointer。细微差别。在第一种情况下,我们引用存储中的位置,并且赋值将内存阵列复制到该存储。在第二种情况下,我们尝试分配一个局部变量,根据文档创建一个对前一个指针的新引用:

对本地存储变量的赋值仅指定引用,并且此引用始终指向状态变量,即使后者在此期间发生更改。

1
2
3
4
5
6
7
8
contract C {
uint[] x; // the data location of x is storage. x的数据位置是存储

// the data location of memoryArray is memory. memoryArray的数据位置是内存
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. 分配指针,y的数据位置是存储
[...]

在上面的示例中,y是指向称为x的相同位置的指针,而修改一个导致另一个的更改。但在我们的例子中,我们试图将一个内存数组分配给一个存储变量,该存储变量属于不同的类型,不能生成指向该内存位置的指针。

另一方面,当我们初始化fixedSizeArray,我们实际上是指一个存储引用,在这种情况下,我们可以从一个内存数组中分配,它具有完全复制源的目标,删除它以前的所有内容。

我们可以将多维数组传递给函数吗?

这取决于。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {

}
}

我们可以使用Solidity的多态来编写具有相同名称和不同签名的4个函数,探索动态和固定大小的二维数组的所有组合。其中两个函数是非法的,只是因为它们的特定数组类型无法传递给函数。非法是一个强有力的词:错误说可以使用类型,但只能使用新的实验ABI编码器,并且为了使用它,有必要包括pragma experimental ABIEncoderV2;。但是,我们会收到警告,说不应该在生产代码中使用。

随着Solidity的新版本出现,这种限制将来可能会被放弃,但就目前而言,我只是不会使用这些功能并寻找解决方法。

这两种类型之间的共同特征是数组的内部类型,即其元素的类型,是动态的,大小未知。这些类型不能传递给函数或从函数返回。

我用另一个例子来完成这篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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[]) {

}
}

最后两个功能是非法的。其原因与以前所说的一切非常一致。stringbytes是动态类型。具体来说,它们分别是UTF-8字符和字节的数组。出于这个原因,上面的返回类型不是像getIntsgetAddresses那样简单的单维数组,而是具有动态内部类型的二维数组。因此,在当前的Solidity阶段,它们不能传递给函数或从函数返回。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是Solidity中的数组