任何语言都离不开函数,这篇文章主要是来说说Solidity中的函数是如何构成的,以及主要的关键字。
solidity官方文档中,函数的形式是这样的
function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]
看着些复杂,我们可以从前往后一个一个看(方括号中的是可写可不写的关键字):
function: 声明函数时的固定用法,想写函数,就要以function关键字开头。<function name>:函数名。(<parameter types>):圆括号里写函数的参数,也就是要输入到函数的变量类型和名字。一般来说,形参是以下划线开始,比如uint _id这样。{internal|external|public|private}:函数可见性说明符,一共4种。没标明函数类型的,默认public。合约之外的函数,即"自由函数",始终具有隐含internal可见性。
public: 内部外部均可见。private: 只能从本合约内部访问,继承的合约也不能用。external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)。internal: 只能从合约内部访问,继承的合约可以用。
Note1 : 没有标明可见性类型的函数,默认为public。
Note2 : public|private|internal 也可用于修饰状态变量(eg:public uint _id)。 public变量会自动生成同名的getter函数,用于查询数值。
Note3 : 没有标明可见性类型的函数,默认为public。
什么是Pure和View?
刚开始学solidity的时候,一直不理解pure跟view关键字,因为别的语言没有类似的关键字。solidity加入这两个关键字,我认为是因为gas fee。合约的状态变量存储在链上,gas fee很贵,如果不改变链上状态,就不用付gas。包含pure跟view关键字的函数是不改写链上状态的,因此用户直接调用他们是不需要付gas的(合约中非pure/view函数调用它们则会改写链上状态,需要付gas)。
pure,中文意思是“纯”,在solidity里理解为“纯纯牛马”。包含pure关键字的函数,不能读取也不能写入存储在链上的状态变量。
view,“看”,在solidity里理解为“看客”。包含view关键字的函数,能读取但也不能写入状态变量。
不写pure也不写view,函数既可以读取也可以写入状态变量。
在以太坊中,以下语句被视为修改链上状态:
- 写入状态变量。
- 释放事件。
- 创建其他合约。
- 使用
selfdestruct.(selfdestruct 已经被 EIP-6049 弃用了一段时间。弃用仍然有效,编译器仍会对其使用发出警告。即使考虑到新的行为,也强烈不建议在新部署的合约中使用) - 通过调用发送以太币。
- 调用任何未标记
view或pure的函数。 - 使用低级调用(low-level calls)。
- 使用包含某些操作码的内联汇编。
下面用个几个例子来说明一下
1 pure 和 view
我们在合约里定义一个状态变量 number = 5。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract FunctionTypes{
uint256 public number = 5;
}
定义一个add()函数,每次调用,每次给number + 1。
// 默认
function add() external{
number = number + 1;
}
如果add()包含了pure关键字,例如 function add() external pure,就会报错。因为pure(纯纯牛马)是不配读取合约里的状态变量的,更不配改写。那pure函数能做些什么?举个例子,你可以给函数传递一个参数 _number,然后让他返回 _number+1。
// pure: 纯纯牛马
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number + 1;
}
如果add()包含view,比如function add() external view,也会报错。因为view能读取,但不能够改写状态变量。可以稍微改写下方程,让他不改写number,而是返回一个新的变量。
// view: 看客
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}
2 internal和external
// internal: 内部
function minus() internal {
number = number - 1;
}
// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}
我们定义一个internal的minus()函数,每次调用使得number变量减1。由于是internal,只能由合约内部调用,而外部不能。因此,我们必须再定义一个external的minusCall()函数,来间接调用内部的minus()。
3 payable 可以收钱的函数
// payable: 递钱,能给合约支付eth的函数
function minusPayable() external payable returns(uint256 balance) {
minus();
balance = address(this).balance;
}
我们定义一个external payable的minusPayable()函数,间接的调用minus(),并且返回合约里的ETH余额(this关键字可以让我们引用合约地址)。 我们可以在调用minusPayable()时,往合约里转入1个ETH。
我们可以在返回的信息中看到,合约的余额是1 ETH。
返回值 return和returns
Solidity有两个关键字与函数输出相关:return和returns,他们的区别在于:
returns加在函数名后面,用于声明返回的变量类型及变量名;return用于函数主体中,返回指定的变量。
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
return(1, true, [uint256(1),2,5]);
}
上面这段代码中,我们声明了returnMultiple()函数将有多个输出:returns(uint256, bool, uint256[3] memory),接着我们在函数主体中用return(1, true, [uint256(1),2,5])确定了返回值。
命名式返回
我们可以在returns中标明返回变量的名称,这样solidity会自动给这些变量初始化,并且自动返回这些函数的值,不需要加return。
// 命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
_number = 2;
_bool = false;
_array = [uint256(3),2,1];
}
在上面的代码中,我们用returns(uint256 _number, bool _bool, uint256[3] memory _array)声明了返回变量类型以及变量名。这样,我们在主体中只需要给变量_number,_bool和_array赋值就可以自动返回了。
当然,你也可以在命名式返回中用return来返回变量:
// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}
解构式赋值
solidity使用解构式赋值的规则,支持读取函数的全部或部分返回值。
- 读取所有返回值:声明变量,并且将要赋值的变量用
,隔开,按顺序排列。
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
- 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。下面这段代码中,我们只读取
_bool,而不读取返回的_number和_array:
(, _bool2, ) = returnNamed();
总结
我们介绍了solidity中的函数,比较难理解的是pure和view,在其他语言中没出现过。solidity引入pure和view关键字主要是为了节省gas和控制函数权限:如果用户直接调用pure/view方程是不消耗gas的(合约中非pure/view函数调用它们则会改写链上状态,需要付gas)。