智能合约语言 Solidity 教程 - 函数调用
函数调用及参数
内部调用是一种在智能合约内部执行函数的方式,它不会创建一个新的以太坊虚拟机(EVM)消息调用。相反,它会直接调用当前合约内部的函数,还可以在函数内部递归地再次执行相同的函数。

如下面这个例子:
pragma solidity ^0.4.16;
contract C {
function g(uint a) public pure returns (uint ret) {
return f(); // 直接调用
}function f() internal pure returns (uint ret) {
return g(7) + f(); // 直接调用及递归调用
}
}
这些函数调用被处理为以太坊虚拟机(EVM)内部的简单指令跳转,而不是创建新的消息调用。这种方法的好处在于,当前内存不会被回收。在内部调用中,可以非常高效地传递内存引用。需要注意的是,内部调用只能在同一个智能合约的不同函数之间进行。
外部函数调用(External Function Calls)
外部调用涉及到创建以太坊虚拟机(EVM)消息调用。例如,表达式this.g(8);
和 c.g(2);
(这里的c
是一个合约实例)都采用外部函数调用方式,它们会触发一个新的消息调用,而不是直接在EVM中执行指令跳转。需要注意的是,在合约构造函数中,不能使用this
来调用函数,因为合约尚未完全创建。
在调用另一个合约的函数时,必须将所有函数参数复制到内存中。此外,在进行外部调用时,可以使用选项.value()
和 .gas()
分别指定要发送的以太币(以wei为单位)和 gas 值。如:
pragma solidity ^0.4.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}contract Consumer {
InfoFeed feed;function setFeed(address addr) public {
feed = InfoFeed(addr);
}function callFeed() public {
feed.info.value(10).gas(800)(); // 附加以太币及gas来调用info
}
}
为了在 info()
函数中接收以太币,必须在函数声明中使用 payable
关键字。否则,无法通过 value()
方法来接收以太币。
表达式 InfoFeed(addr)
是一个显式类型转换,用于将给定的地址标识为类型为 InfoFeed
的合约。这并不会触发构造函数的初始化。在进行显式类型转换时,必须非常小心,确保不要调用未知类型的合约函数。
另一种方式是使用 function setFeed(InfoFeed _feed) { feed = _feed; }
来直接为 feed
变量赋值。这种方式更加明确,通过传递一个已知的 InfoFeed
类型的参数,可以避免不必要的风险。
注意feed.info.value(10).gas(800)**仅仅是对发送的以太币和gas值进行了设置,真正的调用是后面的括号()。
调用callFeed时,需要预先存入一定量的以太币,不然会因为余额不足报错。
当与不熟悉的合约互动时,存在潜在风险,即使被调用的合约继承自已知的父合约(这种继承只要求正确实现接口,而不关心实际实现内容)。这是因为与这些合约进行交互等于将控制权交给被调用的合约,允许它几乎可以执行任何操作。
此外,被调用的合约还可以更改调用合约的状态变量。因此,在编写函数时,需要非常小心,以避免潜在的可重入性漏洞,这些漏洞可能导致不希望的行为。我们可以查看安全建议来详细了解如何防范这些风险。
函数参数
与其他编程语言类似,Solidity中的函数可以接受参数作为输入,包括函数类型本身可以作为参数传递。不同于JavaScript和C的是,Solidity还支持函数返回多个参数作为输出。这意味着函数可以返回多个值,而不仅仅是单一的返回结果。
输入参数
输入参数的声明方式与变量相同, 未使用的参数可以省略变量名称。假设我们希望合约接受一种带有两个整数参数的外部调用,可以这样写:
pragma solidity ^0.4.16;
contract Simple {
function taker(uint _a, uint _b) public pure {
// 使用 _a _b
}
}
输出参数
输出参数的声明和输入参数一样,只不过它接在returns 之后,假设我们希望返回两个结果:两个给定整数的和及积,可以这样写:
pragma solidity ^0.4.16;
contract Simple {
function arithmetics(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
在Solidity中,你可以省略输出参数的名称,也可以使用return
语句明确指定函数的输出值。此外,return
语句可以用来返回多个值(如下所示)。
如果没有明确为输出参数赋值,它们默认为0。
输入参数和输出参数可以像普通变量一样在函数内的表达式中使用,也可以作为赋值的目标,如下所示:
contract Simple {
function taker(uint _a, uint _b) public pure returns (uint _c) {
_a = 1;
_b = 2;
_c = 3;
}
}
返回多个值
当一个函数需要返回多个输出参数时,Solidity允许你使用元组(tuple)来封装多个值。元组是一个包含固定数量、可以是不同类型的元素的数据结构,通常用小括号来表示。你可以使用 return (v0, v1, ..., vn)
语句来返回多个值,确保返回的元组中的元素数量与函数的输出参数声明一致。
function f() public pure returns (uint, bool, uint) {
// 使用元组返回多个值
return (7, true, 2);
}function callf() public {
uint x;
bool y;
uint z;
// 使用元组给多个变量赋值
(x, y , z) = f();
}
补充关于元组的介绍
在上面的代码中,我们使用了元组来返回多个值,并且还使用元组来实现变量的赋值,这通常被称为解构。解构是一种常见的概念,特别在函数式编程语言中经常使用。
此外,元组还有其他有用的用途,比如可以用来快速交换变量的值。
(x, y) = (y, x);
元组支持省略一些元素, 如:
(x, y, ) = (1, 2, 4);
开头的元素也可以省略,如:
(, y, ) = (1, 2, 4);
注意 (1,) 是一个一个元素的元组, (1) 只是1。
使用命名参数调用
在函数调用中,你可以使用指定名称的方式传递参数,将参数包装在花括号 {}
内,而且不需要按照参数的顺序,只需确保参数的名称、类型和数量与函数定义匹配即可。这种方式提供了更大的灵活性,你可以根据需要以任意顺序提供参数。如:
pragma solidity ^0.4.0;
contract C {
function f(uint key, uint value) public {
// ...
}function g() public {
f({value: 2, key: 3}); // 命名参数
}
}
省略函数参数名称
没有使用的参数名称可以省略(一般常见于返回值)。这些参数依然在栈(stack)上存在,但不可访问。
pragma solidity ^0.4.16;
contract C {
// omitted name for parameter
function func(uint k, uint) public pure returns(uint) {
return k;
}
}