函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。例如,如果希望合约接受有两个整数形参的函数的外部调用,可以像下面这样写:
pragma solidity ^0.5.0;
contract Simple {
uint sum;
function taker(uint _a, uint _b) public {
sum = _a + _b;
}
}
函数参数可以当作为本地变量,也可用在等号左边被赋值。
函数返回变量的声明方式在关键字returns
之后,与参数的声明方式相同。
例如,如果我们需要返回两个结果:两个给定整数的和与积,应该写作:
pragma solidity ^0.5.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;
}
}
返回变量名可以被省略。 返回变量可以当作为函数中的本地变量,没有显式设置的话,会使用 默认值 <default-value>
返回变量可以显式给它附一个值,也可以使用 return 语句指定,使用return语句可以一个或多个值。
pragma solidity ^0.5.0;
contract Simple {
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
return (_a + _b, _a * _b);
}
}
这个形式等同于赋值给返回参数,然后用 return; 退出。
当函数需要使用多个值,可以用语句 return (v0, v1, ..., vn) 。 参数的数量需要和声明时候一致。
变量无论在函数内什么位置定义,其作用域均为整个函数,而非大多数据语言常见的块级作用域。下面的例子会报错DeclarationError: Undeclared identifier.
。
pragma solidity ^0.5.0;
contract ScopeErr{
function f() pure public returns (uint8) {
{ uint8 a = 0;}
return a;
}
}
上例中,由于变量作用域不同,导致a在函数里面并没有被声明,在返回的时候就会报错。
来看一个稍微隐蔽点的例子:
pragma solidity ^0.5.0;
contract FunctionScope{
function f() public view returns (uint8){
for(var i = 0; i < 10; i++){
//do sth
}
return i;
}
}
读者可以试想一下上述代码能否编译通过呢。 答案是不可以的,原因就是因为i变量虽然是在for循环中被定义,但它的的作用域不是整个函数,但是在函数内的任何位置均可以访问到这个变量。
函数是合约中代码的可执行单元,除了对区块链分布式系统设计的internal
和external
这两种不同的函数调用方式,Solidity还提供对函数可见性控制的语法。
Solidity封装了两种函数的调用方式internal
和external
两种。
internal
调用,实现时转为简单的EVM跳转,可以直接使用上下文环境中的数据,对于引用传递将会变得非常高效(不用拷贝数据)。在当前的代码单元内,如对合约内函数,引入的函数库,以及父类合约中的函数直接使用即是以internal
方式的调用;
pragma solidity ^0.5.0;
contract SimpleAuction {
function f() public {}
function callInternally() public {
f();
}
}
在上述代码中,callInternally()以internal的方式对f()函数进行了调用。
external
调用,实现为合约的外部消息调用。合约初始化时不能external
的方式调用自身函数,因为合约还未初始化完成。
pragma solidity ^0.5.0;
contract A {
function f() public pure returns (uint8) {
return 0;
}
}
contract B {
function callExternal(A a) public pure returns (uint8) {
return a.f();
}
}
虽然合约A和B的代码在一个文件中,但部署到区块链上后,它们是完全独立的两个合约,它们之间的调用方法是通过消息调用的,上述代码中,在合约B中的callExternal()
以external
的方式调用了合约A
的f()
。
external
调用时,实际是向目标合约发送一个消息调用,消息中的函数定义部分是一个24字节大小的消息体 ,20个字节的地址,4个字节的函数签名。
external
调用还有另一种写法,可以在合约的调用函数前加this
来强制的以external
方式调用,需要注意这里的this
和大多数语言的意思都不一致。
pragma solidity ^0.5.0;
contract A {
function f() public {}
function callExternally() public {
this.f();
}
}
Solidity的函数除了internal
和external
的调用方式区别之外,还有函数可见性的修饰语法。
Solidity为函数提供了四种可见性,external
, public
, internal
, private
。
external
- 声明为
external
的函数可以从其他合约或者通过Transaction
调用,所以声明为external
的函数是合约对外接口的一部分。 - 不能以
internal
的方式进行调用。 - 有时在接收大的数组时性能会更好。
pragma solidity ^0.5.0;
contract FuntionTest{
function externalFunc() external{}
function callFunc() public {
//以`external`的方式调用函数
this.externalFunc();
}
}
声明为external的externalFunc()只能以external的方式进行调用,以internal的方式调用会报Error: Undeclared identifier.
。
public
声明为public
的函数既可以用internal
的方式调用,也允许以external
的方式调用,也是合约对外接口的一部分。
pragma solidity ^0.5.0;
contract FuntionTest{
//默认是public函数
function publicFunc() public {}
// function publicFunc(){} 等价于 function publicFunc() public {}
function callFunc() public {
//以`internal`的方式调用函数
publicFunc();
//以`external`的方式调用函数
this.publicFunc();
}
}
我们可以看到声明为public的publicFunc()允许两种调用方式。
internal
在当前的合约或继承的合约中,只允许以internal的方式调用。
pragma solidity ^0.4.5;
contract A{
//默认是public函数
function internalFunc() internal{}
function callFunc(){
//以`internal`的方式调用函数
internalFunc();
}
}
contract B is A{
//子合约中调用
function callFunc(){
internalFunc();
}
}
上述例子中声明为internal的internalFunc()在定义合约,和子合约中均只能以internal的方式可以进行调用。
private
只能在当前合约中被访问(不可在被继承的合约中访问)。
即使声明为private,仍能被所有人查看到里面的数据。访问权限只是阻止了其它合约访问函数或修改数据。
pragma solidity ^0.4.5;
contract A{
//默认是public函数
function privateFunc() private{}
function callFunc(){
//以`internal`的方式调用函数
privateFunc();
}
}
contract B is A{
//不可调用`private`
function callFunc(){
//privateFunc();
}
}
上述例子中,声明为private的privateFunc()只能在定义的合约中以internal的方式进行调用。
使用 修饰器 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修饰器 是合约的可继承属性, 并可能被派生合约覆盖。
pure
装饰器,在函数中使用pure装饰器后表示函数不会修改或访问状态变量,对外界没有任何影响。
pragma solidity ^0.4.23;
contract HelloWorld{
function testPure(uint a,uint b) public pure returns(uint){
return a+b;
}
}
比如testPure
函数,既没有修改传递进来的值(值传递),又没有修改状态变量,只是单纯的进行了一个计算,并不会消耗任何gas,应为执行使用的是本地cpu计算,并没有消耗任何链上资源。
view
装饰器,在函数中使用view装饰器后表示函数不会修改状态变量,对链上只有读取操作,同样也不消耗gas。其实很简单,因为作为一个全节点来说,会同步保存所有的信息,保存在本地中,那么我们要查看区块链上的资源,同样可以直接在一个全节点之上查询数据即可,我不需要全世界的节点都知道。都去同时的处理这笔事务。我也不需要将调用这笔函数的信息记录在区块链上。
pragma solidity ^0.5.0;
contract HelloWorld{
string public name = "qyuan";
function getName() public view returns (string memory) {
return name;
}
}
payable
装饰器,允许函数在调用时可以接收和发送以太币。
constant
装饰器等同于view
装饰器,不过在0.5.0后面的版本移除了。
以太坊的智能合约,可以声明一个匿名函数(unnamed function),叫做回退函数(Fallback)函数,这个函数不带任何参数,也没有返回值。当向这个合约发送消息时,如果没有找到匹配的函数就会调用fallback函数。比如向合约转账,但要合约接收 Ether,那么 fallback 函数必须声明为 payable,否则试图向此合约转 ETH 将失败。如下:
function() payable public { // payable 关键字,表明调用此函数,可向合约转 Ether。
}
向合约发送send、transfer、call 消息时候都会调用 fallback 函数,不同的是 send 和 transfer有2300gas的限制,也就是传递给fallback的只有2300gas,这个gas只能用于记录日志,因为其他操作都将超过2300gas。但call则会把剩余的所有gas都给fallback函数,这有可能导致循环调用。严重威胁智能合约安全的代码重入漏洞就是因为通过调用call函数转账时触发fallback函数所引起的。
事件是能方便地调用以太坊虚拟机日志功能的接口,说到事件必然就会提到日志,事件被触发后就会被记录到区块链上就成为了日志,事件是一种行为,日志是对这种行为的记录。 事件是以太坊EVM提供的一种基础设施,用来实现一些交互功能,比如通知UI,返回函数调用结果等。
当定义的事件被触发时,被记录到日志中,日志又与合约相关联一起存储到区块链中,只要某个区块可以访问,其相关的日志就能访问,但是在合约中,并不能直接访问日志中的事件数据。
pragma solidity ^0.4.21;
contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount); // 事件
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
}
}
使用event
关键字定义一个事件,参数列表中为要记录的日志参数的名称及类型。
var event = myContract.HighestBidIncreased(function(error, result){
for(let key in result){
console.log(key + " : " + result[key]);
}
});
Indexed
属性,可以在事件参数上增加Indexed
属性,最多可以对三个参数增加这样的属性,加上这个属性,就可以允许在web3.js中通过对加了这个属性的参数值进行过滤。
var event = myContract.transfer({value: "100"});
上面实现的是对value值为100的日志,过滤后的返回。 如果你想同时匹配多个值,还可以传入一个要匹配的数组。
var event = myContract.transfer({value: ["99","100","101"]});
增加了indexed
的参数值会存到日志结构的Topic
部分,便于快速查找。而未增加indexed
的参数会存在data
部分成为了原始日志。需要注意的是,如果增加indexed
属性的是数组(包括string
和bytes
),那么只会在Topic
存储对应的sha3哈希值,不会存储原始数据。
因为Topic是用于快速查找的,不能存任意长度的数据,所以通过Topic实际存在的是数组这种非固定长度数据的哈希结果,要查找时,是将要查找的内容与Topic内容进行匹配,但不能反推哈希结果,从而不能得到原始的值。