引用类型:

引用类型可以通过多个不同的名称修改它的值,而值类型的变量,每次都有独立的副本。因此,必须比值类型更谨慎地处理引用类型。 目前,引用类型包括结构,数组和映射,如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:

  • 内存memory即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。存贮在内存中
  • 存储storage状态变量保存的位置,只要合约存在就一直存储.存储在区块链中
  • 调用数据calldata用来保存函数参数的特殊数据位置,是一个只读位置。调用数据是不可修改的

赋值行为:

  • 在存储storage和内存memory之间两两赋值(或者从调用数据calldata赋值 ),都会创建一份独立的拷贝。
  • 从内存memory到内存memory的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
  • 从存储storage到本地存储变量的赋值也只分配一个引用。
  • 其他的向存储storage的赋值,总是进行拷贝。 这种情况的示例如对状态变量或存储storage的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面ArrayContract合约 更容易理解)。
  • pragma solidity >=0.5.0 <0.9.0;contract Tiny {uint[] x; // x 的数据存储位置是 storage, 位置可以忽略// memoryArray 的数据存储位置是 memoryfunction f(uint[] memory memoryArray) public {x = memoryArray; // 将整个数组拷贝到 storage 中,可行uint[] storage y = x;// 分配一个指针(其中 y 的数据存储位置是 storage),可行y[7]; // 返回第 8 个元素,可行y.pop(); // 通过 y 修改 x,可行delete x; // 清除数组,同时修改 y,可行// 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,// 但 storage 是“静态”分配的:// y = memoryArray;// 下面这一行也不可行,因为这会“重置”指针,// 但并没有可以让它指向的合适的存储位置。// delete y;g(x); // 调用 g 函数,同时移交对 x 的引用h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝}function g(uint[] storage ) internal pure {}function h(uint[] memory) public pure {}}

    数组

数组可以在声明时指定长度,也可以动态调整大小(长度)。

一个元素类型为T,固定长度为k的数组可以声明为T[k],而动态数组声明为T[]。 举个例子,一个长度为 5,元素类型为uint的动态数组的数组(二维数组),应声明为uint[][5]

数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反。

如:如果有一个变量为uint[][5]memoryx, 要访问第三个动态数组的第二个元素,使用 x[2][1],要访问第三个动态数组使用x[2]。 同样,如果有一个T类型的数组T[5]a, T 也可以是一个数组,那么a[2]总会是T类型。

访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用.push()方法在末尾追加一个新元素,其中.push()追加一个零初始化的元素并返回对它的引用。

bytesstring类型的变量是特殊的数组。bytes类似于byte[],但它在调用数据calldata和内存memory中会被“紧打包” .stringbytes相同,但不允许用长度或索引来访问。如果使用一个长度限制的字节数组,应该使用一个bytes1bytes32的具体类型,因为它们GAS便宜得多。

如果想要访问以字节表示的字符串s,请使用bytes(s).length或者bytes(s)[7]='x';

创建内存数组

pragma solidity >=0.4.16 <0.9.0;contract TX {function f(uint len) public pure {uint[] memory a = new uint[](7);bytes memory b = new bytes(len);//assert断言函数,用于在调试过程中捕捉程序的错误assert(a.length == 7);assert(b.length == len);a[6] = 8;}}

数组成员:

length:

表示当前数组的长度。 一经创建,内存数组的大小就是固定的(但却是动态的,也就是说,它可以根据运行时的参数创建)。

push():

动态的存储数组以及bytes类型(string类型不可以)都有一个push()的成员函数,它用来添加新的零初始化元素到数组末尾,并返回元素引用. 因此可以这样: x.push().t=2x.push()=b.

通过push增加数组长度会消耗gas费用

push(x):

动态的存储 数组以及bytes类型(string类型不可以)都有一个push(x)的成员函数,用来在数组末尾添加一个给定的元素,这个函数没有返回值.

pop:

变长的存储数组以及bytes类型(string类型不可以)都有一个pop的成员函数, 它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用delete

数组切片

数组切片是数组连续部分的视图,用法如:x[start:end]startend是 uint256 类型(或结果为 uint256 的表达式)。x[start:end]的第一个元素是x[start], 最后一个元素是x[end-1]

如果startend大或者end比数组长度还大,将会抛出异常。

startend都可以是可选的:start默认是 0, 而end默认是数组长度。

切片数组可以隐式转换为其“背后”类型的数组,并支持索引访问。 索引访问也是相对于切片的开始位置。 数组切片没有类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。

实例1:

pragma solidity >=0.6.99 > 8) |(bytes4(_payload[2]) >> 16) |(bytes4(_payload[3]) >> 24);if (sig == bytes4(keccak256("setOwner(address)"))) {address owner = abi.decode(_payload[4:], (address));require(owner != address(0), "Address of owner cannot be zero.");}(bool status,) = client.delegatecall(_payload);require(status, "Forwarded call failed.");}}

结构体

尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身。 这个限制是有必要的,因为结构体的大小必须是有限的。

注意:在函数中使用结构体时,一个结构体是如何赋值给一个存储位置是存储的局部变量。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。

简化的众筹实例:

pragma solidity >=0.6.0  Funder) funders;}uint numCampaigns;mapping (uint => Campaign) campaigns;function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {campaignID = numCampaigns++; // campaignID 作为一个变量返回// 不能使用 "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"// 因为RHS会创建一个包含映射的内存结构体 "Campaign"Campaign storage c = campaigns[campaignID];c.beneficiary = beneficiary;c.fundingGoal = goal;}function contribute(uint campaignID) public payable {Campaign storage c = campaigns[campaignID];// 以给定的值初始化,创建一个新的临时 memory 结构体,// 并将其拷贝到 storage 中。// 注意你也可以使用 Funder(msg.sender, msg.value) 来初始化。c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});c.amount += msg.value;}function checkGoalReached(uint campaignID) public returns (bool reached) {Campaign storage c = campaigns[campaignID];if (c.amount < c.fundingGoal)return false;uint amount = c.amount;c.amount = 0;c.beneficiary.transfer(amount);return true;}}

映射:

映射类型在声明时的形式为mapping(_KeyType=>_ValueType)

其中_KeyType可以是任何基本类型,即可以是任何的内建类型,bytesstring或合约类型、枚举类型。 而其他用户定义的类型或复杂的类型如:映射、结构体、即除bytesstring之外的数组类型是不可以作为_KeyType的类型的。

_ValueType可以是包括映射类型在内的任何类型。

映射可以视作哈希表,它们在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的默认值。然而下面是映射与哈希表不同的地方: 在映射中,实际上并不存储 key,而是存储它的keccak256哈希值,从而便于查询实际的值。正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有其他信息键的信息是无法被删除

映射只能是存储storage的数据位置,因此只允许作为状态变量 或 作为函数内的存储storage引用 或 作为库函数的参数。 它们不能用合约公有函数的参数或返回值。

可见性和 getter 函数

由于 Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为“消息调用”,而外部调用则会产生一个 EVM 调用), 函数和状态变量有四种可见性类型。 函数可以指定为externalpublicinternal或者private。 对于状态变量,不能设置为external,默认是internal

  • external

外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数f不能从内部调用(即f不起作用,但this.f()可以)。 当收到大量数据的时候,外部函数有时候会更有效率,因为数据不会从calldata复制到内存.

  • public

public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数(见下面)。

  • internal

这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用this调用。

  • private

private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

在下面的示例中,MappingExample合约定义了一个公共balances映射,键类型为address,值类型为uint, 将以太坊地址映射为 无符号整数值。 由于uint是值类型,因此getter返回与该类型匹配的值, 可以在 MappingLBC 合约中看到合约在指定地址返回该值。

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.0  uint) public balances;function update(uint newBalance) public {balances[msg.sender] = newBalance;}}contract MappingLBC {function f() public returns (uint) {MappingExample m = new MappingExample();m.update(100);return m.balances(this);}}

实例2【ERC20token简单版】

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.22  uint256) private _balances;mapping (address => mapping (address => uint256)) private _allowances;event Transfer(address indexed from, address indexed to, uint256 value);event Approval(address indexed owner, address indexed spender, uint256 value);function allowance(address owner, address spender) public view returns (uint256) {return _allowances[owner][spender];}function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {_transfer(sender, recipient, amount);approve(sender, msg.sender, amount);return true;}function approve(address owner, address spender, uint256 amount) public returns (bool) {require(owner != address(0), "ERC20: approve from the zero address");require(spender != address(0), "ERC20: approve to the zero address");_allowances[owner][spender] = amount;emit Approval(owner, spender, amount);return true;}function _transfer(address sender, address recipient, uint256 amount) internal {require(sender != address(0), "ERC20: transfer from the zero address");require(recipient != address(0), "ERC20: transfer to the zero address");_balances[sender] -= amount;_balances[recipient] += amount;emit Transfer(sender, recipient, amount);}}

涉及LValues的运算符:

a+=e等同于a=a+e。 其它运算符-=*=/=%=|=&=以及^=都是如此定义的。a++a--分别等同于a+=1a-=1,但表达式本身的值等于a在计算之前的值。 与之相反,--a++a虽然最终a的结果与之前的表达式相同,但表达式的返回值是计算之后的值。

基本类型转化:

例如,uint8可以转换成uint16int128转换成int256,但int8不能转换成uint256(因为uint256不能涵盖某些值,例如,-1)。

//如果一个类型显式转换成更小的类型,相应的高位将被舍弃uint32 a = 0x12345678;uint16 b = uint16(a); // 此时 b 的值是 0x5678//如果将整数显式转换为更大的类型,则将填充左侧(即在更高阶的位置)。 转换结果依旧等于原来整数uint16 a = 0x1234;uint32 b = uint32(a); // b 为 0x00001234 nowassert(a == b);//定长字节数组转换则有所不同, 他们可以被认为是单个字节的序列和转换为较小的类型将切断序列bytes2 a = 0x1234;bytes1 b = bytes1(a); // b 为 0x12//如果将定长字节数组显式转换为更大的类型,将按正确的方式填充。 以固定索引访问转换后的字节将在和之前的值相等 (如果索引仍然在范围内):bytes2 a = 0x1234;bytes4 b = bytes4(a); // b 为 0x12340000assert(a[0] == b[0]);assert(a[1] == b[1]);//因为整数和定长字节数组在截断(或填充)时行为是不同的, 如果整数和定长字节数组有相同的大小,则允许他们之间进行显式转换, 如果要在不同的大小的整数和定长字节数组之间进行转换 ,必须使用一个中间类型来明确进行所需截断和填充的规则:bytes2 a = 0x1234;uint32 b = uint16(a); // b 为 0x00001234uint32 c = uint32(bytes4(a)); // c 为 0x12340000uint8 d = uint8(uint16(a)); // d 为 0x34uint8 e = uint8(bytes1(a)); // e 为 0x12

地址类型:

通过校验和测试的正确大小的十六进制字面常量会作为address类型。没有其他字面常量可以隐式转换为address类型。

bytes20或其他整型显示转换为address类型时,都会作为addresspayable类型。

一个地址addressa可以通过“payable(a)“ 转换为 addresspayable类型.