1.6.3 类型介绍
Solidity是一种静态类型语言,因此每个变量都需要在编译时指定其数据类型。
Solidity数据类型分为两类:值类型(Value Type)(变量在赋值或传递参数时,进行值拷贝)和引用类型(Reference Types)。
1.6.3.1 值类型(Value Type)
值类型又可具体分为以下若干类:
1)布尔类型(Booleans)
2)整型(Integers)
3)定长浮点型(Fixed Point Numbers)
4)地址(Address)
5)地址成员(Members of Addresses)
6)定长字节数组(Fixed-size byte arrays)
7)变长字节数组(Dynamically-sized byte array)
8)地址字面值(Address Literals)
9)有理数字面值和整数字面值(Rational and Integer Literals)
10)字符串字面值(String literals)
11)十六进制字面值(Hexadecimal literals)
12)枚举(Enums)
13)函数类型(Function Types)
1.6.3.1.1 布尔类型(Booleans)
布尔类型的变量取值为常量true或false。布尔类型支持的运算符有:
1) !逻辑非
2) && 逻辑与
3) || 逻辑或
4) == 等于
5) != 不等于
注意:运算符&&和||是短路运算符,比如“i==1 || j == 1”,当“i == 1”为真时,则不会继续判断“j == 1”是否成立,当“i == 1”不成立时才判断“j == 1”是否成立。
1.6.3.1.2 整型(Integers)
int/uint分别表示有符号和无符号整数。支持关键字uint8到uint256(以8为步进长度),uint和int默认对应uint256和int256。
整型变量支持的运算符有:
1) 比较运算符: <=, < , ==, !=, >=, > (等同bool运算)
2) 位运算符: &,|,^(异或),~(按位取反)
3) 算术运算符:+,-,一元运算-,一元运算+,*,/,%(取余数),**(幂), << (左移),>>(右移)
说明:
1) 在Solidity中,整数的除法运算结果会被截断,比如 1/4计算结果会为0。如果参与运算的数是literals则不会被截断。
2) 整数相除时,如果除数为0则抛出异常。
3) 移位运算结果的正负取决于操作符左边数的正负。
4) 移位运算时,所移位数不能为负,即操作符右边的数不能为负,否则会抛出异常。
注意:Solidity中,右移位和除等价,因此一个负数经过右移位运算,会被四舍五入取整为0,这点和其它语言里的右移位运算不同。
1.6.3.1.3 定长浮点型(Fixed Point Numbers)
注意:定长浮点型目前(发文时)在Solidity中还不完全支持,它可以用来声明变量,但无法被赋值和用于赋值。
fixed/ufixed分别表示有符号和无符号的定长浮点数。关键字为ufixedMxN 和 ufixedMxN。M表示这个类型占用的位数,N表示小数点后位数。M必须为8的整数倍,可为8到256位之间的任意值,N可为0到80之间的值。
定长浮点型支持的运算符有:
1) 比较运算符: <=, < , ==, !=, >=, > (等同bool运算)
2) 算术运算符:+,-,*,/, %(取余数)
3) 一元运算符:-,+
注意:Solidity中的定长浮点型和大多数语言的float和double(更准确地说是IEEE 754标准)不一样,前者整数和小数部分总共占有的位数是固定的,而后者不固定。
1.6.3.1.4 地址(Address)
在Solidity中,一个地址是长度为20个字节的值类型,这也是以太坊地址的长度。地址类型有成员,地址是所有合约的基础。地址所支持的运算符有:<=,<,==,!=,>= 和 >。
注意:从0.5.0版本开始,合约不再自地址类型得出,但仍然可以显式转换为地址。
1.6.3.1.5 地址成员(Members of Addresses)
地址有如下成员。
1) balance 及transfer
balance是地址的属性,可用来查询该地址账户的余额,transfer()可用来发送以太币(以wei为单位)。如:
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
注意:如果x是合约地址,合约的回退函数(fallback function)会随transfer一起被执行(这是EVM特性),如果因gas耗光或其它原因导致执行失败,该transfer交易会回退,合约会出抛异常而停止执行。
2) send
send 的功能对应transfer,但属于更底层的函数。如果send执行失败,当前合约不会停止执行,也不会抛出异常, send会返回false。
注意:调用send() 有一定风险。如果调用栈的深度达到1024或gas耗光,交易会失败。因此,为了保证安全,必须检查send的返回值。建议程序员在编程时使用更安全的方法比如transfer,这样当交易失败时,交易金额会退回。
3) call,callcode 和 delegatecall 函数
为了和非ABI协议的合约进行交互,可以使用call() 函数,。它支持任何类型任意数量的参数,这些参数会被打包成32字节并连接在一起。唯一例外的是:当第一个参数恰好是4个字节时,参数不会被打包作为函数签名使用。具体例子如下所示。
address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);
call函数返回一个布尔值,表明执行成功与否。call正常结束返回true,异常终止返回false,但无法获取返回的结果数据,如果需要获取返回的结果数据需要预先知道返回数据的编码和所占的字节数。用户还可以用.gas()修饰器定义gas费用:
namReg.call.gas(1000000)("register", "MyName");
也可以利用修饰器定义以太币金额:
nameReg.call.value(1 ether)("register", "MyName");
还可以使用多种修饰器,使用多种修饰器时,修饰器之间的顺序可以任意排列:
nameReg.call.gas(1000000).value(1 ether)("register", "MyName");
注:gas和value修饰器不能用于重载函数。对于重载函数,可以在重载时检查gas和value,查看它们是否有定义。
同样我们也可以使用delegatecall函数。它与call的区别在于,它仅调用函数代码,而使用当前合约的所有数据,如存储,余额等。delegatecall常被用来执行另一个合约中的代码。所以开发者需要保证当前合约中的数据满足delegatecall中函数的要求。在homestead版本之前,仅有一个功能受限的callcode方法可用,但callcode无法访问msg.sender和msg.value的数据。
call,delegatecall 和callcode都是底层的调用函数,建议只是在万不得已时才使用,因为使用它们会破坏Solidity的类型安全。.gas()可用于在call,callcode和delegatecall,.value()可用于call,callcode,但不能用于delegatecall。
注:所有的合约都可被转换成address类型,因此可以使用address(this).balance查询余额。我们不鼓励使用callcode,在以后的版本中,该函数会被废弃。
上述函数都是底层函数,在使用时要相当小心。当调用一个未知的,可能是恶意的合约时,一旦用户把控制权交给那个合约,它可能回调用户的合约,所以要准备好在调用返回时,小心应对状态变量可能的变化。
1.6.3.1.6 定长字节数组(Fixed-size byte arrays)
定长字节数组类型的关键字有:bytes1, bytes2, bytes3, …, bytes32,以步长为1递增,byte代表bytes1。
定长字节数组支持的运算符有:
1) 比较运算符: <=, <, ==, !=, >=, > (返回布尔值)
2) 位操作符: &, |, ^ (按位异或),~(按位取反), << (左移), >> (右移)
3) 索引访问:如果x是bytesI,当0 <= k < I ,则x[k]返回第k个字节(只读)。
移位运算和整数类似,移位运算结果的正负取决于操作符左边的数,但所移位数不能为负。
成员.length返回这个字节数组的长度(只读)。
注意:byte[]是字节数组,由于要遵循补位规则(padding rules),数组中每个元素(存储在storage中的例外)会浪费31个字节,因此尽量用bytes,而少用byte[]。
1.6.3.1.7 变长字节数组(Dynamically-sized byte array)
1) bytes:字节数组的长度动态分配
2) string: UTF8编码的字符类型数组,长度动态分配。
可用bytes来存储任意长度的字节数据,用string来存储任意长度的(UTF-8编码)字符串数据。在长度已知的情况下,尽量使用定长类型,比如byte1到byte32中的一个,这样可省空间。
1.6.3.1.8 地址字面值(Address Literals)
地址字面值类型是一个能通过地址合法性检查(address checksum test)的十六进制数,如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF。如果一个39到41位的十六进制数不能通过地址合法性检查则会得到一个警告,而被视为普通的有理数字面值类型(rational literal)。
1.6.3.1.9 有理数字面值和整数字面值(Rational and Integer Literals)
整数字面值类型是由一系列0-9的数字组成。整数字面值类型的最左位不能为0。Solidity中没有八进制字面值类型。
在表示小数字面值类型时,小数点左右两边至少要有一个数字,如“1.”,“.1”和“1.1”。
Solidity也支持科学计数法,基数可以是小数,但指数必须为整数,比如: 5e2,0.3e10等。
有理数字面值类型可支持任意精度,在计算中也不会导致溢出或除法截断。但当它被转换成非字面值类型(non literal),或者与非字面值类型(non literal)进行运算时,则不再保证精度。如:(2800 + 1) - 2800的结果为常量1(uint8类型) ,其中间结果可能不符合字长。再比如:.5 * 8,尽管有非整型数参与了运算,但结果为4。
整型数支持的运算符都适用于整数字面值类型。如果两个操作数是小数,则不允许进行位运算,指数不能为小数。
注意:Solidity中每一个有理数字面值类型都有一个数字字面值类型(number literal)。整数字面值类型和有理数字面值类型都是数字字面值类型。所有的数字字面值类型表达式(只包含数字字面值类型和操作符的表达式)的结果都是数字字面值类型。所以3 + 2和2 + 3都是有理数5的数字字面值类型。
整数字面值类型的除法,在早期的版本中其结果是被截断的,但现在可以被转为有理数,如3/2的值为 1.5而不是1。
数字字面值类型表达式一旦参与了非字面值类型(non literal)表达式的运算则会被转为非字面值类型。下面代码中表达式的结果是个整型,但却无法通过编译,因为2.5 + a无法通过类型检查。
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;
1.6.3.1.10 字符串字面值(String Literals)
字符串字面值类型由单引号或双引号标注(如“name” 或 ‘address’)。字符串字面值类型并不像C语言中那样包含结束符,比如”name”这个字符串字面值类型大小为四个字节,而不像C语言那样是五个字节。和整数字面值类型一样,字符串字面值类型的长度是可变的。字符串字面值类型可以隐式转换为byte1,…byte32, 在合适的情况下也能被转为bytes或string。
字符串字面值类型支持转义字符,比如\n,\xNN,\uNNNN。\xNN把一个16进制数转换为合适的字节。\uNNNN把一个Unicode编码值转换为UTF8字符串。
1.6.3.1.11 十六进制字面值(Hexadecimal Literals)
十六进制字面值类型,以关键字hex打头,后面紧跟用单引号或双引号标注的字符串,比如hex"001122ff",其内容是十六进制字符串,其值是二进制数。
十六进制字面值类型和字符串字面值类型类似,适用同样的转换规则。
1.6.3.1.12 枚举(Enums)
在Solidity中,枚举可以由用户自定义。它可以显式地与整数进行转换,但不能进行隐式转换。显式转换会在运行时检查数值范围,如果不符合规则,将会引起异常。枚举类型应至少有一名成员。下面是一个枚举的例子:
pragma solidity ^0.4.16;
contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() public {
choice = ActionChoices.GoStraight;
}
// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity. The integer type used is just
// large enough to hold all enum values, i.e. if you have more values,
// `uint16` will be used and so on.
function getChoice() public view returns (ActionChoices) {
return choice;
}
function getDefaultChoice() public pure returns (uint) {
return uint(defaultChoice);
}
}
1.6.3.1.13 函数类型(Function Types)
在Solidity中,函数也是一种类型。可以将一个函数赋值给一个函数类型,也可以将一个函数作为参数传递给函数中的参数,还可以把函数作为返回值。函数类型有两种:内部(internal)函数和外部(external)函数。
内部函数只能在当前合约内被调用(在当前的代码块内,包括内部库函数,和继承的函数中)。外部函数由地址和函数签名两部分组成,可分别作为参数传入和返回值返回。
函数类型定义如下:
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
如果函数不需要返回值,则returns (<return types>)语句应该完全省略。默认情况下,函数都是internal,因此关键字internal可以省去。与此相反,合约中的函数默认是public, 仅仅在当作类型名使用时默认为internal。
在合约中,有两种方式访问函数,一种是直接用函数名f(假设f为函数名)访问,另一种是用this.f访问,前者用于内部函数调用,后者用于外部函数调用。
如果一个函数变量没有初始化,直接调用它将会产生异常。如果delete一个函数后继续调用它,也会产生异常。
如果外部函数类型在Solidity的上下文环境以外的地方使用,会被视为函数类型。它会将函数地址和地址前的函数标识符一起编码为bytes24类型。
合约中的public函数,可以使用internal和external两种方式来调用。internal访问形式为f,external访问形式为this.f。
Public(或external)函数有个特殊成员selector,该成员返回ABI函数的selector。
pragma solidity ^0.4.16;
contract Selector {
function f() public view returns (bytes4) {
return this.f.selector;
}
}
下面的例子展示了如何使用internal函数:
pragma solidity ^0.4.16;
library ArrayUtils {
// internal functions can be used in internal library functions because
// they will be part of the same code context
function map(uint[] memory self, function (uint) pure returns (uint) f)
internal
pure
returns (uint[] memory r)
{
r = new uint[](self.length);
for (uint i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
function reduce(
uint[] memory self,
function (uint, uint) pure returns (uint) f
)
internal
pure
returns (uint r)
{
r = self[0];
for (uint i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint length) internal pure returns (uint[] memory r) {
r = new uint[](length);
for (uint i = 0; i < r.length; i++) {
r[i] = i;
}
}
}
contract Pyramid {
using ArrayUtils for *;
function pyramid(uint l) public pure returns (uint) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint x) internal pure returns (uint) {
return x * x;
}
function sum(uint x, uint y) internal pure returns (uint) {
return x + y;
}
}
下面的例子展示如何使用external函数:
pragma solidity ^0.4.22;
contract Oracle {
struct Request {
bytes data;
function(bytes memory) external callback;
}
Request[] requests;
event NewRequest(uint);
function query(bytes data, function(bytes memory) external callback) public {
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
function reply(uint requestID, bytes response) public {
// Here goes the check that the reply comes from a trusted source
requests[requestID].callback(response);
}
}
contract OracleUser {
Oracle constant oracle = Oracle(0x1234567); // known contract
function buySomething() {
oracle.query("USD", this.oracleResponse);
}
function oracleResponse(bytes response) public {
require(
msg.sender == address(oracle),
"Only oracle can call this."
);
// Use the data
}
}
在上面的例子中,我们可以看到public,private,internal和external这几个关键字。它们用于定义函数的可见性,将在后续章节中详细介绍。
1.6.3.2 引用类型(Reference Types)
在前面的章节,我们提到 Solidity 类型分为两类:值类型(Value Type)及引用类型(Reference Type)。前面我们已经介绍了值类型,接下来我们介绍引用类型。
对于复杂的类型(比如其大小并不总是为256位的引用类型)其处理方式必须非常小心。直接拷贝这些类型往往代价很大,程序员必须清楚这些类型的值是要存在memory(临时)中还是存在storage(状态变量存储的地方)中。
1.6.3.2.1 数据位置(Data Location)
所有的复杂类型如数组(array)和结构体(struct)都有一个额外的属性:数据的存储位置(data location),存储位置可为memory或storage。
根据上下文的不同,大多数时候数据的存储位置有默认值,也可通过关键字storage和memory指定数据的存储位置。
函数参数(包含返回参数)默认存储在memory。局部变量(local variables)和 状态变量(state variables) 默认存储在storage。
还有一种存储位置calldata,用来存储函数参数,只读,只用于临时存储。外部函数的参数(不包括返回值)被强制指定存储在calldata时,效果与memory类似。
数据存储位置的指定非常重要,因为他们会影响赋值行为。在memory和storage之间或状态变量之间相互赋值,会创建一个全新的拷贝。给一个局部storage变量赋值,实际上是给其赋值一个引用。所以对于局部变量的修改,同时也会修改其引用的状态变量。将一个memory的引用类型赋值给另一个memory的引用时,不会创建新的拷贝。看下面这段代码:
pragma solidity ^0.4.0;
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
y[7]; // fine, returns the 8th element
y.length = 2; // fine, modifies x through y
delete x; // fine, clears the array, also modifies y
// The following does not work; it would need to create a new temporary /
// unnamed array in storage, but storage is "statically" allocated:
// y = memoryArray;
// This does not work either, since it would "reset" the pointer, but there
// is no sensible location it could point to.
// delete y;
g(x); // calls g, handing over a reference to x
h(x); // calls h and creates an independent, temporary copy in memory
}
function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}
}
小结:
1) 强制数据位置(Forced Data Location)
外部函数(external function)的参数(不包括返回值)强制为calldata。
状态变量(state variable)强制为storage。
2) 默认数据位置(Default Data Location)
函数参数及返回参数:memory
所有其它的局部变量:storage
1.6.3.2.2 数组(Arrays)
数组可以在声明时指定长度,也可以是变长。对storage存储数组来说,元素的数据类型可以是任意的,甚至是数组,映射类型,结构体等。但对于memory内存数组来说,如果作为public函数的参数,它不能是映射类型的数组,只能是ABI类型的数组。
一个元素类型为T,固定长度为k的数组,可以定义为T[k],而一个变长数组可以声明为T[]。如果是一个5维,并且数据类型为变长的uint类型数组可以声明为uint[][5]。(注意,和其它语言,如C++相比,Solidity多维数组的长度声明是显著不同的。)要访问该二维数组中第三个动态数组的第二个元素,则使用x[2][1]。数组的序号从0开始。
Bytes和string类型的变量是一种特殊的数组。bytes类似byte[],但在calldata中,bytes会被压缩打包。string类同bytes,但暂不支持length和索引操作。程序员应尽量使用bytes而不是byte[]。
如果想访问byte类型的字符串s可以使用bytes(s).length/bytes(s)[7]=‘x’。注意,这种方式访问的是底层UTF-8编码的bytes而不是单个字符。类型为数组的状态变量,可以用关键字public定义,从而让Solidity创建一个访问器getter,如果要访问数组的某个元素,可以指定下标用getter访问。
1)分配内存数组(Memory Arrays)
要创建内存中的变长类型数组,可以使用关键字new。与storage数组不同的是,用户不能通过.length来定义内存数组的大小。我们来看看下面的例子:
pragma solidity ^0.4.16;
contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
// 这里a.length == 7 ,b.length == len
a[6] = 8;
}
}
2)数组常量及内联数组
数组常量,是一个还未赋值给任何变量的表达式。下面是一个简单的例子:
pragma solidity ^0.4.16;
contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] _data) public pure {
// ...
}
}
数组常量是定长类型的内存数组,其元素类型则是刚好符合存储规则的类型,比如数组[1, 2, 3]的类型是uint8[3] memory,这里由于每个元素的数据类型为uint8,因此,必须把第一个元素转换为uint。
还需注意的一点是,定长类型的内存数组,不能赋值给变长类型的内存数组,下例代码就无法通过编译。
//无法通过编译
pragma solidity ^0.4.0;
contract C {
function f() public {
// 下面一行有类型错误,因为uint[3] memory
// 不能被转换成uint[] memory.
uint[] x = [uint(1), 3, 4];
}
}
在未来的版本中,这个限制可能会被取消。
3) 成员
length属性
数组有.length属性,记录当前的数组长度。对变长的storage存储数组而言,可以通过给.length赋值调整数组长度。但memory内存数组不支持此种操作。不能通过访问超出当前数组长度的方式,来自动改变数组长度。memory内存数组虽然可以通过参数,灵活指定长度,但一旦创建,数组长度就不可改变。
push方法
变长的storage存储数组和bytes都有一个成员函数push(string没有),用于附加新元素到数组末端,返回值为数组的长度。
几点事项要注意:当前在external函数中,不能使用多维数组;另外,由于EVM的限制,不能通过外部函数返回变长数组的内容。
比如智能合约contract C { function f() returns (uint[]) { ... } }中的函数f如果是通过web.js调用则能返回数据,若是通过Solidity调用则不能返回数据。若想在Solidity调用时也返回数据,现有的解决方案是使用一个大的静态数组。
pragma solidity ^0.4.16;
contract ArrayContract {
uint[2**20] m_aLotOfIntegers;
// 注意,下面的数组不是一对数组而是一个变长的数据对,即长度为2的定长数组
bool[2][] m_pairsOfFlags;
// newPairs存储在内存中 – 默认的函数参数存储方式
function setAllFlagPairs(bool[2][] newPairs) public {
// 给一个storage存储数组赋值,替换原有数组
m_pairsOfFlags = newPairs;
}
function setFlagPair(uint index, bool flagA, bool flagB) public {
// 访问一个不存在的下标将抛出异常
m_pairsOfFlags[index][0] = flagA;
m_pairsOfFlags[index][1] = flagB;
}
function changeFlagArraySize(uint newSize) public {
// 如果新数组长度变小,截去的数组元素将被清空
m_pairsOfFlags.length = newSize;
}
function clear() public {
// 删除数组
delete m_pairsOfFlags;
delete m_aLotOfIntegers;
// 效果同上
m_pairsOfFlags.length = 0;
}
bytes m_byteData;
function byteArrays(bytes data) public {
// 字节数组 ("bytes") 存储时,无需打包拼接
// 可被视为等同"uint8[]"类型
m_byteData = data;
m_byteData.length += 7;
m_byteData[3] = byte(8);
delete m_byteData[2];
}
function addFlag(bool[2] flag) public returns (uint) {
return m_pairsOfFlags.push(flag);
}
function createMemoryArray(uint size) public pure returns (bytes) {
// 变长内存数组用“new”创建
uint[2][] memory arrayOfPairs = new uint[2][](size);
// 创建变长字节数组:
bytes memory b = new bytes(200);
for (uint i = 0; i < b.length; i++)
b[i] = byte(i);
return b;
}
}
1.6.3.2.3 结构体(Struct)
在Solidity中,用户可用struct自定义数据类型。我们看看下面的例子:
pragma solidity ^0.4.11;
contract CrowdFunding {
// Defines a new type with two fields.
struct Funder {
address addr;
uint amount;
}
struct Campaign {
address beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}
uint numCampaigns;
mapping (uint => Campaign) campaigns;
function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID is return variable
// Creates new struct and saves in storage. We leave out the mapping type.
campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
}
function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// Creates a new temporary memory struct, initialised with the given values
// and copies it over to storage.
// Note that you can also use Funder(msg.sender, msg.value) to initialise.
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;
}
}
这是个简化版的众筹合约,它有助于我们理解struct数据结构,struct可以作为映射和数组中的元素,其本身也可以包含映射和数组等数据类型。
一个struct类型不能同时将自身作为其数据成员,这是因为结构体的大小必须是有限的,但struct可以作为mapping的值类型成员。
注意观察在上例函数中,一个struct是如何赋值给一个局部变量的(默认是storage类型),赋值过程实际上只是拷贝引用,因此修改局部变量值的同时,也会修改原变量的值。可以直接通过访问成员修改值,而不一定将其赋值给一个局部变量,如campaigns[campaignID].amount = 0
1.6.3.3 映射(Mappings)
映射类型可被定义为mapping(_KeyType => _ValueType)。KeyType被称为键类型,ValueType被称为值类型。一个键类型和一个值类型组成一对。键类型可以是除映射,变长数组,合约,枚举和结构体外的几乎所有类型。值类型没有任何限制,可以为任何类型包括映射。
映射可以被视为一个哈希表,每一个键都被映射到一个默认值(零)。在映射中,并不存储键的值,仅仅存储它的keccak256哈希值,这个哈希值用于查找该键对应的值时需要用到。因此,映射是没有长度的,也没有键集合或值集合的概念。
映射类型,仅能用作为状态变量,或在内部函数中作为storage引用类型。
可以用关键字public来定义映射,让Solidity创建一个访问器getter。通过提供一个键类型KeyType为参数来访问它,得到相应的ValueType。映射的值类型ValueType也可以是映射,使用访问器访问时,要提供一个参数供查找值类型。我们来看一个例子:
pragma solidity ^0.4.0;
contract MappingExample {
mapping(address => uint) public balances;
function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}
contract MappingUser {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(this);
}
}
注意:映射无法迭代,但程序员可以自行实现一个基于映射的迭代结构。
1.6.3.4 涉及LValues的运算
如果a是个LValue变量,则运算式a = a + e可简化为 a += e。含有运算符 -=,*=,/=,%=,|=,&=,^=的运算式都可做类似简化。a+ + 和 a- - 等同a = a+1和a= a – 1。++ a 和 - - a也类似,这点和C++/C一样。
1.6.3.4.1 Delete
delete a 操作会把a类型的初始值赋给a。如果a是个整型,则操作完后a = 0。如果a是个变长数组,则把其长度会被设为零,若a是定长数组或是结构变量,则所有元素的值全部被重置。
delete对mappings不起作用。因此如果delete的是个struct,并且该struct包含mappings,则除mappings以外的其它变量全部被重置。
要注意的是delete a相当于是给a的赋值运算。我们看下面的例子:
pragma solidity ^0.4.0;
contract DeleteExample {
uint data;
uint[] dataArray;
function f() public {
uint x = data;
delete x; // 把x赋值0
delete data; // 把data赋值0
uint[] storage y = dataArray;
delete dataArray; // 将dataArray.length 设为0,但uint[] 仍然保持原有数据
// y 受到了影响,因为y是个storage存储对象
// 注意, "delete y" 无效
// referencing storage objects can only be made from existing storage objects.
}
}
1.6.3.5 基本数据类型之间的转换
1.6.3.5.1 隐式转换
当一个运算作用于不同的数据类型时,编译器通常会隐式地把一个数据类型转换为另一个数据类型。通常这种类型转换不会遗失数据信息,比如uint8转换为uint16,uint128转换为uint256,但int8不能转换为uint256(因为int8类型变量的值可以为-1,而uint256没有-1这种值)。另外无符号型整数可被转换为同样长度或更长的bytes类型,但反之则不成立。比如任何能转换为uint160的类型也可以转换为address类型。
1.6.3.5.2 显式转换
在某些情况下,如果编译器不做隐式转换,但代码要求必须进行类型转换时,可进行显式转换。但要注意的是,显式转换可能会带来意料之外的后果。下例中把int8转换为uint:
int8 y = -3;
uint x = uint(y);
执行完这些命令后x的值将为0xfffff..fd(64位16进制数)。
如果一个占字节数比较长的类型转换为短的类型,则高位部分被截取,如下例所示。
uint32 a = 0x12345678;
uint16 b = uint16(a); // b 将变成 0x5678
1.6.3.6 类型推断
有时为了简便起见,并不总是要在变量定义的时候标明变量的类型,编译器会自动把第一个给该变量赋值的类型定义为该变量的类型,如下例所示。
uint24 x = 0x123;
var y = x;
这里y会被定义为uint24,但var不能用来定义函数参数或返回值。
类型推断时,第一个赋值类型将被作为变量的数据类型。因此下例
for (var i = 0; i < 2000; i++) { ... }
语句将会无限循环,因为i的数据类型将被定义为uint8,而uint8类型的最大值永远小于2000。
Last updated