Solidity智能合约开发(提高篇)

一、函数

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来修饰函数,这时候该函数只能够读取状态变量,而不能修改状态变量,也不能发送或接收以太币,只能调用其他pureview函数。

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.。这是因为10uint8(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|falsedata;可以指定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合约。

父合约的函数可以被派生合约中具有相同名称、相同参数的函数重载。如果派生合约的重载函数的返回值类型或数量不相同,会导致错误。
Solidity智能合约开发(提高篇)
如果父合约构造函数带参数,那么可以在派生合约声明时候指定参数,或者也可以在构造函数位置以修饰器方式提供。

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接口之间无法继承。
Solidity智能合约开发(提高篇)
一个合约可以继承多个接口。

interface InterfaceA {}
interface InterfaceB {}
interface Impl is InterfaceA, InterfaceB {}

如果一个合约没有实现接口里面的所有函数,那么该合约是一个抽象合约。

上一篇:AUTOSAR-PreBuild Data Set Contract Phase


下一篇:大数据应用与管理2:逻辑回归