与在Javascript中一样,函数可以将参数作为输入; 与Javascript和C不同,它们也可以返回任意数量的参数作为输出。
输入参数的声明方式与变量相同。可以省略未使用参数的名称。例如,假设我们希望我们的合约接受一种带有两个整数的外部调用,我们会写如下:
pragma solidity >=0.4.16 <0.6.0;
contract Simple {
uint sum;
function taker(uint _a, uint _b) public {
sum = _a + _b;
}
}
输入参数可以像使用任何其他局部变量一样使用,也可以分配给它们。
可以在returns
关键字后使用相同的语法声明输出参数 。例如,假设我们希望返回两个结果:两个给定整数的和和乘积,然后我们写:
pragma solidity >=0.4.16 <0.6.0;
contract Simple {
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
输出参数的名称可以省略。也可以使用return
语句指定输出值,这些语句也能够返回多个值。返回参数可以用作任何其他局部变量,并且它们是零初始化的; 如果它们没有明确设置,它们保持为零。
大括号语言中已知的大多数控制结构都可以在Solidity中找到:
有:if
,else
,while
,do
,for
,break
,continue
,return
,具有选自C或JavaScript已知的常规语义。
条件不能省略括号,但可以在单语句体周围省略卷曲的brances。
请注意,有从非布尔没有类型转换为Boolean类型,因为在C和JavaScript,因此是不是有效的密实度。if (1) { ... }
当函数有多个输出参数时,可以返回多个值。组件数必须与输出参数的数量相同。return (v0, v1, ..., vn)
当前合约的函数可以直接调用(“内部”),也可以递归调用,如这个荒谬的例子所示:
pragma solidity >=0.4.16 <0.6.0;
contract C {
function g(uint a) public pure returns (uint ret) { return a + f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
这些函数调用转换为EVM内部的简单跳转。这导致当前存储器未被清除,即将内存引用传递给内部调用的函数非常有效。只能在内部调用同一合同的功能。
您仍应避免过度递归,因为每个内部函数调用至少使用一个堆栈槽,并且最多有1024个可用插槽。
表达式this.g(8);
和c.g(2);
(其中c
是契约实例)也是有效的函数调用,但这次,函数将通过消息调用“外部”调用,而不是直接通过跳转调用。请注意,函数调用this
不能在构造函数中使用,因为尚未创建实际的合约。
其他合约的功能必须在外部调用。对于外部调用,必须将所有函数参数复制到内存中。
注意
从一个合约到另一个合约的函数调用不会创建自己的事务,它是作为整个事务的一部分的消息调用。
在调用其他合约的功能时,可以使用特殊选项指定通过call和gas发送的Wei数量,.value()
并.gas()
分别:
pragma solidity >=0.4.0 <0.6.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public { feed = addr; }
function callFeed() public { feed.info.value(10).gas(800)(); }
}
您需要将修饰符payable
与info
函数一起使用,否则该.value()
选项将不可用。
警告
注意feed.info.value(10).gas(800)
只在本地设置函数调用发送的value
数量和数量,gas
最后的括号执行实际调用。所以在这种情况下,不调用该函数。
如果被调用的合约不存在(在该帐户不包含代码的意义上)或者被调用的合约本身抛出异常或没有gas,则函数调用会导致异常。
警告
与另一份合同的任何互动都会带来潜在的危险,特别是如果事先不知道合约的源代码。目前的合约将控制权移交给被call的合约,这可能会对任何事情产生影响。即使被调用的合约继承自已知的父合约,继承合约也只需要具有正确的接口。然而,合约的实施可以完全是任意的,因此构成危险。此外,如果它在第一次调用返回之前调用您系统的其他合约或甚至返回到调用合约,请做好准备。这意味着被调用的合约可以通过其函数来改变调用合约的状态变量。以某种方式编写函数,例如,
函数调用参数可以按名称以任何顺序给出,如果它们被包含在中,如下例所示。参数列表必须与函数声明中的参数列表一致,但可以按任意顺序排列。{ }
pragma solidity >=0.4.0 <0.6.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
可以省略未使用参数的名称(尤其是返回参数)。这些参数仍然存在于堆栈中,但它们无法访问。
pragma solidity >=0.4.16 <0.6.0;
contract C {
// omitted name for parameter
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
new
合约可以使用new
关键字创建其他合约。编译创建合约时必须知道正在创建的合约的完整代码,因此无法实现递归创建依赖性。
pragma solidity >0.4.99 <0.6.0;
contract D {
uint public x;
constructor(uint a) public payable {
x = a;
}
}
contract C {
D d = new D(4); // will be executed as part of C's constructor
function createD(uint arg) public {
D newD = new D(arg);
newD.x();
}
function createAndEndowD(uint arg, uint amount) public payable {
// Send ether along with the creation
D newD = (new D).value(amount)(arg);
newD.x();
}
}
如示例所示,可以在创建D
使用该.value()
选项的实例时发送以太网,但不能限制gas量。如果创建失败(由于堆栈外,没有足够的平衡或其他问题),则抛出异常。
未指定表达式的求值顺序(更正式地,未指定表达式树中一个节点的子节点的计算顺序,但它们当然在节点本身之前进行求值)。只保证语句按顺序执行,并且完成布尔表达式的短路。有关更多信息,请参阅运算符的优先顺序。
Solidity内部允许元组类型,即可能不同类型的对象列表,其编号在编译时是常量。这些元组可以用于同时返回多个值。然后可以将这些变量分配给新声明的变量或预先存在的变量(或一般的LValues)。
元组在Solidity中不是正确的类型,它们只能用于形成表达式的句法分组。
pragma solidity >0.4.23 <0.6.0;
contract C {
uint[] data;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
// Variables declared with type and assigned from the returned tuple,
// not all elements have to be specified (but the number must match).
(uint x, , uint y) = f();
// Common trick to swap values -- does not work for non-value storage types.
(x, y) = (y, x);
// Components can be left out (also for variable declarations).
(data.length, , ) = f(); // Sets the length to 7
}
}
无法混合变量声明和非声明赋值,即以下内容无效: (x, uint y) = (1, 2);
注意
在版本0.5.0之前,可以分配给较小尺寸的元组,或者填充在左侧或右侧(它曾经是空的)。现在不允许这样做,因此双方必须拥有相同数量的组件。
警告
在涉及引用类型的同时分配多个变量时要小心,因为它可能导致意外的复制行为。
对于非值类型(如数组和结构),赋值的语义稍微复杂一些。分配给状态变量始终会创建一个独立的副本。另一方面,分配给局部变量只为基本类型创建一个独立的副本,即适合32个字节的静态类型。如果将结构或数组(包括bytes
和string
)从状态变量赋值给局部变量,则局部变量保存对原始状态变量的引用。对局部变量的第二次赋值不会修改状态,只会更改引用。对局部变量的成员(或元素)的赋值确实会改变状态。
声明的变量将具有初始默认值,其字节表示全为零。变量的“默认值”是任何类型的典型“零状态”。例如,对于一个的默认值bool
是false
。uint
或int
类型的默认值是0
。对于静态大小的数组和bytes1
对bytes32
,每个单独的元件将被初始化为对应于其类型的默认值。最后,对于动态大小的阵列,bytes
并且string
,默认值是空数组或字符串。
在Solidity中确定范围遵循C99(以及许多其他语言)的广泛范围规则:变量在其声明之后直到包含声明的最小块的结尾处可见。作为此规则的一个例外,在for循环的初始化部分中声明的变量只有在for循环结束时才可见。{ }
在代码块之外声明的变量和其他项,例如函数,合约,用户定义的类型等,甚至在声明之前都是可见的。这意味着您可以在声明它们之前使用状态变量,并递归调用函数。
因此,以下示例将在没有警告的情况下进行编译,因为这两个变量具有相同的名称但是不相交的范围。
pragma solidity >0.4.99 <0.6.0;
contract C {
function minimalScoping() pure public {
{
uint same;
same = 1;
}
{
uint same;
same = 3;
}
}
}
作为C99范围规则的一个特例,请注意,在下文中,第一个赋值x
将实际分配外部变量而不是内部变量。在任何情况下,您都会收到有关被遮蔽的外部变量的警告。
pragma solidity >0.4.99 <0.6.0;
// This will report a warning
contract C {
function f() pure public returns (uint) {
uint x = 1;
{
x = 2; // this will assign to the outer variable
uint x;
}
return x; // x has value 2
}
}
警告
在版本0.5.0之前,Solidity遵循与JavaScript相同的作用域规则,也就是说,在函数内任何地方声明的变量都在整个函数的范围内,无论它在何处被声明。以下示例显示了用于编译但导致从版本0.5.0开始的错误的代码段。
pragma solidity >0.4.99 <0.6.0;
// This will not compile
contract C {
function f() pure public returns (uint) {
x = 2;
uint x;
return x;
}
}
Solidity使用状态恢复异常来处理错误。这样的异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。便利功能assert
和require
可用于检查的条件,并抛出一个异常,如果条件不满足。该assert
函数仅应用于测试内部错误,并检查不变量。该require
函数应用于确保满足有效条件(如输入或合同状态变量),或验证调用外部合约的返回值。如果使用得当,分析工具可以评估您的合同,以确定将导致失败的条件和函数调用assert
。正常运行的代码永远不会达到失败的断言语句; 如果发生这种情况,您的合同中有一个错误,您应该修复。
还有另外两种触发异常的方法:该revert
函数可用于标记错误并恢复当前调用。可以提供一个字符串消息,其中包含有关将传递回调用者的错误的详细信息。
注意
曾经有一个被调用的关键字throw
具有与revert()
版本0.4.13中不推荐使用并在版本0.5.0中删除的相同语义。
当子调用中发生异常时,它们会自动“冒泡”(即异常被重新抛出)。此规则的例外是send
和低级函数call
,delegatecall
并且staticcall
- false
在异常的情况下返回作为它们的第一个返回值而不是“冒泡”。
警告
低级别的功能call
,delegatecall
并staticcall
返回true
他们的第一个返回值,如果被叫帐户是不存在的,如EVM的设计的一部分。如果需要,必须在调用之前检查存在性。
捕捉异常尚不可能。
在下面的示例中,您可以看到如何require
使用它来轻松检查输入条件以及如何assert
用于内部错误检查。请注意,您可以选择提供消息字符串require
,但不能提供assert
。
pragma solidity >0.4.99 <0.6.0;
contract Sharer {
function sendHalf(address payable addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = address(this).balance;
addr.transfer(msg.value / 2);
// Since transfer throws an exception on failure and
// cannot call back here, there should be no way for us to
// still have half of the money.
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
return address(this).balance;
}
}
一种assert
风格的例外在下列情况下产生的:
x[i]
where 或)访问数组。i >= x.length
i < 0
bytesN
以过大或负的索引访问固定长度。5 / 0
23 % 0
assert
使用计算结果为false的参数调用。require
在以下情况下会生成A 风格的异常:
require
使用求值为的参数调用false
。call
,send
,delegatecall
,callcode
或staticcall
使用。低级操作从不抛出异常,但通过返回指示失败false
。new
关键字创建合同但合同创建未正确完成(请参阅上面的“未正确完成”的定义)。payable
修饰符(包括构造函数和回退函数)。.transfer()
失败了。在内部,Solidity 0xfd
对require
a-样式异常执行恢复操作(指令),并执行无效操作(指令0xfe
)以抛出assert
样式异常。在这两种情况下,这都会导致EVM还原对状态所做的所有更改。恢复的原因是没有安全的方法来继续执行,因为没有发生预期的影响。因为我们希望保留事务的原子性,所以最安全的做法是还原所有更改并使整个事务(或至少调用)不起作用。请注意,assert
样式异常消耗了call可用的所有gas,而 require
样式异常不会消耗从Metropolis版本开始的任何gas。
以下示例显示了如何将错误字符串与revert和require一起使用:
pragma solidity >0.4.99 <0.6.0;
contract VendingMachine {
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// Alternative way to do it:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// Perform the purchase.
}
}
提供的字符串将被abi编码,就好像它是对函数的调用一样Error(string)
。在上面的示例中,将导致以下十六进制数据设置为错误返回数据:revert("Not enough Ether provided.");
0x08c379a0 // Function selector for Error(string)
0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset
0x000000000000000000000000000000000000000000000000000000000000001a // String length
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data