Solidity是一种静态类型语言,需要再编译期间指定每个变量(静态和局部)的类型。Solidity提供了几种基本类型,可以通过基本类型组合成复杂类型。另外,在带有操作符的表达式中,类型之间会相互影响。
数值类型
下面介绍数值类型,为什么叫数据类型,因为这些变量类型都需要传入一个值,例如:在函数变量或是赋值,数值类型都会进行拷贝。
布尔型
bool: 值为 true 或是 false
操作:
! 逻辑非
&& 逻辑与
|| 逻辑或
== 相等
!= 不等
|| 和 && 具有简化规则,比如 f(x) || g(x) ,如果 f(x)为true,将不会计算g(x)的值,即使g(x)有可能为false
整型
int/uint : 有符号整型和无符号整型 有多种长度。 unit8 到 uint258 相差8个(从8到258),int8到int256一样。unit 和int的别名分别为:unit256 和 int256.
操作:
比较:<= , < , == , != , >= , >
位操作: &(与) , |(或) , ^ (异或), ~ (非)
算术操作:+ , - , * , /, % , **(乘方,求幂) ,<<(左移) , >>(右移)
除法操作经常会被截断(在EVM中会被编译成DIV操作码),但是如果相除的两个数都是 数字常量,结果不会被截断。如果除数为0,将会抛出运行时异常。
移位操作的结果是左操作数,x << y 和 x * 2 ** y 相同,x >> y 和 x / 2**y 相同。这就意味着,如果对一个负数进行移位操作,会扩展到符号位。如果一个数被 移位了 负数次,会抛出运行时异常。
地址
address: 20 字节长度的值(以太坊的地址),地址类型有很多成员变量,是所有合约的基础。
操作: <= , < , == , != , >= , >
地址类型的成员
- balance 和 transfer
可以通过地址的balance属性来查看一个地址的余额,发送以太币(单位为:wei)到一个地址可以使用 transfer方法
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
注意:如果x是一个合约地址,它的代码(如果存在的话,更明确是为 fallback 函数)将会和 transfer 调用一起被执行(这是EVM的限制,是不可阻止的)。如果执行过程中gas不够或是失败,当前合约会终止抛出异常
-
send
send方法和transfer很相似,但是比transfer更低级。如果send失败,当前的合约不会中断,也不会抛出异常,会返回一个false。
注意:使用send有一些风险:如果调用栈深度超过1024或是gas不够,所有的转让操作都会失败,为了更安全的以太币转移,如果用send就必须每次都要检查返回值,使用transfer方法会更好;
- call, callcode, delegatecall
而且,call方法可以和没有依附于ABI上的合约进行交互,它提供了任意多个任意类型的参数。这些参数填充成32字节,并连接起来。有情况如果第一个参数被加密成4个字节,就会抛出异常,这种情况下,是不允许使用这个方法。
address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);
call 返回一个boolean值,被调用函数终止返回true或是有EVM异常就返回false。不会返回访问的具体数据(对此,我们需要预先知道编码方式和大小)。
同样的方式,delegatecall也可以使用,和call的唯一区别是 delegatecall会使用给定地址的代码。所有其他方面(存储,余额等)都是从当前合约中获取。delegatecall的作用是使用其他合约里的library代码。用户需要确保两个合约的存储布局适用于 deletegatecall的使用。
call, delegatecall和call都是很低层次的方法,只有在实在没办法的时候才使用,这三个方法会破坏Solidity里的类型安全。
三个方法都可以使用 .gas(),而 .value() 不适合deletegatecall.
注意:所有的合约都继承了地址相关成员方法。所以可以使用 this.balance 来查询当前合约的余额
⚠️ 这些方法都是低层数的方法,使用的时候一定要小心。如果使用不当,任何未知的合约都可能别破坏。 你应该移交控制到可以通过回调到你自己合约的那个合约里,这样通过返回值就可以更新你自己的state变量
固定大小的byte数组
bytes1, bytes2, bytes3 ... bytes32。 byte和byte1一样。
操作:
比较: <= , <, == , != , >= , >
位操作:& , | , ^ , ~, << , >>
下标访问:如果x是 byteI类型,那么 x[k] (0<= k < I),返回第k个byte (只读)
移位操作可以作为右操作使用在任何整型上(返回的是一个左操作的类型),如果移位的位数是个负数,会抛出运行时异常。
成员: .length 得到byte数组的固定长度
动态大小的byte数组
bytes: 动态大小byte数组,不是一个值类型
string:动态大小UTF-8编码string,不是一个值类型
作为一个经验法则,对于任意长度的raw数据,使用bytes。对于任意长度的string(UTF-8)数据,使用string。如果可以限定bytes到一定长度,优先使用byte1到byte32,这32个byte类型开销更小。
地址常量
十六进制常量并通过地址的checksum的验证。比如 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 是一个address类型。十六进制常量是39位到41位数字长度。如果没有通过checksum的检查,会产生一个警告,但是还是会作为一个合理的数字常量。
有理数和整数常量
整数常量由0到9的数字组成的一列数字。这些数字序列会被解释成小数。比如,69是表示数字 69。在Solidity不存在八进制,以0开头的是无效的。
小数常量是由 . 组成,并且 . 的一边必须要有数字,比如: 1. , .1 或是 1.3 。科学计数法也是同样支持,基数可以有小数部分,但是指数部分不可以。比如:2e10, -2e10, 2e-10, 2.5e1 。 数字常量表达式保持任意精度,直到他们可以转换成一个非常量类型。这意味着 计数指令不会溢出,分割部分在数字常量表达式中不会被截断。例如:(2**800 + 1) - 2**800 虽然中间结果是不符合机器字大小的,但是值还是常量1(类型为uint8)。此外,.5 * 8 结果是整型4(虽然在表达式中使用了非整型)。
如果结果不是一个整型,会根据小数部分的bits大小来使用ufixed还是fixed类型。在var x = 1/4; 中,x的类型为 ufixed0x8,在 var x = 1/3 ,x的类型为ufixed0x256,因为 1/3 的结果在二进制中是不能用有限来表示的。只要操作数是整型,任何应用于整型的操作都可以用于数字常量表达式中。如果两个操作数中公有一个是小数,位操作就不允许。同样的,如果指数是小数,乘方操作也不允许。
字符串常量
字符串常量可以写成用双引号或是单引号("foo"
, 'bar'
)。Solidity里的字符串常量不像C语言中,后面有个0结尾。"foo"是三个bytes长度,而不是四个。在整型常量中,类型可以改变,他们可以转换成 bytes1
, ..., bytes32 ,如果他们符合类型,可以从bytes或是string。字符串常量支持转义字符,例如
\n
, \xNN,
\uNNNN 。
十六进制常量
十六进制是以 关键字 hex开头的,后面的数字用双引号或是单引号引起(hex"001122FF")。引号内的内容必须是十六进制的字符串。十六进制常量的行为和字符串常量一样,并且有同样的限制。
枚举
枚举是一种用户定义类型。他们可以显示转换成所有的整型类型,但是不允许隐式转换。在运行时会检查显示转换的值范围,如果转换失败,就会抛出异常。枚举至少要有一个成员
pragma solidity ^0.4.0;
contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() {
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() returns (ActionChoices) {
return choice;
}
function getDefaultChoice() returns (uint) {
return uint(defaultChoice);
}
}
函数类型
函数类型是方法类型,类似于函数指针。方法可以赋值给函数类型的变量,函数类型的方法参数可用于传递方法或是返回方法。函数类型有两种形式:内部方法和外部方法。内部方法只能用于当前合约中,因为他们不可在当前合约的上下文之外执行。调用一个内部就跳转到这个方法的人口处,就想调用一个当前合约的内部方法一样。外部方法由一个地址和一个函数签名组成,可以通过一个外部方法传值或是返回。
函数类型声明如下:
function () {internal|external} [constant] [payable] [returns ()]
和参数类型相比,返回类型不能为空。如果函数类型不需要返回任何值,整个returns (
调用一个没有初始化的方法类型变量会抛出异常,同样的如果你如果调用一个已经调用过delete之后的方法类型变量也会抛出异常。如果一个外部方法类型使用在Solidity的上下文之外,他们被看成是一个 函数类型,它的方法标识符和它的地址会用一个bytes24类型 编码在一起。注意当前合约的public方法可以即作为内部和外部方法。直接使用 f 就作为一个内部方法,如果你想使用外部方法形式,就使用 this.f
如下例子展示了如何使用内部函数类型
pragma solidity ^0.4.5;
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) returns (uint) f)
internal
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) returns (uint) f
)
internal
returns (uint)
{
r = self[0];
for (uint i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint length) internal 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) returns (uint) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint x) internal returns (uint) {
return x * x;
}
function sum(uint x, uint y) internal returns (uint) {
return x + y;
}
}
另外一个使用外部函数类型例子:
pragma solidity ^0.4.11; 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) { requests.push(Request(data, callback)); NewRequest(requests.length - 1); } function reply(uint requestID, bytes response) { // 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) { require(msg.sender == address(oracle)); // Use the data } }
引用类型
复杂类型,比如相比于我们常见的数值类型, 我们要更小心的处理那些不一定总是会符合256bits的类型。由于拷贝操作的代价很高昂,我们需要考虑是否要把他们存在内存中还是存在存储空间中。
数据位置
每一个复杂类型,比如 array或是structs 都有一个额外的注解,数据位置,用于标识数据是在存在内存还是存储空间中。根据上下文,总是有一个默认值, 但是可以通过重写来指定是存储空间还是内存。默认的方法形参包括返回参数都是存储在内存中,局部变量的默认存储是在存储空间,静态变量智能存储在存储空间中。
还有一种是第三数据位置,calldata, 是一个不可变,非持久区域,方法实参存储在这个区域。外部方法的形参不是返回参数智能存储在 calldata中,存储在calldata里的数据行为和存在内存里一样。
数据位置非常重要,它决定了如何分配。存储空间,内存和一个静态变量的分配总是创建一个独立的拷贝空间。而一个局部变量只是分配一个引用,这个引用总是 指向一个静态变量,即使静态变量同时改变了。另一方面,从一个内存存储的引用到一个存储空间存储的引用不会创建一个拷贝。
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) { 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) {} }
概括
指定地址位置
- 外部方法的形参: calldata
- 静态变量: 存储空间
默认地址位置
- 内部方法的形参包括返回参数 : 内存
- 所有的其他局部变量:存储空间
数组
数组在编译期间固定大小或是声明为一个动态数组, 对于存储数组,数组里的元素类型是可以为任意的(可以是其他数组,mapping或是structs).对于内存数组,如果是一个public方法的实参,元素不能是mapping,而且必须是ABI类型。
如果数组的长度为x, 元素类型为T,则可以写成 T[x],一个动态长度的数组,可以写成 T[]。例如,一个数组为5的动态数组,类型为uint,可以写成 uint[][5]。访问第二个uint中的第三个元素,可以使用 T[2][1] (数组下标从0开始)。bytes和string是特殊的数组,bytes类似于byte[],但是它在calldata里是紧凑存放。string等于bytes,但是不允许根据长度或下标访问。所以从廉价程度来说,相对于byte[],更优先使用bytes。
分配内存数组
在内存里创建一个可变长度数组可以使用new关键字,和存储空间数组相反,它不能通过 .length 重新设置长度。
pragma solidity ^0.4.0; contract C { function f(uint len) { uint[] memory a = new uint[](7); bytes memory b = new bytes(len); // Here we have a.length == 7 and b.length == len a[6] = 8; } }
数组常量 或是内联数组
常量数组可以写成一种表达式,并且不可以赋值给一个变量。
pragma solidity ^0.4.0; contract C { function f() { g([uint(1), 2, 3]); } function g(uint[3] _data) { // ... } }
常量数组是一个固定长度的内存数组,它的基本类型是给定元素的类型。[1,2,3]的类型为 uint8[3] memory ,因为包含的每个元素类型都为 uint8。 所以有必要把前面例子的第一个元素类型改成 uint。需要注意:固定长度的内存数组是不可以赋值给 动态长度的内存数组。下面的例子是错误的。
pragma solidity ^0.4.0; contract C { function f() { // The next line creates a type error because uint[3] memory // cannot be converted to uint[] memory. uint[] x = [uint(1), 3, 4]; }
这个限制现有有计划会在后续版本里去掉。
成员
- length
数组有个length成员来表示数组元素的个数。动态数组可以在存储空间(不是内存)里通过改变 length值来重新设置长度。
- push
动态存储数组 和 bytes(不是string)可以使用push来增加一个元素到数组的尾部。这个方法将返回一个新的长度。
pragma solidity ^0.4.0; contract ArrayContract { uint[2**20] m_aLotOfIntegers; // Note that the following is not a pair of dynamic arrays but a // dynamic array of pairs (i.e. of fixed size arrays of length two). bool[2][] m_pairsOfFlags; // newPairs is stored in memory - the default for function arguments function setAllFlagPairs(bool[2][] newPairs) { // assignment to a storage array replaces the complete array m_pairsOfFlags = newPairs; } function setFlagPair(uint index, bool flagA, bool flagB) { // access to a non-existing index will throw an exception m_pairsOfFlags[index][0] = flagA; m_pairsOfFlags[index][1] = flagB; } function changeFlagArraySize(uint newSize) { // if the new size is smaller, removed array elements will be cleared m_pairsOfFlags.length = newSize; } function clear() { // these clear the arrays completely delete m_pairsOfFlags; delete m_aLotOfIntegers; // identical effect here m_pairsOfFlags.length = 0; } bytes m_byteData; function byteArrays(bytes data) { // byte arrays ("bytes") are different as they are stored without padding, // but can be treated identical to "uint8[]" m_byteData = data; m_byteData.length += 7; m_byteData[3] = 8; delete m_byteData[2]; } function addFlag(bool[2] flag) returns (uint) { return m_pairsOfFlags.push(flag); } function createMemoryArray(uint size) returns (bytes) { // Dynamic memory arrays are created using `new`: uint[2][] memory arrayOfPairs = new uint[2][](size); // Create a dynamic byte array: bytes memory b = new bytes(200); for (uint i = 0; i < b.length; i++) b[i] = byte(i); return b; } }
结构
Solidity提供了一种用户自定义格式的新类型。例如:
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) 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) payable { Campaign 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) returns (bool reached) { Campaign c = campaigns[campaignID]; if (c.amount < c.fundingGoal) return false; uint amount = c.amount; c.amount = 0; c.beneficiary.transfer(amount); return true; } }
结构可以用在mapping和数组的内部,当然结构本身也可以包含mapping或是数组。虽然结构本身可以作为mapping的成员值,但是结构不可以包含它本身类型。这个约束是有必要的,是为了防止出现结构套结构,无限的循环套用。注意:结构是赋值给一个局部变量(默认为存储空间数据位置),不是拷贝结构,而是存储一个引用指向局部变量。当然,不用赋值给局部变量,也可以直接访问结构体的成员 campaigns[campaignID].amount = 0
Mappings
Mappings类型定义为 mapping(_KeyType => _ValueType),这里 _KeyType可以是除了mapping外的其他任意类型。 _ValueType 可以是任意类型,包括mapping。Mapping可以看成是一个哈希表,初始化每个存在的key,对应的value的值会初始化为所有的字节都为0。mapping的key事实上不是存在mapping中,只有key的keccak256 哈希才用于查找值。所以,mapping是没有长度和设置key和value的概念。mapping只允许静态变量或是内部方法中的存储空间引用类型。
pragma solidity ^0.4.0; contract MappingExample { mapping(address => uint) public balances; function update(uint newBalance) { balances[msg.sender] = newBalance; } } contract MappingUser { function f() returns (uint) { return MappingExample().balances(this); } }
LValues的操作
如果a是一个LValue(一个变量,或是可以被赋值的),提供了一下操作:
a+= e 等于 a = a + e;同样 -=
, *=
, /=
, %=
, a |=
, &=
和 ^= , a++ , a--, ++a, --a。
- delete
delete a 为a赋值一个初始值。比如 对于 整型,a = 0; 这个也可以用于数组,把一个动态数组长度设置为0 或是静态数组中的所有元素重设为初始值。对于结构体来说,也是重设结构体里的所有元素。delete对于mapping是无效的。所以delete 一个结构体,会递归重置所有元素,除了mapping。但是对于mapping的单独的key或是key指定的元素值可以被 delete。
delete a是表示重设a的值。
pragma solidity ^0.4.0; contract DeleteExample { uint data; uint[] dataArray; function f() { uint x = data; delete x; // sets x to 0, does not affect data delete data; // sets data to 0, does not affect x which still holds a copy uint[] y = dataArray; delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also // y is affected which is an alias to the storage object // On the other hand: "delete y" is not valid, as assignments to local variables // referencing storage objects can only be made from existing storage objects. } }
数据类型的转换
隐式转换
如果操作两个不同类型的数据,编译器会尝试隐式转换其中的一个操作数类型到另外一个操作类型。通常来说,如果语法是有意义并且不会出现信息丢失情况下,值之间的隐式转换是可以的:uint8转换到 uint16, uint 128到 uint 256。但是 uint8不可以转换到 uint256,因为uint256不能包含uint8的所有数字,比如 -1。无符号整型可以转换成同样大小的 bytes,但是反过来不行。所有可以转换成uint160的都可以转换成 address。
显式转换
如果编译器不允许隐式转换,但是你自己知道自己的做法,可以进行显式转换。但是显式转换可能出现很多异常情况,需要充分测试。比如:
int8 y = -3; uint x = uint(y);
如果尝试去强制转换到一个小的数据类型,会出现高位数据丢失情况
uint32 a = 0x12345678; uint16 b = uint16(a); // b will be 0x5678 now
欢迎大家关注微信号:蜗牛讲技术。扫下面的二维码