温馨提示:本文共31868个字,读完预计80分钟。
文章目录
一、数据类型
1.1、值类型
1.1.1、布尔
pragma solidity ^0.4.25;
contract TestBool {
bool flag;
int num1 = 100;
int num2 = 200;
// default false
function getFlag() public view returns(bool) {
return flag; // false
}
// 非
function getFlag2() public view returns(bool) {
return !flag; // true
}
// 与
function getFlagAnd() public view returns(bool) {
return (num1 != num2) && !flag; // true
}
// 或
function getFlagOr() public view returns(bool) {
return (num1 == num2) || !flag; // true
}
}
加减乘除、取余、幂运算,
pragma solidity ^0.4.25;
// 整型特性与运算
contract TestInteger {
int num1; // 有符号整型 int256
uint num2; // 无符号整型 uint256
function add(uint _a, uint _b) public pure returns(uint) {
return _a + _b;
}
function sub(uint _a, uint _b) public pure returns(uint) {
return _a - _b;
}
function mul(uint _a, uint _b) public pure returns(uint) {
return _a * _b;
}
function div(uint _a, uint _b) public pure returns(uint) {
return _a / _b; // 在solidity中,除法是做整除,没有小数点
}
function rem(uint _a, uint _b) public pure returns(uint) {
return _a % _b;
}
function square(uint _a, uint _b) public pure returns(uint) {
return _a ** _b; // 幂运算在0.8.0之后,变为右优先,即a ** b ** c => a ** (b ** c)
}
function max() public view returns(uint) {
return uint(-1);
// return type(uint).max; // 0.8不再允许uint(-1)
}
}
位运算,
pragma solidity ^0.4.25;
// 位运算
contract TestBitwise {
uint8 num1 = 3;
uint8 num2 = 4;
function bitAdd() public view returns(uint) {
return num1 & num2;
}
function bitOr() public view returns(uint) {
return num1 | num2;
}
function unBit() public view returns(uint) {
return ~num1;
}
function bitXor() public view returns(uint) {
return num1 ^ num2;
}
function bitRight() public view returns(uint) {
return num1 >> 1;
}
function bitLeft() public view returns(uint) {
return num1 << 1;
}
}
1.1.3、定长浮点型
目前只支持定义,不支持赋值使用,
fixed num; // 有符号
ufixed num; // 无符号
1.1.4、地址类型
address addr = msg.sender;
address addr = 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF;
1.1.5、合约类型
在合约TestType中使用TestBitwise合约,
TestBitwise t = TestBitwise(addr);
1.1.6、枚举类型
enum ActionChoices { Up, Down, Left, Right }
1.1.7、定长字节数组
pragma solidity ^0.4.25;
// 固定长度的字节数组(静态),以及转换为string类型
contract TestBytesFixed {
// public 自动生成同名的get方法
bytes1 public num1 = 0x7a; // 1 byte = 8 bit
bytes1 public num2 = 0x68;
bytes2 public num3 = 0x128b; // 2 byte = 16 bit
// 获取字节数组长度
function getLength() public view returns(uint) {
return num3.length; // 2
}
// 字节数组比较
function compare() public view returns(bool) {
return num1 < num2;
}
// 字节数组位运算
function bitwise() public view returns(bytes1,bytes1,bytes1) { // 多返回值
return ((num1 & num2), (num1 | num2), (~num1));
}
// 先转为bytes动态数组,再通过string构造 0x7a7a -> zz
function toString(bytes2 _val) public pure returns(string) {
bytes memory buf = new bytes(_val.length);
for (uint i = 0; i < _val.length; i++) {
buf[i] = _val[i];
}
return string(buf);
}
}
pragma solidity ^0.4.25;
// 固定长度的字节数组(静态)扩容和压缩
contract TestBytesExpander {
// public 自动生成同名的get方法
bytes6 name = 0x796f7269636b;
function changeTo1() public view returns(bytes1) {
return bytes1(name); // 0x79
}
function changeTo2() public view returns(bytes2) {
return bytes2(name); // 0x796f
}
function changeTo16() public view returns(bytes16) {
return bytes16(name); // 0x796f7269636b00000000000000000000
}
}
function (<parameter types>) {internal|external|public|private} [pure|constant|view|payable] [returns ()]
1.2、引用类型
1.2.1、字符串
pragma solidity ^0.4.25;
// 修改string类型的数据
contract TestString {
string name = 'yorick'; // 字符串可以使用单引号或者双引号
string name2 = "!@#$%^&"; // 特殊字符占1个byte
string name3 = "张三"; // 中文在string中使用utf8的编码方式存储,占用3个byte
function getLength() view public returns(uint) {
// 不可以直接获取string的length
return bytes(name).length; // 6
}
function getLength2() view public returns(uint) {
return bytes(name2).length; // 7
}
function getLength3() view public returns(uint) {
return bytes(name3).length; // 6
}
function getElmName() view public returns(bytes1) {
// 不可以直接通过数组下标name[0]获取
return bytes(name)[0]; // 0x79
}
function changeElmName() public {
bytes(name)[0] = "h";
}
function getName() view public returns(bytes) {
return bytes(name);
}
}
1.2.2、变长字节数组
pragma solidity ^0.4.25;
// 动态的字节数组,以及转换为string类型
contract TestBytesDynamic {
bytes public dynamicBytes;
function setDynamicBytes(string memory val) public {
dynamicBytes = bytes(val);
}
function getVal() public view returns(string){
return string(dynamicBytes);
}
}
定长数组,
pragma solidity ^0.4.25;
// 定长数组
contract TestArrFixed {
uint[5] arr = [1,2,3,4,5];
// 修改数组元素内容
function modifyElements() public {
arr[0] = 12;
arr[1] = 14;
}
// 查看数组
function watchArr() public view returns(uint[5]) {
return arr;
}
// 数组元素求和计算
function sumArr() public view returns(uint) {
uint sum = 0;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
// 数组长度
function getLength() public view returns(uint) {
return arr.length;
}
// delete重置数组某下标的元素值,不会真正删除该元素
function deleteElm(uint idx) public {
delete arr[idx];
}
// delete重置整个数组
function deleteArr() public {
delete arr;
}
/** 定长数组不允许改变长度和push
// 压缩数组后,右侧多余元素被丢弃
function changeLengthTo1() public {
arr.length = 1;
}
// 扩容数组后,右侧元素补0
function changeLengthTo10() public {
arr.length = 10;
}
// 追加新元素
function pushElm(uint _elm) public {
arr.push(_elm);
}
*/
}
变长数组,
pragma solidity ^0.4.25;
// 变长数组
contract TestArrDynamic {
uint[] arr = [1,2,3,4,5];
// 查看数组
function watchArr() public view returns(uint[]) {
return arr;
}
// 数组长度
function getLength() public view returns(uint) {
return arr.length;
}
// 压缩数组后,右侧多余元素被丢弃
function changeLengthTo1() public {
arr.length = 1;
}
// 扩容数组后,右侧元素补0
function changeLengthTo10() public {
arr.length = 10;
}
// 追加新元素
function pushElm(uint _elm) public {
arr.push(_elm);
}
// delete重置数组某下标的元素值,不会真正删除该元素
function deleteElm(uint idx) public {
delete arr[idx];
}
// delete重置整个数组
function deleteArr() public {
delete arr;
}
}
二维数组,
pragma solidity ^0.4.25;
/**
二维数组
solidity的二维数组与其他语言不同,[2] [3]表示3行2列,而其他语言为2行3列;
二维动态数组与一维数组类似,可以改变其数组长度;
*/
contract TestArr2Dimensional {
uint[2][3] arr = [[1,2],[3,4],[5,6]];
function getRowSize() public view returns(uint) {
return arr.length; // 3
}
function getColSize() public view returns(uint) {
return arr[0].length; // 2
}
function watchArr() public view returns(uint[2][3]) {
return arr; // 1,2,3,4,5,6
}
function sumArr() public view returns(uint) {
uint sum = 0;
for (uint i = 0; i < getRowSize(); i++) {
for (uint j = 0; j < getColSize(); j++) {
sum += arr[i][j];
}
}
return sum;
}
function modifyArr() public {
arr[0][0] = 99;
}
}
数组字面值,
pragma solidity ^0.4.25;
// 数组字面值
contract TestArrLiteral {
// 最小存储匹配,未超过255,所以使用uint8存储
function getLiteral8() pure public returns(uint8[3]) {
return [1,2,3];
}
// 超过255,所以使用uint16存储
function getLiteral16() pure public returns(uint16[3]) {
return [256,2,3]; // [255,2,3] 不被uint16允许
}
// 强制转换为uint256
function getLiteral256() pure public returns(uint[3]) {
return [uint(1),2,3]; // 给任意元素强转即可,否则不被允许
}
// 计算外界传入的内容
function addLiterals(uint[3] arr) pure external returns(uint) {
uint sum = 0;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
}
1.2.4、结构体
pragma solidity ^0.4.25;
// 结构体初始化的两种方法
contract TestStruct {
struct Student {
uint id;
string name;
mapping(uint=>string) map;
}
// 默认为storage类型,只能通过storage类型操作结构体中的mapping类型数据
Student storageStu;
// mapping类型可以不用在定义的时候赋值,而其他类型不赋值则会报错
function init() public pure returns(uint, string) {
Student memory stu = Student(100, "Jay");
return (stu.id, stu.name);
}
function init2() public pure returns(uint, string) {
Student memory stu = Student({name: "Jay", id: 100});
return (stu.id, stu.name);
}
function init3() public returns(uint, string, string) {
Student memory stu = Student({name: "Jay", id: 100});
// 直接操作结构体中的mapping不被允许: Student memory out of storage
// stu.map[1] = "artist";
// 通过storage类型的变量操作结构体中的mapping
storageStu = stu;
storageStu.map[1] = "artist";
return (storageStu.id, storageStu.name, storageStu.map[1]);
}
// 结构体作为入参时,为memory类型,并且不能使用public或external修饰函数:Internal or recursive type is not allowed for public or external functions.
// 赋值时也要指定为memory,否则报错
function testIn(Student stu) internal returns(uint) {
return stu.id;
}
// 结构体作为出参,同样只能private或internal声明内部使用
function testOut(Student stu) private returns(Student) {
return stu;
}
}
contract TestMapping {
mapping(address => uint) private scores; // <学生,分数>的单层映射
mapping(address => mapping(bytes32 => uint8)) private _scores; // <学生,<科目,分数>>的两层映射
function getScore() public view returns(address, uint) {
address addr = msg.sender;
return (addr, scores[addr]);
}
function setScore() public {
scores[msg.sender] = 100;
}
}
二、作用域(访问修饰符)
contract TestAccessCtrl {
constructor () public {}
uint public num1 = 1; // 自动为public生成同名的get函数,但在编码时不可直接调用num1()
uint private num2 = 2;
uint num3 = 3; // 不写则默认private
function funcPublic() public returns(string) {
return "public func";
}
function funcPrivate() private returns(string) {
return "private func";
}
function funcInternal() internal returns(string) {
return "internal func";
}
function funcExternal() external returns(string) {
return "external func";
}
function test1(uint choice) public returns(string) {
if (choice == 1) return funcPublic();
if (choice == 2) return funcPrivate();
if (choice == 3) return funcInternal();
//if (choice == 4) return funcExternal(); // external不允许直接在内部用
if (choice == 4) return this.funcExternal(); // 间接通过this才可以调用external
}
}
contract TestAccessCtrlSon is TestAccessCtrl {
function test2(uint choice) public returns(string) {
if (choice == 1) return funcPublic(); // public允许派生合约使用
//if (choice == 2) return funcPrivate(); // private不允许派生合约使用
if (choice == 3) return funcInternal(); // internal允许派生合约使用
//if (choice == 4) return funcExternal(); // external也不允许派生合约直接使用
}
}
contract TestAccessCtrl2 {
function test2(uint choice) public returns(string) {
TestAccessCtrl obj = new TestAccessCtrl();
return obj.funcExternal(); // external只允许在外部合约中这样间接调用
}
}
2.1、private
仅在当前合约使用,且不可被继承,私有状态变量只能从当前合约内部访问,派生合约内不能访问。
2.2、public
同时支持内部和外部调用。修饰状态变量时,自动生成同名get函数,
但在编码时不可直接调用num1()
。
2.3、internal
只支持内部调用,也包括其派生合约内访问。
2.4、external
外部才可调用,内部想要调用可以用this
。
三、函数修饰符
contract TestFuncDecorator {
uint public num = 1;
/// pure
function testPure(uint _num) public pure {
//uint num1 = num; // pure不允许读状态变量
//num = _num; // pure不允许修改状态变量
}
/// view
function testView(uint _num) public view {
uint num1 = num; // 允许读状态变量
num = _num; // 0.4语法上允许修改状态变量,但实际不会修改,所以num还是1
// 0.5及之后不允许在view中这样修改,否则编译不通过
}
/// payable
function () public payable {}
function getBalance() public view returns(uint) { // balance获取合约地址下的以太币余额
return address(this).balance;
}
// 充值函数payable,只有添加这个关键字,才能在执行这个函数时,给这个合约充以太币,否则该函数自动拒绝所有发送给它的以太币
function testPayable() payable public { // transfer转账
address(this).transfer(msg.value);
}
}
3.1、pure
表明该函数是纯函数,连状态变量都不用读,函数的运行仅仅依赖于参数。不消耗gas。承诺不读取或修改状态,否则编译出错。
3.2、view
设置了view修饰符,就是一次调用,不需要执行共识、进入EVM,而是直接查询本地节点数据,因此性能会得到很大提升。不消耗gas。不会发起交易,所以不能实际改变状态变量。
3.3、payable
允许函数被调用的时候,让合约接收以太币。如果未指定,该函数将自动拒绝所有发送给它的以太币。
四、构造函数
唯一,不可重载。也可以接收入参。
pragma solidity ^0.4.25;
contract Test1 {
address private _owner;
constructor() public {
_owner = msg.sender;
}
/**constructor(int num) public { 重载构造->编译错误
_owner = msg.sender;
}*/
}
contract Test2 {
uint public num;
constructor(uint x) public { // 带参构造,在deploy时传入
num = x;
}
}
五、修饰器modifier
方法修饰器modifier
,类似AOP处理。
pragma solidity ^0.4.25;
contract TestModifier {
address private _owner;
bool public endFlag; // 执行完test后的endFlag仍是true
constructor() public {
_owner = msg.sender;
}
modifier onlyOwner { // 权限拦截器,非合约部署账号执行test()则被拦截
require(_owner == msg.sender, "Auth: only owner is authorized.");
_; // 类似被代理的test()方法调用
endFlag = true;
}
function test() public onlyOwner {
endFlag = false;
}
}
六、数据位置
6.1、memory
其生命周期只存在于函数调用期间,局部变量默认存储在内存,不能用于外部调用。内存位置是临时数据,比存储位置便宜。它只能在函数中访问。
通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。
6.2、storage
状态变量保存的位置,只要合约存在就一直保存在区块链中。该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。
保存在存储区(Storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。
6.3、calldata
calldata是不可修改的只读的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,calldata是外部函数的参数(而不是返回参数)的默认位置。
0.4的external入参声明calldata则报错,0.5及之后的external入参必须声明calldata否则报错。
从 Solidity 0.6.9 版本开始,之前仅用于外部函数的calldata位置,现在可以在内部函数使用了。
6.4、stack
堆栈是由EVM (Ethereum虚拟机)维护的非持久性数据。EVM使用堆栈数据位置在执行期间加载变量。堆栈位置最多有1024个级别的限制。
【总结】
花费gas:storage > memory(calldata) > stack
状态变量总是存储在存储区storage中。(隐式地标记状态变量的位置)
函数参数(值类型)包括返回参数(值类型)都存储在内存memory中
值类型的局部变量:栈(stack)
值类型的局部变量存储在内存中。但是,对于引用类型,需要显式地指定数据位置
不能显式声明具有值类型的局部变量为memory还是storage
七、事件event
定义使用event
,类似一个函数声明,调用试图emit
,
contract Test {
event testEvent(uint a, uint b, uint c, uint result);
function calc(uint a, uint b, uint c) public returns(uint) {
uint result = a ** b ** c;
emit testEvent(a, b, c, result);
return result;
}
}
事件会输出在logs
中,
[
{
"from": "0x19a0870a66B305BE9917c0F14811C970De18E6fC",
"topic": "0x271cb5fa8dca917938dbd3f2522ef54cf70092ead9e1871225a2b3b407f9a81a",
"event": "testEvent",
"args": {
"0": "2",
"1": "1",
"2": "3",
"3": "8",
"a": "2",
"b": "1",
"c": "3",
"result": "8"
}
}
]
通过给event形参添加indexed
,便于事件条件的筛选,indexed
不能超过三个,否则编译出错,
event testEvent(uint indexed a, uint b, uint c, uint result);
八、单位和全局变量
时间单位不加默认s
,以太币默认wei
,
function testUnit() pure public {
require(1 == 1 seconds);
require(1 minutes == 60 seconds);
require(1 hours == 60 minutes);
require(1 days == 24 hours);
require(1 weeks == 7 days);
require(1 years == 365 days); // years 从 0.5.0 版本开始不再支持
require(1 ether == 1000 finney);
require(1 finney == 1000 szabo); // 从0.7.0开始 finney 和 szabo 被移除了
require(1 szabo == 1e12 wei);
//require(1 gwei == 1e9); // 0.7.0开始加入gwei
}
全局变量,
pragma solidity ^0.8.0;
contract TestGlobalVariable {
function test1() public view returns(/**bytes32,*/ uint, uint, address, uint, uint, uint, uint) {
return (
// blockhash(block.number - 1), // 指定区块的区块哈希,仅可用于最新的 256 个区块且不包括当前区块,否则返回0。0.5移除了block.blockhash
block.basefee, // 当前区块的基础费用
block.chainid, // 当前链 id
block.coinbase, // 挖出当前区块的矿工地址
block.difficulty, // 当前区块难度
block.gaslimit, // 当前区块 gas 限额
block.number, // 当前区块号
block.timestamp // 自 unix epoch 起始当前区块以秒计的时间戳,0.7.0移除了now
);
}
function test2() public payable returns(bytes memory, address, bytes4, uint, uint256, uint, address) {
return (
msg.data, // 完整的 calldata
msg.sender, // 消息发送者(当前调用)
msg.sig, // calldata 的前 4 字节(也就是函数标识符)
msg.value, // 随消息发送的 wei 的数量
gasleft(), // 剩余的 gas,0.5移除了msg.gas
tx.gasprice, // 交易的 gas 价格
tx.origin // 交易发起者(完全的调用链)
);
}
}
九、异常处理
9.1、assert
assert(1 == 1 seconds);
9.2、require
require(1 == 1 seconds);
require(1 == 1 seconds, "err");
9.3、revert
if (x != y) revert("x should equal to b");
9.4、try/catch
Solidity0.6版本之后加入。try/catch仅适用于外部调用,另外,try大括号内的代码是不能被catch捕获的。
pragma solidity ^0.6.10;
contract TestTryCatch {
function execute (uint256 amount) external returns(bool){
try this.onlyEvent(amount){
return true;
} catch {
return false;
}
}
function onlyEvent (uint256 a) public {
//code that can revert
require(a % 2 == 0, "Ups! Reverting");
}
}
【assert和require的选择】
在EVM里,处理assert和require两种异常的方式是不一样的,虽然他们都会回退状态,不同点表现在:
- gas消耗不同。assert类型的异常会消耗掉所有剩余的gas,而require不会消耗掉剩余的gas(剩余的gas会返还给调用者)
- 操作符不同。当assert发生异常时,Solidity会执行一个无效操作(无效指令0xfe)。当发生require类型的异常时,Solidity会执行一个回退操作(REVERT指令0xfd)
- 优先使用require()
- 用于检查用户输入。
- 用于检查合约调用返回值,如require(external.send(amount))。
- 用于检查状态,如msg.send == owner。
- 通常用于函数开头。
- 不知道使用哪一个的时候,就使用require。
- 优先使用assert()
- 用于检查溢出错误,如z = x + y; assert(z >= x);
- 用于检查不应该发生的异常情况。
- 用于在状态改变之后,检查合约状态。
- 尽量少使用assert。
- 通常用于函数中间或者尾部。
十、重载
// 重载
function addNums(uint x, uint y) public pure returns(uint) {
return x + y;
}
function addNums(uint x, uint y, uint z) public pure returns(uint) {
return x + y + z;
}
十一、继承
子合约is
父合约的格式,
pragma solidity ^0.4.25;
contract TestExtendA { // 父类要写在子类之前
uint public a;
constructor() public {
a = 1;
}
}
contract TestExtend is TestExtendA {
uint public b;
constructor() public {
b = 2;
}
}
直接在继承列表中指定基类的构造参数,
contract A { // 父类要写在子类之前
uint public x;
constructor(uint _a) public { // 带参构造
x = _a;
}
}
contract B is A(1) { // 指定父类构造参数
uint public y;
constructor() public {
y = 2;
}
}
通过派生合约(子类)的构造函数中使用修饰符方式调用基类合约,
contract A { // 父类要写在子类之前
uint public x;
constructor(uint _a) public { // 带参构造
x = _a;
}
}
// 方式一:
contract B1 is A {
uint public b;
constructor() A(1) public { // 子类构造使用父类带参修饰符A(1)
b = 2;
}
}
// 方式二:
contract B2 is A {
uint public b;
constructor(uint _b) A(_b / 2) public { // 子类带参构造使用父类带参修饰符A(_b / 2)
b = _b;
}
}
连续继承、多重继承,
/// 连续继承,Z继承Y,Y又继承X
contract X {
uint public x;
constructor() public{
x = 1;
}
}
contract Y is X {
uint public y;
constructor() public{
y = 1;
}
}
// 如果是多个基类合约之间也有继承关系,那么is后面的合约书写顺序就很重要。顺序应该是,基类合约在前面,派生合约在后面,否则无法编译。
// 实际上Z只需要继承Y就行
contract Z is X,Y { // 所以必须是X,Y而不是Y,X
}
/// 多重继承,子类可以拥有多个基类的属性
contract Father {
uint public x = 180;
}
contract Mother {
uint public y = 170;
}
contract Son is Father, Mother {
}
十二、抽象合约
0.6开始支持。
如果一个合约有构造函数,且是内部(internal)函数,或者合约包含没有实现的函数,这个合约将被标记为抽象合约,使用关键字abstract,抽象合约无法成功部署,他们通常用作基类合约。
抽象合约可以声明一个virtual
纯虚函数,纯虚函数没有具体实现代码的函数。其函数声明用;结尾,而不是用{}
结尾。
如果合约继承自抽象合约,并且没有通过重写(override)来实现所有未实现的函数,那么他本身就是抽象合约的,隐含了一个抽象合约的设计思路,即要求任何继承都必须实现其方法。
//pragma solidity ^0.4.25; // 0.4/0.5不兼容
pragma solidity ^0.6.10;
abstract contract TestAbstractContract {
uint public a;
constructor(uint _a) internal {
a = _a;
}
function get () virtual public;
}
十三、重写
0.8以下不支持。
合约中的虚函数(函数使用了virtual
修饰的函数)可以在子合约重写该函数,以更改他们在父合约中的行为。重写的函数需要使用关键字override
修饰。
pragma solidity ^0.8.0;
contract TestOverride {
function get() virtual public{}
}
contract Middle is TestOverride {
}
contract Inherited is Middle{
function get() public override{
}
}
对于多重继承,如果有多个父合约有相同定义的函数,override
关键字后必须指定所有的父合约名称,
contract Base1 {
function get () virtual public{}
}
contract Base2 {
function get () virtual public{}
}
contract Middle2 is Base1, Base2{ // 指定所有父合约名称
function get() public override( Base1, Base2){
}
}
注意:如果函数没有标记为virtual(除接口外,因为接口里面所有的函数会自动标记为virtual),那么派生合约是不能重写来更改函数行为的。另外,private的函数是不可标记为virtual的。
十四、接口
0.8以下不支持。
接口和抽象合约类似,与之不同的是,接口不实现任何函数,同时还有以下限制:
-
无法继承其他合约或者接口
-
无法定义构造函数
-
无法定义变量
-
无法定义结构体
-
无法定义枚举
pragma solidity ^0.8.0;
interface TestInterface {
function transfer (address recipient, uint amount) external;
}
contract TestInterfaceSon {
function transfer(address recipient, uint amount) public {
}
}
就像继承其他合约一样,合约可以继承接口,接口中的函数会隐式地标记为virtual,意味着他们会被重写。
十五、库
开发合约的时候,总是会有一些函数经常被多个合约调用,这个时候可以把这些函数封装为一个库,库的关键字用library
来定义。
如果合约引用的库函数都是内部(internal)函数,那么编译器在编译合约时,会把库函数的代码嵌入到合约里,就像合约自己实现了这些函数,这时并不会单独部署。
pragma solidity >=0.4.0 <0.7.0;
//pragma solidity ^0.4.25;
library TestLibrary{
function add (uint a,uint b) internal pure returns (uint){
uint c = a + b;
require(c > a, "SafeMath: addition overflow");
return c;
}
}
库的调用,
contract Test {
function add (uint x, uint y) public pure returns(uint){
return TestLibrary.add(x, y); // 调用库
}
}
除了使用上面的TestLibrary.add(x, y)
这种方式来调用库函数,还有一个是使用using LibA for B
这种附着库的方式。
它表示把所有LibA的库函数关联到数据类型B,这样就可以在B类型直接调用库函数。
contract Test {
using TestLibrary for uint;
//using TestLibrary for *;
function add2 (uint x,uint y) public pure returns (uint){
return x.add(y); // uint的数据x就可以直接调用add(y)
}
}
0x1、示例代码
在https://remix.ethereum.org/下,基于不同版本语法的差异,分别用三个合约文件基本覆盖到了以上语法,
- Test04.sol => ^0.4.25
- Test06.sol => ^0.6.10
- Test08.sol => ^0.8.0
- Test04.sol,
pragma solidity ^0.4.25;
//pragma solidity ^0.8.0;
/** 1.1.1 */
contract TestBool {
bool flag;
int num1 = 100;
int num2 = 200;
// default false
function getFlag() public view returns(bool) {
return flag; // false
}
// 非
function getFlag2() public view returns(bool) {
return !flag; // true
}
// 与
function getFlagAnd() public view returns(bool) {
return (num1 != num2) && !flag; // true
}
// 或
function getFlagOr() public view returns(bool) {
return (num1 == num2) || !flag; // true
}
}
/** 1.1.2 */
// 整型特性与运算
contract TestInteger {
int num1; // 有符号整型 int256
uint num2; // 无符号整型 uint256
function add(uint _a, uint _b) public pure returns(uint) {
return _a + _b;
}
function sub(uint _a, uint _b) public pure returns(uint) {
return _a - _b;
}
function mul(uint _a, uint _b) public pure returns(uint) {
return _a * _b;
}
function div(uint _a, uint _b) public pure returns(uint) {
return _a / _b; // 在solidity中,除法是做整除,没有小数点
}
function rem(uint _a, uint _b) public pure returns(uint) {
return _a % _b;
}
function square(uint _a, uint _b) public pure returns(uint) {
return _a ** _b; // 幂运算在0.8.0之后,变为右优先,即a ** b ** c => a ** (b ** c)
}
function max() public view returns(uint) {
return uint(-1);
// return type(uint).max; // 0.8不再允许uint(-1)
}
}
// 位运算
contract TestBitwise {
uint8 num1 = 3;
uint8 num2 = 4;
function bitAdd() public view returns(uint) {
return num1 & num2;
}
function bitOr() public view returns(uint) {
return num1 | num2;
}
function unBit() public view returns(uint) {
return ~num1;
}
function bitXor() public view returns(uint) {
return num1 ^ num2;
}
function bitRight() public view returns(uint) {
return num1 >> 1;
}
function bitLeft() public view returns(uint) {
return num1 << 1;
}
}
/** 1.1.3 - 1.1.6 */
contract TestType {
fixed num;
ufixed num2;
fixed8x8 decimal; // fixedMxN, M表示位宽,必须位8的整数倍,N表示十进制小数部分的位数
address addr = msg.sender;
address addr2 = 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF;
TestBitwise t = TestBitwise(addr);
enum ActionChoices { Up, Down, Left, Right }
}
/** 1.1.7 */
// 固定长度的字节数组(静态),以及转换为string类型
contract TestBytesFixed {
// public 自动生成同名的get方法
bytes1 public num1 = 0x7a; // 1 byte = 8 bit
bytes1 public num2 = 0x68;
bytes2 public num3 = 0x128b; // 2 byte = 16 bit
// 获取字节数组长度
function getLength() public view returns(uint) {
return num3.length; // 2
}
// 字节数组比较
function compare() public view returns(bool) {
return num1 < num2;
}
// 字节数组位运算
function bitwise() public view returns(bytes1,bytes1,bytes1) { // 多返回值
return ((num1 & num2), (num1 | num2), (~num1));
}
// 先转为bytes动态数组,再通过string构造 0x7a7a -> zz
function toString(bytes2 _val) public pure returns(string) {
bytes memory buf = new bytes(_val.length);
for (uint i = 0; i < _val.length; i++) {
buf[i] = _val[i];
}
return string(buf);
}
}
// 固定长度的字节数组(静态)扩容和压缩
contract TestBytesExpander {
// public 自动生成同名的get方法
bytes6 name = 0x796f7269636b;
function changeTo1() public view returns(bytes1) {
return bytes1(name); // 0x79
}
function changeTo2() public view returns(bytes2) {
return bytes2(name); // 0x796f
}
function changeTo16() public view returns(bytes16) {
return bytes16(name); // 0x796f7269636b00000000000000000000
}
}
/** 1.2.1 */
// 修改string类型的数据
contract TestString {
string name = 'yorick'; // 字符串可以使用单引号或者双引号
string name2 = "!@#$%^&"; // 特殊字符占1个byte
string name3 = "张三"; // 中文在string中使用utf8的编码方式存储,占用3个byte -> 切换到0.8则中文字符报错
// string memory str = unicode"Hello 😃"; // 0.7.0支持Unicode字符串
// string memory str2 = unicode"\u20ac"; // 0.7.0支持Unicode字符串
function getLength() view public returns(uint) {
// 不可以直接获取string的length
return bytes(name).length; // 6
}
function getLength2() view public returns(uint) {
return bytes(name2).length; // 7
}
function getLength3() view public returns(uint) {
return bytes(name3).length; // 6
}
function getElmName() view public returns(bytes1) {
// 不可以直接通过数组下标name[0]获取
return bytes(name)[0]; // 0x79
}
function changeElmName() public {
bytes(name)[0] = "h";
}
function getName() view public returns(bytes) {
return bytes(name);
}
}
/** 1.2.2 */
// 动态的字节数组,以及转换为string类型
contract TestBytesDynamic {
bytes public dynamicBytes;
function setDynamicBytes(string memory val) public {
dynamicBytes = bytes(val);
}
function getVal() public view returns(string){
return string(dynamicBytes);
}
}
/** 1.2.3 */
// 定长数组
contract TestArrFixed {
uint[5] arr = [1,2,3,4,5];
// 修改数组元素内容
function modifyElements() public {
arr[0] = 12;
arr[1] = 14;
}
// 查看数组
function watchArr() public view returns(uint[5]) {
return arr;
}
// 数组元素求和计算
function sumArr() public view returns(uint) {
uint sum = 0;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
// 数组长度
function getLength() public view returns(uint) {
return arr.length;
}
// delete重置数组某下标的元素值,不会真正删除该元素
function deleteElm(uint idx) public {
delete arr[idx];
}
// delete重置整个数组
function deleteArr() public {
delete arr;
}
/** 定长数组不允许改变长度和push
// 压缩数组后,右侧多余元素被丢弃
function changeLengthTo1() public {
arr.length = 1;
}
// 扩容数组后,右侧元素补0
function changeLengthTo10() public {
arr.length = 10;
}
// 追加新元素
function pushElm(uint _elm) public {
arr.push(_elm);
}
*/
}
// 变长数组
contract TestArrDynamic {
uint[] arr = [1,2,3,4,5];
// 查看数组
function watchArr() public view returns(uint[] memory) {
return arr;
}
// 数组长度
function getLength() public view returns(uint) {
return arr.length;
}
// 压缩数组后,右侧多余元素被丢弃
function changeLengthTo1() public {
arr.length = 1;
}
// 扩容数组后,右侧元素补0
function changeLengthTo10() public {
arr.length = 10;
}
// 追加新元素
function pushElm(uint _elm) public {
arr.push(_elm);
}
/**
// 弹出元素
function popElm(uint _elm) public {
arr.pop(); // 0.4不支持pop
}
*/
// delete重置数组某下标的元素值,不会真正删除该元素
function deleteElm(uint idx) public {
delete arr[idx];
}
// delete重置整个数组
function deleteArr() public {
delete arr;
}
}
/**
二维数组
solidity的二维数组与其他语言不同,[2] [3]表示3行2列,而其他语言为2行3列;
二维动态数组与一维数组类似,可以改变其数组长度;
*/
contract TestArr2Dimensional {
uint[2][3] arr = [[1,2],[3,4],[5,6]];
function getRowSize() public view returns(uint) {
return arr.length; // 3
}
function getColSize() public view returns(uint) {
return arr[0].length; // 2
}
function watchArr() public view returns(uint[2][3]) {
return arr; // 1,2,3,4,5,6
}
function sumArr() public view returns(uint) {
uint sum = 0;
for (uint i = 0; i < getRowSize(); i++) {
for (uint j = 0; j < getColSize(); j++) {
sum += arr[i][j];
}
}
return sum;
}
function modifyArr() public {
arr[0][0] = 99;
}
}
// 数组字面值
contract TestArrLiteral {
// 最小存储匹配,未超过255,所以使用uint8存储
function getLiteral8() pure public returns(uint8[3]) {
return [1,2,3];
}
// 超过255,所以使用uint16存储
function getLiteral16() pure public returns(uint16[3]) {
return [256,2,3]; // [255,2,3] 不被uint16允许
}
// 强制转换为uint256
function getLiteral256() pure public returns(uint[3]) {
return [uint(1),2,3]; // 给任意元素强转即可,否则不被允许
}
// 计算外界传入的内容
function addLiterals(uint[3] arr) pure external returns(uint) {
uint sum = 0;
for (uint i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
}
/** 1.2.4 */
// 结构体初始化的两种方法
contract TestStruct {
struct Student {
uint id;
string name;
mapping(uint=>string) map;
}
// 默认为storage类型,只能通过storage类型操作结构体中的mapping类型数据
Student storageStu;
// mapping类型可以不用在定义的时候赋值,而其他类型不赋值则会报错
function init() public pure returns(uint, string) {
Student memory stu = Student(100, "Jay");
return (stu.id, stu.name);
}
function init2() public pure returns(uint, string) {
Student memory stu = Student({name: "Jay", id: 100});
return (stu.id, stu.name);
}
function init3() public returns(uint, string, string) {
Student memory stu = Student({name: "Jay", id: 100});
// 直接操作结构体中的mapping不被允许: Student memory out of storage
// stu.map[1] = "artist";
// 通过storage类型的变量操作结构体中的mapping
storageStu = stu;
storageStu.map[1] = "artist";
return (storageStu.id, storageStu.name, storageStu.map[1]);
}
// 结构体作为入参时,为memory类型,并且不能使用public或external修饰函数:Internal or recursive type is not allowed for public or external functions.
// 赋值时也要指定为memory,否则报错
function testIn(Student stu) internal returns(uint) {
return stu.id;
}
// 结构体作为出参,同样只能private或internal声明内部使用
function testOut(Student stu) private returns(Student) {
return stu;
}
}
/** 1.3 */
contract TestMapping {
mapping(address => uint) private scores; // <学生,分数>的单层映射
mapping(address => mapping(bytes32 => uint8)) private _scores; // <学生,<科目,分数>>的两层映射
function getScore() public view returns(address, uint) {
address addr = msg.sender;
return (addr, scores[addr]);
}
function setScore() public {
scores[msg.sender] = 100;
}
}
/** 二 */
contract TestAccessCtrl {
constructor () public {}
uint public num1 = 1; // 自动为public生成同名的get函数,但在编码时不可直接调用num1()
uint private num2 = 2;
uint num3 = 3; // 不写则默认private
function funcPublic() public returns(string) {
return "public func";
}
function funcPrivate() private returns(string) {
return "private func";
}
function funcInternal() internal returns(string) {
return "internal func";
}
function funcExternal() external returns(string) {
return "external func";
}
function test1(uint choice) public returns(string) {
if (choice == 1) return funcPublic();
if (choice == 2) return funcPrivate();
if (choice == 3) return funcInternal();
//if (choice == 4) return funcExternal(); // external不允许直接在内部用
if (choice == 4) return this.funcExternal(); // 间接通过this才可以调用external
}
}
contract TestAccessCtrlSon is TestAccessCtrl {
function test2(uint choice) public returns(string) {
if (choice == 1) return funcPublic(); // public允许派生合约使用
//if (choice == 2) return funcPrivate(); // private不允许派生合约使用
if (choice == 3) return funcInternal(); // internal允许派生合约使用
//if (choice == 4) return funcExternal(); // external也不允许派生合约直接使用
}
}
contract TestAccessCtrl2 {
function test2(uint choice) public returns(string) {
TestAccessCtrl obj = new TestAccessCtrl();
if (choice == 4) {
return obj.funcExternal(); // external只允许在外部合约中这样间接调用
} else return "0x0";
}
}
/** 三 */
contract TestFuncDecorator {
uint public num = 1;
/// pure
function testPure(uint _num) public pure {
//uint num1 = num; // pure不允许读状态变量
//num = _num; // pure不允许修改状态变量
}
/// view
function testView(uint _num) public view {
uint num1 = num; // 允许读状态变量
num = _num; // 0.4语法上允许修改状态变量,但实际不会修改,所以num还是1
// 0.5及之后不允许在view中这样修改,否则编译不通过
}
/// payable
function () public payable {}
function getBalance() public view returns(uint) { // balance获取合约地址下的以太币余额
return address(this).balance;
}
// 充值函数payable,只有添加这个关键字,才能在执行这个函数时,给这个合约充以太币,否则该函数自动拒绝所有发送给它的以太币
function testPayable() payable public { // transfer转账
address(this).transfer(msg.value);
}
}
/** 四 */
contract TestConstruct1 {
address private _owner;
constructor() public {
_owner = msg.sender;
}
/**constructor(int num) public { // 重载构造->编译错误
_owner = msg.sender;
}*/
/**
function TestFuncDecorator(uint x) {} // 0.5之前还可以用同名函数定义
*/
}
contract TestConstruct2 {
uint public num;
constructor(uint x) public { // 带参构造,在deploy时传入
num = x;
}
}
/** 五 */
contract TestModifier {
address private _owner;
bool public endFlag; // 执行完test后的endFlag仍是true
constructor() public {
_owner = msg.sender;
}
modifier onlyOwner { // 权限拦截器,非合约部署账号执行test()则被拦截
require(_owner == msg.sender, "Auth: only owner is authorized.");
_; // 类似被代理的test()方法调用
endFlag = true;
}
function test() public onlyOwner {
endFlag = false;
}
}
/** 七 */
contract TestEvent {
event testEvent(uint indexed a, uint indexed b, uint indexed c, uint result); // indexed不能超过三个
function calc(uint a, uint b, uint c) public returns(uint) {
uint result = a ** b ** c;
emit testEvent(a, b, c, result); // 事件会输出在logs中
return result;
}
}
/** 八 */
contract TestUnit {
function testUnit() pure public {
require(1 == 1 seconds);
require(1 minutes == 60 seconds);
require(1 hours == 60 minutes);
require(1 days == 24 hours);
require(1 weeks == 7 days);
require(1 years == 365 days); // years 从 0.5.0 版本开始不再支持
require(1 ether == 1000 finney);
require(1 finney == 1000 szabo);
require(1 szabo == 1e12 wei);
//require(1 gwei == 1e9); // 0.7.0开始加入gwei
}
}
/** 九 */
contract TestException {
function testAssert(int x) public pure {
assert(x >= 0);
}
function testRequire(int x) public pure {
require(x >= 0);
//require(x >= 0, "x < 0");
}
function testRevert(int x, int y) public pure {
if (x != y) revert("x should equal to b");
}
}
/** 十 */
contract TestOverload { // 重载
function addNums(uint x, uint y) public pure returns(uint) {
return x + y;
}
function addNums(uint x, uint y, uint z) public pure returns(uint) {
return x + y + z;
}
}
/** 十一 */
contract TestExtendA { // 父类TestExtendA要写在子类TestExtend之前
uint public a;
constructor() public {
a = 1;
}
}
contract TestExtend is TestExtendA {
uint public b;
constructor() public {
b = 2;
}
}
/// 直接在继承列表中指定基类的构造参数
contract A { // 父类要写在子类之前
uint public x;
constructor(uint _a) public { // 带参构造
x = _a;
}
}
contract B is A(1) { // 指定父类构造参数
uint public y;
constructor() public {
y = 2;
}
}
/// 通过派生合约(子类)的构造函数中使用修饰符方式调用基类合约
// 方式一:
contract B1 is A {
uint public b;
constructor() A(1) public { // 子类构造使用父类带参修饰符A(1)
b = 2;
}
}
// 方式二:
contract B2 is A {
uint public b;
constructor(uint _b) A(_b / 2) public { // 子类带参构造使用父类带参修饰符A(_b / 2)
b = _b;
}
}
/// 连续继承,Z继承Y,Y又继承X
contract X {
uint public x;
constructor() public{
x = 1;
}
}
contract Y is X {
uint public y;
constructor() public{
y = 1;
}
}
// 如果是多个基类合约之间也有继承关系,那么is后面的合约书写顺序就很重要。顺序应该是,基类合约在前面,派生合约在后面,否则无法编译。
// 实际上Z只需要继承Y就行
contract Z is X,Y { // 所以必须是X,Y而不是Y,X
}
/// 多重继承,子类可以拥有多个基类的属性
contract Father {
uint public x = 180;
}
contract Mother {
uint public y = 170;
}
contract Son is Father, Mother {
}
/** 十五 */
library TestLibrary{
function add (uint a,uint b) internal pure returns (uint){
uint c = a + b;
require(c > a, "Math: addition overflow");
return c;
}
}
// 库的调用
contract TestLibraryCall {
function add(uint x, uint y) public pure returns(uint){
return TestLibrary.add(x, y); // 调用库
}
}
// 除了使用上面的TestLibrary.add(x, y)这种方式来调用库函数,还有一个是使用using LibA for B这种附着库的方式。
// 它表示把所有LibA的库函数关联到数据类型B,这样就可以在B类型直接调用库函数。
contract TestLibraryUsing {
using TestLibrary for uint;
//using TestLibrary for *;
function add2(uint x,uint y) public pure returns (uint){
return x.add(y); // uint的数据x就可以直接调用add(y)
}
}
- Test06.sol,
pragma solidity ^0.6.10;
/** 1.2.3 */
contract TestArrDynamic {
uint[] arr = [1,2,3,4,5];
// 弹出元素
function popElm() public {
arr.pop();
}
function watchArr() public view returns(uint[] memory) {
return arr;
}
/**
0.6开始不再可以通过修改length改变数组长度,需要通过push(),push(value),pop的方式,或者赋值一个完整的数组
*/
/**function changeLengthTo1() public {
arr.length = 1;
}*/
}
/** 九 */
// Solidity0.6版本之后加入。try/catch仅适用于外部调用,另外,try大括号内的代码是不能被catch捕获的。
contract TestTryCatch {
function execute (uint256 amount) external returns(bool) {
try this.run(amount) { // 这里的函数异常会被捕获
return true; // 这里的异常不再会被捕获
} catch {
return false;
}
}
function run(uint256 a) public {
//code that can revert
require(a % 2 == 0, "Ups! Reverting");
}
}
/** 十二 */
// 0.6后支持。抽象合约不能使用new创建
abstract contract TestAbstractContract {
uint public a;
constructor(uint _a) internal {
a = _a;
}
function get () virtual public;
}
- Test08.sol,
pragma solidity ^0.8.0;
/** 1.2.1 */
contract TestString {
function test() public view returns(string memory, string memory, string memory) {
string memory str = unicode"Hello 😃"; // Hello 😃
string memory str2 = unicode"\u20ac"; // €
string memory str3 = hex"414243444546474849"; // ABCDEFGHI
// string memory name3 = "张三"; // 0.8不允许中文字符,必须改为unicode
return (str, str2, str3);
}
}
/** 八 */
contract TestGlobalVariable {
function test1() public view returns(/**bytes32,*/ uint, uint, address, uint, uint, uint, uint) {
return (
// blockhash(block.number - 1), // 指定区块的区块哈希,仅可用于最新的 256 个区块且不包括当前区块,否则返回0。0.5移除了block.blockhash
block.basefee, // 当前区块的基础费用
block.chainid, // 当前链 id
block.coinbase, // 挖出当前区块的矿工地址
block.difficulty, // 当前区块难度
block.gaslimit, // 当前区块 gas 限额
block.number, // 当前区块号
block.timestamp // 自 unix epoch 起始当前区块以秒计的时间戳,0.7.0移除了now
);
}
function test2() public payable returns(bytes memory, address, bytes4, uint, uint256, uint, address) {
return (
msg.data, // 完整的 calldata
msg.sender, // 消息发送者(当前调用)
msg.sig, // calldata 的前 4 字节(也就是函数标识符)
msg.value, // 随消息发送的 wei 的数量
gasleft(), // 剩余的 gas,0.5移除了msg.gas
tx.gasprice, // 交易的 gas 价格
tx.origin // 交易发起者(完全的调用链)
);
}
}
/** 十三 */
// 合约中的虚函数(函数使用了virtual修饰的函数)可以在子合约重写该函数,以更改他们在父合约中的行为。重写的函数需要使用关键字override修饰。
// 0.8以下不支持。
contract TestOverride {
function get() virtual public{}
}
contract Middle is TestOverride {
}
contract Inherited is Middle {
function get() public override {
}
}
// 对于多重继承,如果有多个父合约有相同定义的函数,override关键字后必须指定所有的父合约名称
contract Base1 {
function get() virtual public {}
}
contract Base2 {
function get() virtual public {}
}
contract Middle2 is Base1, Base2 { // 指定所有父合约名称
function get() public override (Base1, Base2){
}
}
/** 十四 */
// 0.8以下不支持。
interface TestInterface {
function transfer(address recipient, uint amount) external;
}
contract TestInterfaceSon {
function transfer(address recipient, uint amount) public {
}
}
0x2、各版本主要变化
0.5.0
- sha3改用keccak256, keccak256只允许接收一个参数,使用abi.encodePacked等组合params
- 构造函数由同名空参方法变成constructor
0.6.0
- 仅标记virtual的接口才可以被覆盖,覆盖时需要使用新关键字override,如果多个基类同方法名时,需要像这样列出 override(Base1, Base2)
- 不能通过修改length来修改数组长度,需要通过push(),push(value),pop的方式,或者赋值一个完整的数组
- 使用abstract标识抽象合约,抽象合约不能使用new创建
- 回调函数由function()拆分为fallback()和receive()
- 新增try/catch,可对调用失败做一定处理
- 数组切片,例如: abi.decode(msg.data[4:], (uint, uint)) 是一个对函数调用payload进行解码底层方法
- payable(x) 把 address 转换为 address payable
0.7.0
- call方式调用方法由x.f.gas(1000).value(1 ether)(arg1,arg2)改成 x.f{gas:1000,value:1 ether}(arg1,arg2)
- now 不推荐使用,改用block.timestamp
- gwei增加为关键字
- 字符串支持ASCII字符,Unicode字符串
- 构造函数不在需要 public修饰符,如需防止创建,可定义成abstract
- 不允许在同一继承层次结构中具有同名同参数类型的多个事件
- using A for B,只在当前合约有效, 以前是会继承的,现在需要使用的地方,都得声明一次
0.8.0
-
弃用safeMath,默认加了溢出检查,如需不要检查使用
unchecked { ... }
, 可以节省丢丢手续费 - 默认支持ABIEncoderV2,不再需要声明
-
求幂是右结合的,即表达式
a**b**c
被解析为a**(b**c)
。在 0.8.0 之前,它被解析为(a**b)**c
- assert 不在消耗完 完整的gas,功能和require基本一致,但是try/catch错误里面体现不一样,还有一定作用…
- 不再允许使用uint(-1),改用type(uint).max