区块链学习第八期

myBlog

solidity小细节【上篇】

[toc] ---

1:internal、external、public、private 可见性?

没标明函数类型的,默认 internal。

  • public: 内部外部均可见。(也可用于修饰状态变量,public 变量会自动生成 getter 函数,用于查询数值).
  • private: 只能从本合约内部访问,继承的合约也不能用(也可用于修饰状态变量)。
  • external: 只能从合约外部访问(但是可以用 this.f()来调用,f 是函数名)
  • internal: 只能从合约内部访问,继承的合约可以用(也可用于修饰状态变量)

2:解构式赋值

solidity 使用解构式赋值的规则,支持读取函数的全部或部分返回值。

  • 读取所有返回值:声明变量,并且将要赋值的变量用,隔开,按顺序排列。
1
2
3
4
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
  • 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。下面这段代码中,我们只读取_bool,而不读取返回的_number 和_array:
1
(, _bool2, ) = returnNamed();

3:数据位置 data

solidity 数据存储位置有三类:storage,memory 和 calldata。不同存储位置的 gas 成本不同。storage 类型的数据存在链上,类似计算机的硬盘,消耗 gas 多;memory 和 calldata 类型的临时存在内存里,消耗 gas 少。大致用法:

  1. storage:合约里的状态变量默认都是 storage,存储在链上。

  2. memory:函数里的参数和临时变量一般用 memory,存储在内存中,不上链。

  3. calldata:和 memory 类似,存储在内存中,不上链。与 memory 的不同点在于 calldata 变量不能修改(immutable),一般用于函数的参数。例子:

1
2
3
4
5
6
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}

赋值规则(这个一定要注意)

在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(**修改新变量会影响原变量**)

  1. storage(合约的状态变量)赋值给本地 storage(函数里的)时候,会创建引用,改变新变量会影响原变量。
1
2
3
4
5
6
7
8
9
uint[] x = [1,2,3]; // 状态变量:数组 x

function fStorage() public{
//声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}


  1. storage 赋值给 memory,会创建独立的复本,修改其中一个不会影响另一个;反之亦然
1
2
3
4
5
6
7
8
9
10
11
uint[] x = [1,2,3]; // 状态变量:数组 x

function fMemory() public view{
//声明一个Memory的变量xMemory,复制x。修改xMemory不会影响x
uint[] memory xMemory = x;
xMemory[0] = 100;
xMemory[1] = 200;
uint[] memory xMemory2 = x;
xMemory2[0] = 300;
}


4:变量的作用域

Solidity 中变量按作用域划分有三种,分别是状态变量(state variable),局部变量(local variable)和全局变量(global variable

  1. 状态变量

    状态变量是数据存储在链上的变量,所有合约内函数都可以访问 ,gas 消耗高。状态变量在合约内、函数外声明

    1
    2
    3
    4
    5
    contract Variables {
    uint public x = 1;
    uint public y;
    string public z;
    }
  2. 局部变量

    局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,gas 低。局部变量在函数内声明

    1
    2
    3
    4
    5
    6
    function bar() external pure returns(uint){
    uint xx = 1;
    uint yy = 3;
    uint zz = xx + yy;
    return(zz);
    }
  3. 全局变量

    全局变量是全局范围工作的变量,都是 solidity 预留关键字。他们可以在函数内不声明直接使用

    1
    2
    3
    4
    5
    6
    7
    function global() external view returns(address, uint, bytes memory){
    address sender = msg.sender;
    uint blockNum = block.number;
    bytes memory data = msg.data;
    return(sender, blockNum, data);
    }


5:delete 操作符

delete a 会让变量 a 的值变为初始值

1
2
3
4
5
// delete操作符
bool public _bool2 = true;
function d() external {
delete _bool2; // delete 会让_bool2变为默认值,false
}

6:常数 constant、immutable

只有数值变量可以声明 constant 和 immutable;string 和 bytes 可以声明为 constant,但不能为 immutable

  • constant 变量必须在声明的时候初始化,之后再也不能改变
  • immutable 变量可以在声明时或构造函数中初始化

7:重写、多重继承

重写

virtual: 父合约中的函数,如果希望子合约重写,需要加上 virtual 关键字。
override:子合约重写了父合约中的函数,需要加上 override 关键字。

多重继承

继承时要按辈分最高到最低的顺序排。比如我们写一个 Erzi 合约,继承 Yeye 合约和 Baba 合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成 contract Erzi is Baba, Yeye,不然就会报错。 如果某一个函数在多个继承的合约里都存在,比如例子中的 hip()和 pop(),在子合约里必须重写,不然会报错。 重写在多个父合约中都重名的函数时,override 关键字后面要加上所有父合约名字,例如 override(Yeye, Baba)。

1
2
3
4
5
6
7
8
9
10
contract Erzi is Yeye, Baba{
// 继承两个function: hip()和pop(),输出值为Erzi。
function hip() public virtual override(Yeye, Baba){
emit Log("Erzi");
}

function pop() public virtual override(Yeye, Baba) {
emit Log("Erzi");
}

修饰器的重写

Solidity 中的修饰器(Modifier)同样可以继承,用法与函数继承类似,在相应的地方加 virtual 和 override 关键字即可

1
2
3
4
5
6
contract Base1 {
modifier exactDividedBy2And3(uint _a) virtual {
require(_a % 2 == 0 && _a % 3 == 0);
_;
}
}
1
2
3
4
modifier exactDividedBy2And3(uint _a) override {
_;
require(_a % 2 == 0 && _a % 3 == 0);
}

构造函数的继承

1
2
3
contract C is A {
constructor(uint _c) A(_c * _c) {}
}

8:抽象合约、接口区别

抽象合约

如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为 abstract,不然编译会报错;另外,未实现的函数需要加 virtual,以便子合约重写

接口

接口类似于抽象合约,但它不实现任何功能。接口的规则:不能包含状态变量

  • 不能包含构造函数
  • 不能继承除接口外的其他合约
  • 所有函数都必须是 external 且不能有函数体
  • 继承接口的合约必须实现接口定义的所有功能