一、函数
1.1 函数定义
函数的定义格式:
function 函数名(参数类型 参数名, ...) 修饰符 [returns(返回类型, ...)] {
函数体
}
示例:
function sum(int a, int b) public pure returns(int) {
return a + b;
}
之前说过,函数入参和出参类型不能够使用var关键字。另外,函数可以返回多个值。
function test1(int a, int b) public pure returns(int, int) {
return (a, b);
}
function test2(int a, int b) public pure returns(int x, int y) {
x = a;
y = b;
}
如果参数没有被使用,那么可以省略参数名。但是这些参数仍然存在堆栈中,只是无法访问而已。
function test(int a, int) public pure returns(int) {
return a;
}
上面函数声明使用pure
,代表该函数不能读取或修改状态变量,也不能发送和接收以太币,只能调用其他pure
函数,因此下面注释代码编译报错。
contract SimpleContract {
uint a;
function f() public pure {}
function f(uint _a) public pure {
//a;
//a = _a;
//f();
}
function f(address payable addr) public pure {
//addr.transfer(10);
}
}
除了pure
以外,还可以使用view
来修饰函数,这时候该函数只能够读取状态变量,而不能修改状态变量,也不能发送或接收以太币,只能调用其他pure
或view
函数。
1.2 函数调用
函数调用可以分为内部调用和外部调用。内部调用就是通过函数名直接调用,外部调用是通过合约实例进行调用。
function sum(int a, int b) public pure returns(int) {
return a + b;
}
function test() public {
int result = sum(10, 20); // 内部调用
int result2 = this.sum(10, 20); // 外部调用
}
如果是内部调用,被调用函数在EVM中被解释为简单的代码跳转。而外部函数调用是通过发送消息的方式进行调用。
另外,如果函数使用payable修饰,那么调用该函数时候可以指定花费的钱和gas数量。
contract A {
function test() payable public {
// TODO
}
}
contract B {
A a;
function setA(A _a) public {
a = _a;
}
function call() public {
a.test.value(10).gas(100)();
}
}
上面调用合约a的test函数时,指定了花费的wei和gas数量。如果被调用函数所在合约本身发生抛出异常,或gas用完等情况,函数调用会抛出异常。
注意:当调用其他合约时候,就相当于将控制权交给了被调用的合约。如果不清楚被调用合约的实现细节,这时候会存在很大风险(比如重入攻击)。
1.3 函数重载
函数重载是指在一个合约里面同时存在两个或两个以上同名的函数,但函数的参数类型或数量不一样。
contract A {
function f() public {}
function f(uint a) public {}
function f(uint a, uint b) public {}
}
上面代码存在三个重载函数f。当程序调用函数f时候,会根据传入参数的类型和数量决定调用哪个函数。如果传入参数可以隐式地转换为目标函数参数类型,这时候需要明确指明传入参数的类型。
function f(uint8 a) public {
}
function f(uint16 b) public {
}
function test() public {
// f(10);
// f(uint8(10));
f(uint16(10));
}
上面被注释的两行代码会提示TypeError: No unique declaration found after argument-dependent lookup.
。这是因为10
和uint8(10)
都可以匹配两个重载函数。
1.4 函数的可见性
solidityi提供了四种不同的可见性类型,用于限定函数或状态变量的访问范围。
可见性类型 | 作用 |
---|---|
external | 允许该函数从其他合约和交易中调用,不能在当前合约直接调用,但可以通过this调用 |
public | 允许该函数在合约内部和外部调用 |
internal | 只允许在合约内部或子合约中调用 |
private | 只能够在当前合约中调用 |
1.5 getter函数
对于public修饰的状态变量,solidity编译器会自动为该变量添加对应的getter函数,getter函数名就是状态变量的名称。
contract A {
uint public data;
}
contract B {
function test() public {
A a = new A();
a.data;
a.data();
}
}
上面代码中,a.data()
是通过getter函数方法data变量。
getter函数的可见性为external
,也就是在合约内部无法直接调用,只能通过this或合约外部调用。另外getter函数默认被标记为view
,也就是只能读取状态变量。
对于数组类型的public状态变量,如果要获取整个数组的话,不能够通过getter函数获取,只能通过变量名,或者定义一个函数来返回该变量的方式来获取。
contract A {
uint[] public data;
function getArray() public view returns(uint[] memory) {
return data;
}
}
contract B {
function test() public {
A a = new A();
a.data;
a.getArray();
//a.data();
}
}
上面test函数的最后一行注释的代码会提示TypeError: Wrong argument count for function call: 0 arguments given but expect 1
。这里只能够通过getter函数获取数组中的元素,而不能获取整个数组。
1.6 函数修饰符
函数修饰器用于在执行函数之前执行其他额外的操作,最常见用法是在执行函数前检查是否满足条件,如果满足则执行目标函数。
定义修饰器的语法:
modifier 修饰器名称 {
_;
}
示例:
contract SimpleContract {
address owner;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function test() public onlyOwner {}
}
上面代码定义了一个修饰器onlyOwner
,然后在test函数声明时候指定了该修饰器。那么,当调用该函数时候,会先执行修饰器中的代码,然后再执行test函数中的代码。
一个合约中可以定义多个修饰器。同一个函数也可以使用多个修饰器,多个修饰器之间使用空格隔开。调用函数时多个修饰器会依次执行。
contract SimpleContract {
address owner;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
modifier onlyOwner2 {
require(msg.sender == owner);
_;
}
function test() public onlyOwner onlyOwner2 {}
}
与函数类似的是,修饰器也可以有参数。
modifier onlyOwner(uint a) {}
function test() public onlyOwner(10) {}
上面修饰器带一个参数,所以使用修饰器时候需要传入一个实参。
1.7 全局函数
根据官方文档,solidity自带了一些特殊的变量和函数。这些变量和函数存在于全局的命名空间里面,主要用于提供区块链相关的信息,以及常用的工具函数。
1.区块和交易属性:
函数名 | 描述 |
---|---|
blockhash(uint blockNum) returns(byte32) | 获取最新256个区块中某个区块的哈希 |
block.chainid(uint) | 获取当前区块链的ID |
block.coinbase(address payable) | 获取当前区块矿工的地址 |
block.difficulty(uint) | 获取当前区块的难度值 |
block.gaslimit(uint) | 获取当前区块的gas限额 |
block.number(uint) | 获取当前区块号 |
block.timestamp(uint) | 获取当前区块的时间戳(以秒为单位) |
gasleft() returns(uint256) | 返回剩余的gas数量 |
msg.data(bytes calldata) | 获取完整的calldata |
msg.sender(address) | 获取当前消息调用的发送者 |
msg.sig(bytes4) | 获取函数签名 |
msg.value(uint) | 获取发送消息所需要wei的数量 |
tx.gasprice(uint) | 获取交易的油价 |
tx.origin(address) | 获取交易的发起者 |
2.abi编解码函数:
函数名 | 描述 |
---|---|
abi.decode(bytes memory encodedData, …) returns(…) | 对给定数据进行abi解码,而类型由第二个参数来指定 |
abi.encode(…) returns(bytes memory) | 对指定参数进行abi编码 |
abi.encodePacked(…) returns(bytes memory) | 对指定参数进行压缩编码 |
abi.encodeWithSelector(bytes4 selector, …) returns(bytes memory) | 从第二个参数开始对参数进行编码,并以指定的函数签名作为前缀 |
abi.encodeWithSignature(string memory sig, …) returns(bytes memory) | 相当于abi.encodeWithSelector(bytes4(keccak256(bytes(sig)))) |
3.类型成员
函数名 | 描述 |
---|---|
bytes.concat(…) returns(bytes memory) | 将多个bytes组合成一个bytes |
4.错误处理函数:
函数名 | 描述 |
---|---|
assert(bool condition) | 如果条件不满足,则抛出异常,并且恢复原来状态。通常用于内部错误检查 |
require(bool condition) | 如果条件不满足,则执行revert操作。通常用于检查输入或外部组件引起的错误 |
require(bool condition, string memory message) | 同上,并提供了错误信息 |
revert() | 终止执行,并恢复原来状态 |
revert(string reason) | 同上,并提供了原因 |
需要注意的是,
assert
异常会消耗掉所有可用的gas,而require
从Metropolis版本开始不会小号任何gas。
5.数学和加解密函数:
函数名 | 描述 |
---|---|
addmod(uint x, uint y, uint k) returns(uint) | 计算(x + y) % k,其中加法以任意的精度执行,并且当加法结果超过2的256次方也不会被截断 |
mulmod(uint x, uint y, uint k) returns(uint) | 同上,只是加法换成了乘法 |
keccak(bytes memory) returns(bytes32) | 用Keccak-256算法对输入进行加密 |
sha256(bytes memory) returns(bytes32) | 用SHA-256算法对输入进行加密 |
ripemd160(bytes memory) returns(bytes20) | 用RIPEMD-160算法对输入进行加密 |
ecrecover(bytes32 hash, uint8 v, byte32 r, bytes32 s) returns(address) | 利用椭圆曲线算法恢复公钥地址,失败返回0 |
6.地址相关函数:
函数名 | 描述 |
---|---|
<address>.balance(uint256) | 获取账户地址的余额,单位为wei |
<address>.code(bytes memory) | 返回地址上的代码,可能是空 |
<address>.codehash(bytes32) | 返回地址上的codehash |
<address payable>.transfer(uint256 memory) | 向指定地址发送一定数量的wei,并且发送2300 gas作为矿工费。如果失败产生一个require异常,则执行revert操作 |
<address payable>.send(uint256 amount) returns (bool) | 向指定地址发送一定数量的wei,并且发送2300 gas作为矿工费。如果失败返回false |
<address>.call(bytes memory) returns (bool, bytes memory) | 低级调用合约函数,如果成功,则返回true|false 和data ;可以指定gas数量 |
<address>.delegatecall(bytes memory) returns (bool, bytes memory) | 同上 |
<address>.staticcall(bytes memory) returns (bool, bytes memory) | 同上 |
7.合约相关函数:
函数名 | 描述 |
---|---|
this(contract) | 将当前合约转换成地址类型 |
selfdestruct(address payable recipient) | 销毁合约,并将余额发送到指定地址上 |
1.8 Fallback函数
每个合约都隐式包含一个未命名、没有参数和返回值的函数,成为fallback
函数。如果调用合约函数不存在,那么会自动执行fallback
函数。除此以外,如果合约接收到以太币,fallback
函数也会自动执行,所以fallback
函数必须使用payable
修饰。
contract SimpleContract {
uint a;
function() external payable {
a = 100;
}
}
如果想让合约接收以太币,那么就必须提供fallback
函数,否则会报错并返还以太币。
二、合约
2.1 创建合约实例
创建合约的语法:
new 合约名();
示例:
contract A {}
contract B {
function test() public {
A a = new A();
}
}
创建合约实例的时候,默认会调用构造函数,并执行构造函数中的代码。如果没有定义构造函数,EVM编译器会自动生成一个默认的构造函数。如果构造函数指定了参数,那么创建合约实例时候需要传入相同数量的实参。
contract A {
int num;
constructor(int _num) public {
num = _num;
}
}
contract B {
function test() public {
A a = new A(10);
}
}
注意:合约构造函数是可选的。solidity不支持构造函数的重载,也就是一个合约最多只有一个构造函数。
2.2 合约的继承
一个合约可以继承另外一个合约,也可以同时继承多个合约,比如说:
contract A {}
contract B is A {}
如果同时继承了多个父合约,最远的派生函数会被调用。
contract C is A, B {}
contract SimpleContract {
function test() public returns(uint) {
C c = new C();
c.a(); // 20
c.f(); // 20
}
}
上面合约C同时继承了A和B,因为B合约比A合约距离C合约远,所以在test
函数中调用父合约状态变量和函数,实际调用的是B合约。
父合约的函数可以被派生合约中具有相同名称、相同参数的函数重载。如果派生合约的重载函数的返回值类型或数量不相同,会导致错误。
如果父合约构造函数带参数,那么可以在派生合约声明时候指定参数,或者也可以在构造函数位置以修饰器方式提供。
contract A {
int num;
constructor(int _num) public {
num = _num;
}
}
contract B is A {}
contract B is A(10) {}
contract B is A {
constructor() public A(10) {}
}
2.3 抽象合约和接口
如果一个派生合约没有指定所有父合约的构造函数参数,那么该合约是一个抽象合约。
contract A {
uint a;
constructor(uint _a) internal {
a = _a;
}
}
contract B is A {}
contract SimpleContract {
function test() public {
//A a = new A();
}
}
因为合约B继承合约A时候,没有指定构造函数参数,因此合约B是一个抽象合约。抽象合约不能够实例化,因此上面test
函数中注释代码会报错。
另外,如果一个合约包含未实现的函数,那么该合约也是一个抽象合约。
contract A {
function test() public;
}
上面test
函数没有提供实现,因此合约A也是一个抽象合约。
2.4 接口
接口主要用于规范合约的实现,其定义格式为:
interface 接口名 {}
定义接口使用interface
关键字。接口里面只能够声明函数,不能定义状态变量和构造函数,也不能够对函数提供实现。
interface InterfaceA {
function f() external;
}
上面接口函数f()
必须要使用external
修饰符。与Java不同,solidity接口之间无法继承。
一个合约可以继承多个接口。
interface InterfaceA {}
interface InterfaceB {}
interface Impl is InterfaceA, InterfaceB {}
如果一个合约没有实现接口里面的所有函数,那么该合约是一个抽象合约。