Solidity定义了一个汇编语言,可以不同Solidity一起使用。这个汇编语言还可以嵌入到Solidity源码中,以内联汇编的方式使用。下面我们将从内联汇编如何使用着手,介绍其与独立使用的汇编语言的不同,最后再介绍这门汇编语言。
文档尚待完善的补充的地方:待补充内联汇编的变量作用域的不同,尤其是使用含internal
的函数的库时所引入的复杂度。另外,还需补充,编译器定义的符号(symbols)。
通常我们通过库代码,来增强语言我,实现一些精细化的控制,Solidity为我们提供了一种接近于EVM底层的语言,内联汇编,允许与Solidity结合使用。由于EVM是栈式的,所以有时定位栈比较麻烦,Solidty的内联汇编为我们提供了下述的特性,来解决手写底层代码带来的各种问题:
mul(1, add(2, 3))
等同于push1 3 push1 2 add push1 1 mul
let x := add(2, 3) let y := mload(0x40) x := add(x, y)
function f(uint x) { assembly { x := sub(x, 1) } }
let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
switch x case 0 { y := mul(x, 2) } default { y := 0 }
function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }
下面将详细介绍内联编译(inline assembly)语言。
需要注意的是内联编译是一种非常底层的方式来访问EVM虚拟机。他没有Solidity提供的多种安全机制。
下面的例子提供了一个库函数来访问另一个合约,并把它写入到一个bytes
变量中。有一些不能通过常规的Solidity语言完成,内联库可以用来在某些方面增强语言的能力。
pragma solidity ^0.4.0;
library GetCode {
function at(address _addr) returns (bytes o_code) {
assembly {
// retrieve the size of the code, this needs assembly
let size := extcodesize(_addr)
// allocate output byte array - this could also be done without assembly
// by using o_code = new bytes(size)
o_code := mload(0x40)
// new "memory end" including padding
mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// store length in memory
mstore(o_code, size)
// actually retrieve the code, this needs assembly
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}
内联编译在当编译器没办法得到有效率的代码时非常有用。但需要留意的是内联编译语言写起来是比较难的,因为编译器不会进行一些检查,所以你应该只在复杂的,且你知道你在做什么的事情上使用它。
pragma solidity ^0.4.0;
library VectorSum {
// This function is less efficient because the optimizer currently fails to
// remove the bounds checks in array access.
function sumSolidity(uint[] _data) returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// We know that we only access the array in bounds, so we can avoid the check.
// 0x20 needs to be added to an array because the first slot contains the
// array length.
function sumAsm(uint[] _data) returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i) {
assembly {
o_sum := mload(add(add(_data, 0x20), mul(i, 0x20)))
}
}
}
}
内联编译语言也会像Solidity一样解析注释,字面量和标识符。所以你可以使用//
和/**/
的方式注释。内联编译的在Solidity中的语法是包裹在assembly { ... }
,下面是可用的语法,后续有更详细的内容。
0x123
,42
或abc
(字符串最多是32个字符)mload sload dup1 sstore
,后面有可支持的指令列表add(1, mlod(0)
name:
let x := 7
或 let x := add(y, 3)
jump(name)
,3 x add
3 =: x
。x := add(y, 3)
{ let x := 3 { let y := add(x, 1) } }
这个文档不想介绍EVM虚拟机的完整描述,但后面的列表可以做为EVM虚拟机的指令码的一个参考。
如果一个操作码有参数(通过在栈顶),那么他们会放在括号。需要注意的是参数的顺序可以颠倒(非函数风格,后面会详细说明)。用-
标记的操作码不会将一个参数推到栈顶,而标记为*
的是非常特殊的,所有其它的将且只将一个推到栈顶。
在后面的例子中,mem[a...b)
表示成位置a
到位置b
(不包含)的memory
字节内容,storage[p]
表示在位置p
的strorage
内容。
操作码pushi
和jumpdest
不能被直接使用。
在语法中,操作码被表示为预先定义的标识符。
操作码 | 栈 | 说明 |
---|---|---|
stop | - | stop execution, identical to return(0,0) |
add(x, y) | x + y | |
sub(x, y) | x - y | |
mul(x, y) | x * y | |
div(x, y) | x / y | |
sdiv(x, y) | x / y, for signed numbers in two’s complement | |
mod(x, y) | x % y | |
smod(x, y) | x % y, for signed numbers in two’s complement | |
exp(x, y) | x to the power of y | |
not(x) | ~x, every bit of x is negated | |
lt(x, y) | 1 if x < y, 0 otherwise | |
gt(x, y) | 1 if x > y, 0 otherwise | |
slt(x, y) | 1 if x < y, 0 otherwise, for signed numbers in two’s complement | |
sgt(x, y) | 1 if x > y, 0 otherwise, for signed numbers in two’s complement | |
eq(x, y) | 1 if x == y, 0 otherwise | |
iszero(x) | 1 if x == 0, 0 otherwise | |
and(x, y) | bitwise and of x and y | |
or(x, y) | bitwise or of x and y | |
xor(x, y) | bitwise xor of x and y | |
byte(n, x) | nth byte of x, where the most significant byte is the 0th byte | |
addmod(x, y, m) | (x + y) % m with arbitrary precision arithmetics | |
mulmod(x, y, m) | (x * y) % m with arbitrary precision arithmetics | |
signextend(i, x) | sign extend from (i*8+7)th bit counting from least significant | |
keccak256(p, n) | keccak(mem[p...(p+n))) | |
sha3(p, n) | keccak(mem[p...(p+n))) | |
jump(label) | - | jump to label / code position |
jumpi(label, cond) | - | jump to label if cond is nonzero |
pc | current position in code | |
pop(x) | - | remove the element pushed by x |
dup1 ... dup16 | copy ith stack slot to the top (counting from top) | |
swap1 ... swap16 | * | swap topmost and ith stack slot below it |
mload(p) | mem[p..(p+32)) | |
mstore(p, v) | - | mem[p..(p+32)) := v |
mstore8(p, v) | - | mem[p] := v & 0xff - only modifies a single byte |
sload(p) | storage[p] | |
sstore(p, v) | - | storage[p] := v |
msize | size of memory, i.e. largest accessed memory index | |
gas | gas still available to execution | |
address | address of the current contract / execution context | |
balance(a) | wei balance at address a | |
caller | call sender (excluding delegatecall) | |
callvalue | wei sent together with the current call | |
calldataload(p) | call data starting from position p (32 bytes) | |
calldatasize | size of call data in bytes | |
calldatacopy(t, f, s) | - | copy s bytes from calldata at position f to mem at position t |
codesize | size of the code of the current contract / execution context | |
codecopy(t, f, s) | - | copy s bytes from code at position f to mem at position t |
extcodesize(a) | size of the code at address a | |
extcodecopy(a, t, f, s) | - | like codecopy(t, f, s) but take code at address a |
returndatasize | size of the last returndata | |
returndatacopy(t, f, s) | - | copy s bytes from returndata at position f to mem at position t |
create(v, p, s) | create new contract with code mem[p..(p+s)) and send v wei and return the new address | |
create2(v, n, p, s) | create new contract with code mem[p..(p+s)) at address keccak256(. n . keccak256(mem[p..(p+s))) and send v wei and return the new address | |
call(g, a, v, in, insize, out, outsize) | call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success | |
callcode(g, a, v, in, insize, out, outsize) | identical to call but only use the code from a and stay in the context of the current contract otherwise | |
delegatecall(g, a, in, insize, out, outsize) | identical to callcode but also keep caller and callvalue | |
staticcall(g, a, in, insize, out, outsize) | identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications | |
return(p, s) | - | end execution, return data mem[p..(p+s)) |
revert(p, s) | - | end execution, revert state changes, return data mem[p..(p+s)) |
selfdestruct(a) | - | end execution, destroy current contract and send funds to a |
invalid | - | end execution with invalid instruction |
log0(p, s) | - | log without topics and data mem[p..(p+s)) |
log1(p, s, t1) | - | log with topic t1 and data mem[p..(p+s)) |
log2(p, s, t1, t2) | - | log with topics t1, t2 and data mem[p..(p+s)) |
log3(p, s, t1, t2, t3) | - | log with topics t1, t2, t3 and data mem[p..(p+s)) |
log4(p, s, t1, t2, t3, t4) | - | log with topics t1, t2, t3, t4 and data mem[p..(p+s)) |
origin | transaction sender | |
gasprice | gas price of the transaction | |
blockhash(b) | hash of block nr b - only for last 256 blocks excluding current | |
coinbase | current mining beneficiary | |
timestamp | timestamp of the current block in seconds since the epoch | |
number | current block number | |
difficulty | difficulty of the current block | |
gaslimit | block gas limit of the current block |
你可以使用整数常量,通过直接以十进制或16进制的表示方式,将会自动生成恰当的pushi
指令。
assembly { 2 3 add "abc" and }
上面的例子中,将会先加2,3得到5,然后再与字符串abc
进行与运算。字符串按左对齐存储,且不能超过32字节。
你可以在操作码后接着输入操作码,它们最终都会生成正确的字节码。比如:
3 0x80 mload add 0x80 mstore
下面将会添加3
与memory
中位置0x80
的值。
由于经常很难直观的看到某个操作码真正的参数,Solidity内联编译提供了一个函数风格的表达式,上面的代码与下述等同:
mstore(0x80, add(mload(0x80), 3))
函数风格的表达式不能在内部使用指令风格,如1 2 mstore(0x80, add)
将不是合法的,必须被写为mstore(0x80, add(2, 1))
。那些不带参数的操作码,括号可以忽略。
需要注意的是函数风格的参数与指令风格的参数是反的。如果使用函数风格,第一个参数将会出现在栈顶。
Solidity中的变量和其它标识符,可以简单的通过名称引用。对于memory
变量,这将会把地址而不是值推到栈上。Storage
的则有所不同,由于对应的值不一定会占满整个storage
槽位,所以它的地址由槽和实际存储位置相对起始字节偏移。要搜索变量x
指向的槽位,使用x_slot
,得到变量相对槽位起始位置的偏移使用x_offset
。
在赋值中(见下文),我们甚至可以直接向Solidity变量赋值。
还可以访问内联编译的外部函数:内联编译会推入整个的入口的label
(应用虚函数解析的方式)。Solidity中的调用语义如下:
label
,arg1,arg2, ... argn这个功能使用起来还是有点麻烦,因为堆栈偏移量在调用过程中基本上有变化,因此对局部变量的引用将是错误的。
pragma solidity ^0.4.11;
contract C {
uint b;
function f(uint x) returns (uint r) {
assembly {
r := mul(x, sload(b_slot)) // ignore the offset, we know it is zero
}
}
}
另一个在EVM的汇编的问题是jump
和jumpi
使用了绝对地址,可以很容易的变化。Solidity内联汇编提供了标签来让jump
跳转更加容易。需要注意的是标签是非常底层的特性,尽量使用内联汇编函数,循环,Switch指令来代替。下面是一个求Fibonacci的例子:
{
let n := calldataload(4)
let a := 1
let b := a
loop:
jumpi(loopend, eq(n, 0))
a add swap1
n := sub(n, 1)
jump(loop)
loopend:
mstore(0, a)
return(0, 0x20)
}
需要注意的是自动访问栈元素需要内联者知道当前的栈高。这在跳转的源和目标之间有不同栈高时将失败。当然你也仍然可以在这种情况下使用jump,但你最好不要在这种情况下访问栈上的变量(即使是内联变量)。
此外,栈高分析器会一个操作码接着一个操作码的分析代码(而不是根据控制流),所以在下面的情况下,汇编程序将对标签two
的堆栈高度产生错误的判断:
{
let x := 8
jump(two)
one:
// Here the stack height is 2 (because we pushed x and 7),
// but the assembler thinks it is 1 because it reads
// from top to bottom.
// Accessing the stack variable x here will lead to errors.
x := 9
jump(three)
two:
7 // push something onto the stack
jump(one)
three:
}
这个问题可以通过手动调整栈高来解决。你可以在标签前添加栈高需要的增量。需要注意的是,你没有必要关心这此,如果你只是使用循环或汇编级的函数。
下面的例子展示了,在极端的情况下,你可以通过上面说的解决这个问题:
{
let x := 8
jump(two)
0 // This code is unreachable but will adjust the stack height correctly
one:
x := 9 // Now x can be accessed properly.
jump(three)
pop // Similar negative correction.
two:
7 // push something onto the stack
jump(one)
three:
pop // We have to pop the manually pushed value here again.
}
你可以通过let
关键字来定义在内联汇编中有效的变量,实际上它只是在{}
中有效。内部实现上是,在let
指令出现时会在栈上创建一个新槽位,来保存定义的临时变量,在块结束时,会自动在栈上移除对应变量。你需要为变量提供一个初始值,比如0
,但也可以是复杂的函数表达式:
pragma solidity ^0.4.0;
contract C {
function f(uint x) returns (uint b) {
assembly {
let v := add(x, 1)
mstore(0x80, v)
{
let y := add(sload(v), 1)
b := y
} // y is "deallocated" here
b := add(b, v)
} // v is "deallocated" here
}
}
你可以向内联局部变量赋值,或者函数局部变量。需要注意的是当你向一个指向memory
或storage
赋值时,你只是修改了对应指针而不是对应的数据。
有两种方式的赋值方式:函数风格和指令风格。函数风格,比如variable := value
,你必须在函数风格的表达式中提供一个变量,最终将得到一个栈变量。指令风格=: variable
,值则直接从栈底取。以于两种方式冒号指向的都是变量名称(译者注:注意语法中冒号的位置)。赋值的效果是将栈上的变量值替换为新值。
assembly {
let v := 0 // functional-style assignment as part of variable declaration
let g := add(v, 2)
sload(10)
=: v // instruction style assignment, puts the result of sload(10) into v
}
你可以使用switch
语句来作为一个基础版本的if/else
语句。它需要取一个值,用它来与多个常量进行对比。每个分支对应的是对应切尔西到的常量。与某些语言容易出错的行为相反,控制流不会自动从一个判断情景到下一个场景(译者注:默认是break的)。最后有个叫default
的兜底。
assembly {
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
可以有的case
不需要包裹到大括号中,但每个case
需要用大括号的包裹。
内联编译支持一个简单的for
风格的循环。for
风格的循环的头部有三个部分,一个是初始部分,一个条件和一个后叠加部分。条件必须是一个函数风格的表达式,而其它两个部分用大括号包裹。如果在初始化的块中定义了任何变量,这些变量的作用域会被默认扩展到循环体内(条件,与后面的叠加部分定义的变量也类似。译者注:因为默认是块作用域,所以这里是一种特殊情况)。
assembly {
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
汇编语言允许定义底层的函数。这些需要在栈上取参数(以及一个返回的代码行),也会将结果存到栈上。调用一个函数与执行一个函数风格的操作码看起来是一样的。
函数可以在任何地方定义,可以在定义的块中可见。在函数内,你不能访问一个在函数外定义的一个局部变量。同时也没有明确的return
语句。
如果你调用一个函数,并返回了多个值,你可以将他们赋值给一个元组,使用a, b := f(x)
或let a, b := f(x)
。
下面的例子中通过平方乘来实现一个指数函数。
assembly {
function power(base, exponent) -> result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
内联汇编语言使用中需要一个比较高的视野,但它又是非常底层的语法。函数调用,循环,switch
被转换为简单的重写规则,另外一个语言提供的是重安排函数风格的操作码,管理了jump
标签,计算了栈高以方便变量的访问,同时在块结束时,移除块内定义的块内的局部变量。特别需要注意的是最后两个情况。你必须清醒的知道,汇编语言只提供了从开始到结束的栈高计算,它没有根据你的逻辑去计算栈高(译者注:这常常导致错误)。此外,像交换这样的操作,仅仅交换栈里的内容,并不是变量的位置。
与EVM汇编不同,Solidity知道类型少于256字节,如,uint24
。为了让他们更高效,大多数的数学操作仅仅是把也们当成是一个256字节的数字进行计算,高位的字节只在需要的时候才会清理,比如在写入内存前,或者在需要比较时。这意味着如果你在内联汇编中访问这样的变量,你必须要手动清除高位的无效字节。
Solidity以非常简单的方式来管理内存:内部存在一个空间内存的指针在内存位置0x40
。如果你想分配内存,可以直接使用从那个位置的内存,并相应的更新指针。
Solidity中的内存数组元素,总是占用多个32字节的内存(也就是说byte[]
也是这样,但是bytes
和string
不是这样)。多维的memory
的数组是指向memory
的数组。一个动态数组的长度存储在数据的第一个槽位,紧接着就是数组的元素。
固定长度的
memory
数组没有一个长度字段,但它们将很快增加这个字段,以让定长与变长数组间有更好的转换能力,所以请不要依赖于这点。
库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性DELEGATECALL
(Homestead之前是用CALLCODE
)来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用this
将会指向到调用合约,而且可以访问调用合约的存储(storage)
。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的状态变量(state variables)
,否则除此之外,没有任何方法去知道这些状态变量。
使用库合约的合约,可以将库合约视为隐式的父合约(base contracts)
,当然它们不会显式的出现在继承关系中。但调用库函数的方式非常类似,如库L
有函数f()
,使用L.f()
即可访问。此外,internal
的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是internal
的调用惯例,这意味着所有internal
类型可以传进去,memory
类型则通过引用传递,而不是拷贝的方式。为了在EVM中实现这一点,internal
的库函数的代码和从其中调用的所有函数将被拉取(pull into)
到调用合约中,然后执行一个普通的JUMP
来代替DELEGATECALL
。
下面的例子展示了如何使用库(后续在using for
章节有一个更适合的实现Set
的例子)。
pragma solidity ^0.4.0;
library Set {
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data { mapping(uint => bool) flags; }
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter 'self', if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
returns (bool)
{
return self.flags[value];
}
}
contract C {
Set.Data knownValues;
function register(uint value) {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
if (!Set.insert(knownValues, value))
throw;
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
上面的例子中:
Library
定义了一个数据结构体,用来在调用的合约中使用(库本身并未实际存储的数据)。如果函数需要操作数据,这个数据一般是通过库函数的第一个参数传入,按惯例会把参数名定为self
。self
的类型是storage
,那么意味着传入的会是一个引用,而不是拷贝的值,那么修改它的值,会同步影响到其它地方,俗称引用传递,非值传递。c.register
中可以看出是直接使用Set.insert
。但实际上当前的这个合约本身就是它的一个实例。c
可以直接访问,knownValues
。虽然这个值主要是被库函数使用的。当然,你完全可以不按上面的方式来使用库函数,可以不需要定义结构体,不需要使用storage
类型的参数,还可以在任何位置有多个storage
的引用类型的参数。
调用Set.contains
,Set.remove
,Set.insert
都会编译为以DELEGATECALL
的方式调用external
的合约和库。如果使用库,需要注意的是一个实实在在的外部函数调用发生了。尽管msg.sender
,msg.value
,this
还会保持它们在此调用中的值(在Homestead
之前,由于实际使用的是CALLCODE
,msg.sender
,msg.value
会变化)。
下面的例子演示了如何使用memory
类型和内部函数(inernal function)
,来实现一个自定义类型,但不会用到外部函数调用(external function)
。
pragma solidity ^0.4.0;
library BigInt {
struct bigint {
uint[] limbs;
}
function fromUint(uint x) internal returns (bigint r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint _a, bigint _b) internal returns (bigint r) {
r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
uint carry = 0;
for (uint i = 0; i < r.limbs.length; ++i) {
uint a = limb(_a, i);
uint b = limb(_b, i);
r.limbs[i] = a + b + carry;
if (a + b < a || (a + b == uint(-1) && carry > 0))
carry = 1;
else
carry = 0;
}
if (carry > 0) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint _a, uint _limb) internal returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a, uint b) private returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for BigInt.bigint;
function f() {
var x = BigInt.fromUint(7);
var y = BigInt.fromUint(uint(-1));
var z = x.add(y);
}
}
因为编译器并不知道库最终部署的地址。这些地址须由linker
填进最终的字节码中(使用命令行编译器来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符_Set___
(其中Set
是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。
对比普通合约来说,库的限制:
状态变量(state variables)
。ether
。这些限制将来也可能被解除!
指令using A for B;
用来附着库里定义的函数(从库A
)到任意类型B
。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似,python
中的self
变量一样。
using A for *
的效果是,库A
中的函数被附着在做任意的类型上。
在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。
using A for B;
指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。
下面我们来换个方式重写set
的例子。
pragma solidity ^0.4.0;
// This is the same code as before, just without comments
library Set {
struct Data { mapping(uint => bool) flags; }
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
returns (bool)
{
return self.flags[value];
}
}
contract C {
using Set for Set.Data; // this is the crucial change
Set.Data knownValues;
function register(uint value) {
// Here, all variables of type Set.Data have
// corresponding member functions.
// The following function call is identical to
// Set.insert(knownValues, value)
if (!knownValues.insert(value))
throw;
}
}
我们也可以通过这种方式来扩展基本类型(elementary types)
。
pragma solidity ^0.4.0;
library Search {
function indexOf(uint[] storage self, uint value) returns (uint) {
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return uint(-1);
}
}
contract C {
using Search for uint[];
uint[] data;
function append(uint value) {
data.push(value);
}
function replace(uint _old, uint _new) {
// This performs the library function call
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}
需要注意的是所有库调用都实际上是EVM函数调用。这意味着,如果你传的是memory
类型的,或者是值类型(vaue types)
,那么仅会传一份拷贝,即使是self
变量。变通之法就是使用存储(storage)
类型的变量,这样就不会拷贝内容。
接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:
其中的一些限制可能在未来放开。
接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。
接口用自己的关键词表示:
interface Token {
function transfer(address recipient, uint amount);
}
合约可以继承于接口,因为他们可以继承于其它的合约。
抽象函数是没有函数体的的函数。如下:
pragma solidity ^0.4.0;
contract Feline {
function utterance() returns (bytes32);
}
这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。
pragma solidity ^0.4.0;
contract Feline {
function utterance() returns (bytes32);
function getContractName() returns (string){
return "Feline";
}
}
contract Cat is Feline {
function utterance() returns (bytes32) { return "miaow"; }
}
如果一个合约从一个抽象合约
里继承,但却没实现所有函数,那么它也是一个抽象合约
。
Solidity通过复制包括多态的代码来支持多重继承。
所有函数调用是虚拟(virtual)
的,这意味着最远的派生方式会被调用,除非明确指定了合约。
当一个合约从多个其它合约那里继承,在区块链上仅会创建一个合约,在父合约里的代码会复制来形成继承合约。
基本的继承体系与python
有些类似,特别是在处理多继承上面。
下面用一个例子来详细说明:
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
}
// Use "is" to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
function lookup(uint id) returns (address adr);
}
contract NameReg {
function register(bytes32 name);
function unregister();
}
// Multiple inheritance is possible. Note that "owned" is
// also a base class of "mortal", yet there is only a single
// instance of "owned" (as for virtual inheritance in C++).
contract named is owned, mortal {
function named(bytes32 name) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).register(name);
}
// Functions can be overridden by another function with the same name and
// the same number/types of inputs. If the overriding function has different
// types of output parameters, that causes an error.
// Both local and message-based function calls take these overrides
// into account.
function kill() {
if (msg.sender == owner) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
mortal.kill();
}
}
}
// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) {
if (msg.sender == owner) info = newInfo;
}
function get() constant returns(uint r) { return info; }
uint info;
}
上面的例子的named
合约的kill()
方法中,我们调用了motal.kill()
调用父合约的销毁函数(destruction)
。但这样可能什么引发一些小问题。
pragma solidity ^0.4.0;
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() { /* do cleanup 1 */ mortal.kill(); }
}
contract Base2 is mortal {
function kill() { /* do cleanup 2 */ mortal.kill(); }
}
contract Final is Base1, Base2 {
}
对Final.kill()
的调用只会调用Base2.kill()
,因为派生重写,会跳过Base1.kill
,因为它根本就不知道有Base1
。一个变通方法是使用super
。
pragma solidity ^0.4.0;
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() { /* do cleanup 1 */ super.kill(); }
}
contract Base2 is mortal {
function kill() { /* do cleanup 2 */ super.kill(); }
}
contract Final is Base2, Base1 {
}
如果Base1
调用了函数super
,它不会简单的调用基类的合约函数,它还会调用继承关系图谱上的下一个基类合约,所以会调用Base2.kill()
。需要注意的最终的继承图谱将会是:Final,Base1,Base2,mortal,owned。使用super时会调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这类似于普通虚函数查找(ordinary virtual method lookup)
派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子:
pragma solidity ^0.4.0;
contract Base {
uint x;
function Base(uint _x) { x = _x; }
}
contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) {
}
}
或者直接在继承列表中使用is Base(7)
,或像修改器(modifier)
使用方式一样,做为派生构造器定义头的一部分Base(_y * _y)
。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。
实现多继承的编程语言需要解决几个问题,其中之一是菱形继承问题
又称钻石问题
,如下图。
Solidity的解决方案参考Python
,使用C3_linearization来强制将基类合约转换一个有向无环图(DAG)的特定顺序。结果是我们希望的单调性,但却禁止了某些继承行为。特别是基类合约在is
后的顺序非常重要。下面的代码,Solidity会报错Linearization of inheritance graph impossible
。
pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}
原因是C
会请求X
来重写A
(因为继承定义的顺序是A,X
),但A
自身又是重写X
的,所以这是一个不可解决的矛盾。
一个简单的指定基类合约的继承顺序原则是从most base-like
到most derived
。
当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的getter
可以覆盖一个public
的函数。
事件是使用EVM日志内置功能的方便工具,在DAPP的接口中,它可以反过来调用Javascript的监听事件的回调。
事件在合约中可被继承。当被调用时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并合并到区块链中,只要区块可以访问就一直存在(至少Frontier,Homestead是这样,但Serenity也许也是这样)。日志和事件在合约内不可直接被访问,即使是创建日志的合约。
日志的SPV(简单支付验证)是可能的,如果一个外部的实体提供了一个这样证明的合约,它可以证明日志在区块链是否存在。但需要留意的是,由于合约中仅能访问最近的256个区块哈希,所以还需要提供区块头信息。
可以最多有三个参数被设置为indexed
,来设置是否被索引。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。
如果数组(包括string
和bytes
)类型被标记为索引项,会用它对应的Keccak-256
哈希值做为topic
。
除非是匿名事件,否则事件签名(比如:Deposit(address,hash256,uint256)
)是其中一个topic
,同时也意味着对于匿名事件无法通过名字来过滤。
所有未被索引的参数将被做为日志的一部分被保存起来。
被索引的参数将不会保存它们自己,你可以搜索他们的值,但不能检索值本身。
下面是一个简单的例子:
pragma solidity ^0.4.0;
contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
function deposit(bytes32 _id) {
// Any call to this function (even deeply nested) can
// be detected from the JavaScript API by filtering
// for `Deposit` to be called.
Deposit(msg.sender, _id, msg.value);
}
}
下述是使用javascript来获取日志的例子。
var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at(0x123 /* address */);
var event = clientReceipt.Deposit();
// watch for changes
event.watch(function(error, result){
// result will contain various information
// including the argumets given to the Deposit
// call.
if (!error)
console.log(result);
});
// Or pass a callback to start watching immediately
var event = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
通过函数log0
,log1
,log2
,log3
,log4
,可以直接访问底层的日志组件。logi
表示总共有带i + 1
个参数(i
表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下:
log3(
msg.value,
0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20,
msg.sender,
_id
);
其中的长16进制串是事件的签名,计算方式是keccak256("Deposit(address,hash256,uint256)")
更多的理解事件的资源
每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。
此外,当合约收到ether
时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。
下述提供给回退函数可执行的操作会比常规的花费得多一点。
ether
请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。
一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:
pragma solidity ^0.4.0;
contract Test {
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the "payable"
// modifier.
function() { x = 1; }
uint x;
}
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
function() payable { }
}
contract Caller {
function callTest(Test test) {
test.call(0xabcdef01); // hash does not exist
// results in test.x becoming == 1.
// The following call will fail, reject the
// Ether and return false:
test.send(2 ether);
}
}
在浏览器中跑的话,记得要先存ether。
状态变量
可以被定义为constant
,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:1)访问storage
;2)区块链数据,如now
,this.balance
,block.number
;3)合约执行的中间数据,如msg.gas
;4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数keccak256
,keccak256
,ripemd160
,ecrecover
,addmod
,mulmod
可以允许调用,即使它们是调用的外部合约。
允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。
编译器并不会为常量在storage
上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。
不是所有的类型都支持常量,当前支持的仅有值类型和字符串。
pragma solidity ^0.4.0;
contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}
函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。
pragma solidity ^0.4.0;
contract C {
function f(uint a, uint b) constant returns (uint) {
return a * (b + 42);
}
}
访问器(Accessor)
方法默认被标记为constant
。当前编译器并未强制一个constant
的方法不能修改状态。但建议大家对于不会修改数据的标记为constant
。
修改器(Modifiers)
可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)
。下面我们来看一段示例代码:
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
// This contract only defines a modifier but does not use
// it - it will be used in derived contracts.
// The function body is inserted where the special symbol
// "_;" in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
if (msg.sender != owner)
throw;
_;
}
}
contract mortal is owned {
// This contract inherits the "onlyOwner"-modifier from
// "owned" and applies it to the "close"-function, which
// causes that calls to "close" only have an effect if
// they are made by the stored owner.
function close() onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
function Register(uint initialPrice) { price = initialPrice; }
// It is important to also provide the
// "payable" keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) onlyOwner {
price = _price;
}
}
修改器可以被继承,使用将modifier
置于参数后,返回值前即可。
特殊_
表示使用修改符的函数体的替换位置。
从合约Register
可以看出全约可以多继承,通过,
号分隔两个被继承的对象。
修改器也是可以接收参数的,如priced
的costs
。
使用修改器实现的一个防重复进入的例子。
pragma solidity ^0.4.0;
contract Mutex {
bool locked;
modifier noReentrancy() {
if (locked) throw;
locked = true;
_;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within msg.sender.call cannot call f again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() noReentrancy returns (uint) {
if (!msg.sender.call()) throw;
return 7;
}
}
例子中,由于call()
方法有可能会调回当前方法,修改器实现了防重入的检查。
如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。
需要注意的是,在Solidity的早期版本中,有修改器的函数,它的
return
语句的行为有些不同。
在修改器中和函数体内的显式的return
语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的"_"后继续执行。
修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。
编译器为自动为所有的public
的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫data
的无参,返回值是uint
的类型的值data
。状态变量的初始化可以在定义时完成。
pragma solidity ^0.4.0;
contract C{
uint public c = 10;
}
contract D{
C c = new C();
function getDataUsingAccessor() returns (uint){
return c.c();
}
}
访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过this.
,那么它必须通过函数的方式来调用。
pragma solidity ^0.4.0;
contract C{
uint public c = 10;
function accessInternal() returns (uint){
return c;
}
function accessExternal() returns (uint){
return this.c();
}
}
在acessExternal
函数中,如果直接返回return this.c;
,会出现报错Return argument type function () constant external returns (uint256) is not implicitly convertible to expected type (type of first return variable) uint256.
。原因应该是通过外部(external)的方式只能访问到this.c
作为函数的对象,所以它认为你是想把一个函数转为uint
故而报错。
下面是一个更加复杂的例子:
pragma solidity ^0.4.0;
contract ComplexSimple{
struct Cat{
uint a;
bytes3 b;
mapping(uint => uint) map;
}
//
mapping(uint => mapping(bool => Cat)) public content;
function initial(){
content[0][true] = Cat(1, 1);
content[0][true].map[0] = 10;
}
function get() returns (uint, bytes3, uint){
return (content[0][true].a, content[0][true].b, content[0][true].map[0]);
}
}
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
}
文档中自带的的这个Demo始终跑不通,数组类型这里不知为何会抛invalid jump
。把这块简化了写了一个ComplextSimple
供参考。
需要注意的是public
的mapping
默认访问参数是需要参数的,并不是之前说的访问函数都是无参的。
mapping
类型的数据访问方式变为了data[arg1][arg2][arg3].a
结构体(struct)
里的mapping
初始化被省略了,因为并没有一个很好的方式来对键赋值。
Solidity有两种函数调用方式,一种是内部调用,不会创建一个EVM调用(也叫做消息调用),另一种则是外部调用,会创建EVM调用(会发起消息调用)。Solidity对函数和状态变量提供了四种可见性。分别是external
,public
,internal
,private
。其中函数默认是public
。状态变量默认的可见性是internal
。
external
:
外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数f
,不能通过内部的方式来发起调用,(如f()
不可以,但可以通过this.f()
)。外部函数在接收大的数组数据时更加有效。
public
:
公开函数是合约接口的一部分,可以通过内部,或者消息来进行调用。对于public
类型的状态变量,会自动创建一个访问器(详见下文)。
internal
:
这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀this
,前缀this
是表示通过外部方式访问。
private
:
私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问。
备注
所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为private
仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。
可见性的标识符的定义位置,对于state variable
是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子:
pragma solidity ^0.4.0;
contract C {
function f(uint a) private returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
在下面的例子中,D
可以调用c.getData()
来访问data
的值,但不能调用f
。合约E
继承自C
,所以它可以访问compute
函数。
pragma solidity ^0.4.0;
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
contract D {
function readData() {
C c = new C();
uint local = c.f(7); // error: member "f" is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member "compute" is not visible
}
}
contract E is C {
function g() {
C c = new C();
uint val = compute(3, 5); // acces to internal member (from derivated to parent contract)
}
}
Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的状态变量(state variables)
,和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量(state variables)
就不能访问了。
合约可以通过Solidity,或不通过Solidity创建。当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。
构造器函数是可选的。仅能有一个构造器,所以不支持重载。
如果不通过Solidity,我们可以通过web3.js
,使用JavaScript的API来完成合约创建:
// Need to specify some source including contract name for the data param below
var source = "contract CONTRACT_NAME { function CONTRACT_NAME(unit a, uint b) {} }";
// The json abi array generated by the compiler
var abiArray = [
{
"inputs":[
{"name":"x","type":"uint256"},
{"name":"y","type":"uint256"}
],
"type":"constructor"
},
{
"constant":true,
"inputs":[],
"name":"x",
"outputs":[{"name":"","type":"bytes32"}],
"type":"function"
}
];
var MyContract_ = web3.eth.contract(source);
MyContract = web3.eth.contract(MyContract_.CONTRACT_NAME.info.abiDefinition);
// deploy new contract
var contractInstance = MyContract.new(
10,
11,
{from: myAccount, gas: 1000000}
);
具体内部实现里,构造器的参数是紧跟在合约代码的后面,但如果你使用web3.js
,可以不用关心这样的细节。
如果一个合约要创建另一个合约,它必须要知道源码。这意味着循环创建依赖是不可能的。
pragma solidity ^0.4.0;
contract OwnedToken {
// TokenCreator is a contract type that is defined below.
// It is fine to reference it as long as it is not used
// to create a new contract.
TokenCreator creator;
address owner;
bytes32 name;
// This is the constructor which registers the
// creator and the assigned name.
function OwnedToken(bytes32 _name) {
// State variables are accessed via their name
// and not via e.g. this.owner. This also applies
// to functions and especially in the constructors,
// you can only call them like that ("internall"),
// because the contract itself does not exist yet.
owner = msg.sender;
// We do an explicit type conversion from `address`
// to `TokenCreator` and assume that the type of
// the calling contract is TokenCreator, there is
// no real way to check that.
creator = TokenCreator(msg.sender);
name = _name;
}
function changeName(bytes32 newName) {
// Only the creator can alter the name --
// the comparison is possible since contracts
// are implicitly convertible to addresses.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) {
// Only the current owner can transfer the token.
if (msg.sender != owner) return;
// We also want to ask the creator if the transfer
// is fine. Note that this calls a function of the
// contract defined below. If the call fails (e.g.
// due to out-of-gas), the execution here stops
// immediately.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
returns (OwnedToken tokenAddress)
{
// Create a new Token contract and return its address.
// From the JavaScript side, the return type is simply
// "address", as this is the closest type available in
// the ABI.
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) {
// Again, the external type of "tokenAddress" is
// simply "address".
tokenAddress.changeName(name);
}
function isTokenTransferOK(
address currentOwner,
address newOwner
) returns (bool ok) {
// Check some arbitrary condition.
address tokenAddress = msg.sender;
return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
}
}
为了增强对语言的细粒度的控制,特别是在写通用库时,可以在一个语言中交错使用Solidity的语句来接近其中一个虚拟机。但由于EVM是基于栈执行的,所以有时很难定位到正确的栈槽位,从而提供正确的的参数或操作码。Solidit的内联汇编尝试解决这个问题,但也引入了其它的问题,当你通过下述特性进行手动的汇编时:
mul(1, add(2, 3))
代替push1 3 push1 2 add push1 1 mul
let x := add(2, 3) let y := mload(0x40) x := add(x, y)
function f(uint x){ assembly { x := sub(x,1)}}
let x := 10 repeat := sub(x, 1) jumpi(repeat, eq(x, 0))
Solidity Assembly是对内联汇编的详细介绍。
有一些情况下,异常是自动抛出来的(见下),你也可以使用throw
来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退)。异常还会通过Solidity的函数调用向上冒泡(bubbled up)传递。(send
,和底层的函数调用call
,delegatecall
,callcode
是一个例外,当发生异常时,这些函数返回false
)。
捕捉异常是不可能的(或许因为异常时,需要强制回退的机制)。
在下面的例子中,我们将如展示如何使用throw
来回退转帐,以及演示如何检查send
的返回值。
pragma solidity ^0.4.0;
contract Sharer {
function sendHalf(address addr) payable returns (uint balance) {
if (!addr.send(msg.value / 2))
throw; // also reverts the transfer to Sharer
return this.balance;
}
}
当前,Solidity在下述场景中自动产生运行时异常。
bytesN
,序号越界,或是负的序号值。call
,send
,delegatecall
或callcode
除外,它们不会抛出异常,但它们会通过返回false
来表示失败。new
创建一个新合约时,但合约的初化化由于类似3中的原因没有正常完成。public
的函数在没有payable
关键字时,却尝试在接收ether
(包括构造函数,和回退函数)。public
的getter
函数(public getter funciton)接收ether
。.transfer()
执行失败assert
返回false
当一个用户通过下述方式触发一个异常:
throw
。require
,但参数值为false。当上述情况发生时,在Solidity会执行一个回退操作(指令0xfd
)。与之相对的是,如果发生运行时异常,或assert
失败时,将执行无效操作(指令0xfe
)。在上述的情况下,由此促使EVM撤回所有的状态改变。这样做的原因是,没有办法继续安全执行了,因为想要发生的事件并未发生。因为我们想保持交易的原子性(一致性),所以撤销所有操作,让整个交易没有任何影响。
通过assert
判断内部条件是否达成,require
验证输入的有效性。这样的分析工具,可以假设正确的输入,减少错误。这样无效的操作码将永远不会出现。
一个变量在声明后都有初始值为字节表示的全0值。也就是所有类型的默认值是典型的零态(zero-state)
。举例来说,默认的bool
的值为false
,uint
和int
的默认值为0
。
对从byte1
到byte32
定长的字节数组,每个元素都被初始化为对应类型的初始值(一个字节的是一个字节长的全0值,多个字节长的是多个字节长的全零值)。对于变长的数组bytes
和string
,默认值则为空数组和空字符串。
函数内定义的变量,在整个函数中均可用,无论它在哪里定义)。因为Solidity使用了javascript
的变量作用范围的规则。与常规语言语法从定义处开始,到当前块结束为止不同。由此,下述代码编译时会抛出一个异常,Identifier already declared
。
pragma solidity ^0.4.0;
contract ScopingErrors {
function scoping() {
uint i = 0;
while (i++ < 1) {
uint same1 = 0;
}
while (i++ < 2) {
uint same1 = 0;// Illegal, second declaration of same1
}
}
function minimalScoping() {
{
uint same2 = 0;
}
{
uint same2 = 0;// Illegal, second declaration of same2
}
}
function forLoopScoping() {
for (uint same3 = 0; same3 < 1; same3++) {
}
for (uint same3 = 0; same3 < 1; same3++) {// Illegal, second declaration of same3
}
}
function crossFunction(){
uint same1 = 0;//Illegal
}
}
另外的,如果一个变量被声明了,它会在函数开始前被初始化为默认值。所以下述例子是合法的。
pragma solidity ^0.4.0;
contract C{
function foo() returns (uint) {
// baz is implicitly initialized as 0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;// never executes
}
return bar;// returns 5
}
}