Solidity08 Solidity 函数
文章目录
- 一、函数声明
- 二、函数声明语法
- 2.1 参数列表
- 2.2 返回值列表
- 2.3 函数可见性
- 2.3.1 Solidity 可见性
- 2.3.2 合约分类
- 2.3.3 可见性对于合约访问的限制
- 2.4 函数状态可变性
- 2.4.1 怎样才算查询合约状态
- 2.4.2 怎样才算修改合约状态
- 2.4.3 view 函数
- 2.4.4 pure 函数
- 2.4.5 payable 函数
- 2.4.6 到底什么是 `Pure` 和`View`?
- 2.4.7 状态可变性与 Gas
- 2.5 函数修饰器
- 2.6 函数输出
- 2.6.1 返回值:return 和 returns
- 2.6.2 命名式返回
- 2.6.3 解构式赋值
- 三、sol合约代码示例
函数是一组逻辑代码块,代表着合约可以进行的的某项操作或者行为。它是构建合约的基本组成单位之一。 例如一个借贷合约可能包括几个操作:提供款项(supply),借款(borrow),还款(repay)等都可以实现为函数。
一、函数声明
我们先来看一个简单函数声明的例子:
实现了一个 add 函数,对两个数相加求和
function add(uint lhs, uint rhs) public pure returns(uint) {
return lhs + rhs;
}
我们一个字段一个字段地看看这个函数声明中每个部分的含义。
function
函数声明需要以 function 开头add
函数的名称(uint lhs, uint rhs)
函数的输入参数列表,这里有两个参数,分别是 lhs 和 rhs,都是无符号整型(uint)public
函数可见性,public 表示这个函数可以被内部或外部调用pure
函数状态可变性,pure 表示这个函数不会查询或更改合约状态returns(uint)
函数的返回值列表,是一个无符号整型{ return lhs + rhs; }
函数主体,包含了实际要执行的函数逻辑, 在这里的函数逻辑是返回 lhs 和 rhs 的和
这个函数的作用是接受两个无符号整型的参数 lhs
和 rhs
,并返回它们的和。这个函数是可以被外部调用的,并且是纯函数,不会更改合约状态。
二、函数声明语法
看完了上面的例子,我们现在再来看函数声明的语法就一目了然了。下图表示了函数声明应该包含的内容:
-
function:声明函数时的固定用法。要编写函数,就需要以
function
关键字开头。 -
myFuncName:函数的名称
-
( parameter-list ):函数的参数列表,即输入到函数的变量类型和名称。
-
visibility :函数可见性,有四个选项:internal, external, private, public
public
:内部和外部均可见。private
:只能从本合约内部访问,继承的合约也不能使用。external
:只能从合约外部访问(但内部可以通过this.f()
来调用,f
是函数名)。internal
: 只能从合约内部访问,继承的合约可以用。
注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。
注意 2:
public|private|internal
也可用于修饰状态变量。public
变量会自动生成同名的getter
函数,用于查询数值。未标明可见性类型的状态变量,默认为internal
。 -
state mutability:决定函数权限/功能的关键字,有三个选项:pure, view, payable
payable
(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。
-
modifiers :自定义的函数修饰器(可选), 可以限制, 修改函数的行为,可以有0个或多个修饰器。
-
returns (return-list) :函数返回值列表(可选),指定返回值的类型,可以是多个返回值
-
{ // statements } :函数主体, 包含实际执行的操作
注意: state-mutability, modifiers, parameter-list, return-list
都是可选的。所以函数声明可以是下面这样:
// 仅指定 函数可见性
function f1() public {}
// 指定 函数可见性 和 函数状态可变性
function f2() public pure {}
// 指定 函数可见性 和 函数状态可变性
function f3() public view {}
// 指定 函数可见性 和 函数状态可变性,函数有一个入参
function f4(uint a) public pure {}
// 指定 函数可见性 和 函数状态可变性,函数有一个入参和一个返回值
function f5(uint a) public pure returns(uint) {}
// 指定 函数可见性 和 函数状态可变性,函数有多个入参和多个返回值
function f6(uint a, uint b, uint c) public pure returns(uint, uint, uint) {}
// 指定 函数可见性 和 函数修饰器
function f7(uint a) external onlyOwner {} // onlyOwner 修饰器需要事先定义
2.1 参数列表
我们可以为函数提供零或多个参数。多个参数可以使用逗号进行分割。如下所示:
// 提供0个参数
function foo() public {};
// 提供1个参数
function foo(uint a) public {};
// 提供多个参数
function foo(uint a, uint b, uint c) public {};
2.2 返回值列表
我们可以为函数提供零或多个返回值。多个返回值可以使用逗号进行分割。如下所示:
// 提供0个返回值
function foo() public {};
// 提供1个返回值
function foo() public returns(uint) {};
// 提供多个返回值
function foo() public returns(uint, uint, uint) {};
2.3 函数可见性
2.3.1 Solidity 可见性
为了保证合约安全性, Solidity 允许对变量和函数的访问进行限制。开发者可以自己指定当前合约的变量和函数可以被哪些合约访问。而 Solidity 中的可见性( visibility )指的是其他合约对当前合约中的变量或函数的可访问性( accessibility )。换一种说法就是:其他合约是否能访问当前合约的变量和函数是由可见性决定的。Solidity 中的「可见性修饰符」是用来指定「可见性」层级的。
Solidity支持四种可见性修饰符:
-
public
-
private
-
internal
-
external
其中,变量可以使用的可见性修饰符有三个:
public
表示该变量可以在当前合约内部和外部访问private
表示该变量只能在当前合约内部访问internal
表示该变量只能在当前合约内部或其「子合约」中访问
而函数可以使用上面所有四个修饰符:
public
表示该函数可以在当前合约内部和外部访问external
表示该函数只能在当前合约外部访问private
表示该函数只能在当前合约内部访问internal
表示该函数只能在当前合约内部或其「子合约」中访问
2.3.2 合约分类
在我们继续讨论可见性之前,先对合约进行一下分类。因为可见性影响到不同类型的合约对「当前合约」的可访问性(accessibility)。根据可见性对不同合约可访问性的影响,我们可以把合约分为三类:
- 主合约 (也就是当前合约内部)
- 子合约 (也就是继承「当前合约」的合约)
- 第三方合约 (也就是当前合约外部)
简单来讲,子合约继承了主合约,而第三方合约跟主/子合约没有任何继承关系。把主合约和子合约想象成一个家庭的话,第三方合约就是个陌生人。
要注意我们所谈到的访问限制都是以主合约(当前合约)为视角的。也就是当我们在编写主合约的时候,使用可见性修饰符来决定哪些合约可以访问主合约的哪些变量和函数。
主合约
主合约其实就是一个普通合约,内部定义了很多变量和函数。这些变量和函数可能有不同的可见性。主合约可以访问自己内部可见性为 private
, internal
, public
的任何变量和函数。
// 主合约可以访问自己内部可见性为 private, internal, public 的变量和函数
contract MainContract {
uint varPrivate;
uint varInternal;
uint varPublic;
function funcPrivate() private {}
function funcInternal() internal {}
function funcExternal() external {}
function funcPublic() public {}
}
子合约
子合约继承了主合约。继承的语法是 Child is Parent 。关于继承有关的详细介绍,我们此处不再知识点延伸。子合约允许访问主合约中可见性为 internal , public 的函数。
contract ChildContract is MainContract {
function funcChild() private {
funcInternal(); // 子合约可以访问主合约中可见性为internal,public的函数
funcPublic(); // 子合约可以访问主合约中可见性为internal,public的函数
}
}
第三方合约
第三方合约是一个普通合约。可以通过主合约的地址来与主合约进行交互, 其交互语法如下所示。第三方合约可以访问主合约中可见性为 external
, public
的函数
contract ThirdPartyContract {
function funcThirdParty(address mainContractAddress) private {
MainContract(mainContractAddress).funcExternal();//第三方合约可以访问主合约中可见性为external,public的函数
MainContract(mainContractAddress).funcPublic(); //第三方合约可以访问主合约中可见性为external,public的函数
}
}
2.3.3 可见性对于合约访问的限制
我们已经提到,可见性就是合约变量和函数的可访问性。那么可见性对每种类型合约的访问限制是什么呢?我们继续延伸一下
public
可见性为 public
的变量和函数可以被任何合约访问。也就是可以被 MainContract
, ChildContract
, ThirdPartyContract
访问。如下图所示:
external
可见性为 external
的函数只能被第三方合约访问。也就是只能被 ThirdPartyContract
访问。注意变量是没有 external
修饰符的。如下图所示:
internal
可见性为 internal
的变量和函数可以被主合约和子合约访问。也就是可以被 MainContract
, ChildContract
访问。如下图所示:
private
可见性为 private
的变量和函数只能被主合约访问。也就是只能被 MainContract
访问。如下图所示:
2.4 函数状态可变性
函数状态可变性是指函数内部是否能够修改合约的状态。默认情况下,函数是可以修改合约状态的。某些情况下你可能不希望合约状态被修改,为了提高合约安全性和可读性,你可以加上状态可变性修饰符。Solidity 有三种函数状态可变性可以选择:view
, pure
, payable
。
合约函数通常需要修改区块链的状态,例如转账,修改状态变量,触发事件等等。但是有一小部分函数只是起辅助作用的,你不希望它们去修改合约的状态。这个时候你可以使用状态可变性修饰符(state mutability modifier)来修饰你的函数。这样显式地表示你不希望这个函数修改合约状态。一旦函数内部试图修改合约状态,编译器会抛出编译错误。利用好状态可变性可以提高合约安全性,可读性,还可以方便 Debug。
Solidity 提供了三个状态可变性修饰符:
view
函数只能查询合约状态,不能更改合约状态。简单来讲就是只读不写的pure
既不能查询,也不能修改函数状态。只能使用函数参数进行简单计算并返回结果payable
允许函数接受 Ether 转账。函数默认情况下是不能接受转账的,如果你需要接受转账,那么必须指定其为 payable
2.4.1 怎样才算查询合约状态
上面我们所谈到的查询合约状态是一个笼统定义。我们需要明确界定到底哪些行为会被认为是查询了合约状态。Solidity 中有5种行为被认为是查询了合约状态:
- 读取状态变量
- 访问
address(this).balance
或者<address>.balance
- 访问
block
,tx
,msg
的成员 - 调用未标记为
pure
的任何函数 - 使用包含某些操作码的内联汇编
2.4.2 怎样才算修改合约状态
同样地,我们也需要明确界定到底哪些行为会被认为是修改了合约状态。Solidity 中有8种行为被认为是修改了合约状态:
- 修改状态变量
- 触发事件
- 创建其他合约
- 使用
selfdestruct
来销毁合约 - 通过函数调用发送以太币
- 调用未标记为
view
或pure
的任何函数 - 使用低级别调用,如
transfer
,send
,call
,delegatecall
等 - 使用包含某些操作码的内联汇编
2.4.3 view 函数
view
(视图)函数只能读取合约状态,不能修改合约状态。在 view
函数体修改合约状态时,编译器会直接报错。使用 view
函数可以提高代码安全性,避免出现与预期不符副作用。如果我们的函数承诺不会修改合约状态,那么我们应该将其声明为 view
函数。如下所示:
uint count;
function GetCount() public view returns(uint) {
return count;
}
2.4.4 pure 函数
纯函数(pure function)是函数式编程的一个重要概念。在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:
- 相同的输入值时,需产生相同的输出
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
- 不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外的内容等
简单而言就是:纯函数不读不写,没有副作用。使用纯函数可以提高代码安全性,避免出现与预期不符的副作用。
**如果我们的函数承诺不需要查询,也不需要修改合约状态,那么我们应该为它加上 pure
修饰符。**如下所示:
function add(uint lhs, uint rhs) public pure returns(uint) {
return lhs + rhs;
}
2.4.5 payable 函数
函数默认是不能接受 Ether 转账的。如果我们的函数需要接受转账,那么我们应该为它加上 payable
修饰符。如下所示:
function deposit() external payable {
// deposit to current contract
}
2.4.6 到底什么是 Pure
和View
?
刚开始学习 solidity
时,pure
和 view
关键字可能令人费解,因为其他编程语言中没有类似的关键字。solidity
引入这两个关键字主要是因为 以太坊交易需要支付手续费(gas fee)。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas
。包含 pure
和 view
关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 pure
/view
函数调用 pure
/view
函数时需要付gas)。
在以太坊中,以下语句被视为修改链上状态:
- 写入状态变量。
- 释放事件。
- 创建其他合约。
- 使用
selfdestruct
. - 通过调用发送以太币。
- 调用任何未标记
view
或pure
的函数。 - 使用低级调用(low-level calls)。
- 使用包含某些操作码的内联汇编。
让我们总结一下:
pure
函数既不能读取也不能写入链上的状态变量。view
函数能读取但也不能写入状态变量。- 非
pure
或view
的函数既可以读取也可以写入状态变量。
2.4.7 状态可变性与 Gas
为了防止滥用区块链,以太坊规定,对于改变链上的状态的操作,需要支付一定价值的 gas 费。
因为改变了链上的状态,就需要将这些改变同步到区块链的全部节点,达成全网共识,保证数据一致,这个成本是非常高的。
而对于不改变链上状态的操作,只需要在接入节点上完成,无需全网进行数据同步,这个成本就很低,因此无需付费。
正是基于以上原因,调用 view
、pure
函数,无需支付 gas,而调用非 view
、pure
函数就需要支付一定的 gas。
所以,我们在设计合约的时候,为了节约成本,减少支付 gas,就应该仔细划分函数的范围,尽量使用 view
、pure
函数。
2.5 函数修饰器
修饰器可以用来改变函数的行为,它们可以在函数执行前进行预处理和验证操作。例如可以验证函数入参是否符合预期,或者验证调用者是否具有特定的权限。使用修饰器能提高代码复用和可读性。
2.6 函数输出
2.6.1 返回值:return 和 returns
Solidity 中与函数输出相关的有两个关键字:return
和returns
。它们的区别在于:
returns
:跟在函数名后面,用于声明返回的变量类型及变量名。return
:用于函数主体中,返回指定的变量。
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
return(1, true, [uint256(1),2,5]);
}
在上述代码中,我们利用 returns
关键字声明了有多个返回值的 returnMultiple()
函数,然后我们在函数主体中使用 return(1, true, [uint256(1),2,5])
确定了返回值。
这里uint256[3]
声明了一个长度为3
且类型为uint256
的数组作为返回值。因为[1,2,3]
会默认为uint8(3)
,因此[uint256(1),2,5]
中首个元素必须强转uint256
来声明该数组内的元素皆为此类型。
2.6.2 命名式返回
我们可以在 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]);
}
2.6.3 解构式赋值
Solidity 支持使用解构式赋值规则来读取函数的全部或部分返回值。
-
读取所有返回值:声明变量,然后将要赋值的变量用
,
隔开,按顺序排列。uint256 _number; bool _bool; uint256[3] memory _array; (_number, _bool, _array) = returnNamed();
-
读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。在下面的代码中,我们只读取
_bool
,而不读取返回的_number
和_array
:(, _bool2, ) = returnNamed();
三、sol合约代码示例
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
uint256 public number = 5;
constructor() payable {}
// 函数类型
// function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
// 默认function
function add() external{
number = number + 1;
}
// pure: 不能读也不能写
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number+1;
}
// view: 只能读
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}
// internal: 内部函数
function minus() internal {
number = number - 1;
}
// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}
// payable: 递钱,能给合约支付eth的函数
function minusPayable() external payable returns(uint256 balance) {
minus();
balance = address(this).balance;
}
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
return(1, true, [uint256(1),2,5]);
}
// 命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
_number = 2;
_bool = false;
_array = [uint256(3),2,1];
}
// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}
// 读取返回值,解构式赋值
function readReturn() public pure{
// 读取全部返回值
uint256 _number;
bool _bool;
bool _bool2;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
// 读取部分返回值,解构式赋值
(, _bool2, ) = returnNamed();
}
}