枚举
枚举是创建用户自定义类型的一种方式。
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill } ActionChoices choice;
枚举可以和所有的整型显示相互转换,但是不能隐式转换。
uint num = uint(choice);
从整型显示转换到枚举类型,会在运行时检查整数是否在枚举范围内,超过的话会导致异常。
choice = ActionChoices(num);
枚举最少包含 1 个成员,最多可以包含 256 个成员。
枚举默认值是第一个成员。
枚举的数据表示和 C 语言是一样的,从 0 开始的无符号整数开始递增。
构造函数
部署合约时会由 EVM 自动调用构造函数,和常规的编程语言语法一致。
contract MyContract { constructor () { } }
如果在构造函数中设置参数的话,那么在部署时需要传入对应参数的值。
contract MyContract { constructor (uint256 initNum) { } }
构造函数不支持重载。
如果一个合约没有构造函数,那么会采用默认构造函数,将所有变量初始化为类型对应的默认值。
函数
语法是 function(type param) {internal|external} [pure|view|payable] [returns(paramType)]
可访问性标识符、状态标识符、函数修改器
函数可以定义在合约之外,但是只能通过 internal 的形式访问。
函数可以接受多个参数,也可以返回多个返回值。
函数修改器
可以放在函数声明中,具有修改函数行为的能力。
modifier onlyOwner() { require(msg.sender == owner, "only owner"); _; }
常用的关键字有 require 和 _。
require 有两个参数,第一个参数是一个 bool 值,如果为 false,那么就会触发错误,终止函数运行。第二个参数是当发生错误时的消息。
_ 表示函数运行。
使用函数修改器只需要在函数的修饰符部分添加修改器的名字即可,如果要添加多个修改器,使用空格隔开。
function setState(uint _state) external onlyOwner m2 m3 { state = state; emit StateChanged(_state) }
函数修改器可以被继承。
函数修饰符
修饰符可以用在成员属性或者函数上,它决定了成员属性/函数的访问权限,共有 4 种:
- public:最大访问权限,任何人都可以调用。
- private:只有合约内部可以调用,不可以被继承。
- internal:子合约可以继承和调用。
- external:外部可以调用,子合约可以继承和调用,当前合约不可以调用。
external 和 public 的函数是合约的成员变量,可以通过 fn.address 来获取地址,通过 .selector 来获取标识符,这也被称作函数选择器。
函数调用
函数分为内部函数与外部函数。
内部函数
只有在同一个合约内的函数可以内部调用,内部调用可以递归调用。函数调用在 EVM 中会被解释为简单地跳转,内存不会被清除。
比如可以做斐波那契数列。
contract MyContract { function fibonacci(uint256 n) public returns (uint256) { if (n == 1 || n == 2) { return 1; } return fibonacci(n - 2) + fibonacci(n - 1); } }
外部调用
调用父合约的 external 方法和调用其他合约中的 external/public 方法,都属于外部调用。
调用父合约的方法使用 this.fn();,调用外部合约的方法使用 contract.fn();。
进行外部调用会通过消息调用,而不是简单跳转。
接口
与传统语言一样,使用关键字 interface。
接口可以被合约继承。
interface Token { function transfer(address recipient, uint amount) external; } contract MyToken is Token { function transfer(address recipient, uint amount) external override {} }
事件
定义事件:
event eventName(paramsType paramsName)
触发事件。
emit eventName(params)
事件会被记录到区块链的 Log 中,区块链的 Log 分为索引和数据。我们可以指定最多 3 个参数为 indexed,表示它们可以被索引。
前端可以通过 web3.js 来订阅和监听事件。
事件也可以被继承。
控制结构
solidity 支持大多数传统编程语言的流程控制语句。比如 if、else、while、do、for、break、continue、return。但是不支持 goto 和 switch。
solidity 支持 try/catch 做异常处理,但是只支持外部函数调用和合约创建调用。
数据存储位置
所有引用类型的数据(包括数组、结构体、mapping、合约等)都有三种存储位置。分别是:
- 内存 memory:合约执行时的内存。
- 存储 storage:合约的永久存储。
- 调用数据 calldata:不可修改,函数的参数。和 memory 有些像,但和内存不在同一个位置。
直接声明在合约中的变量都会存储在 storage 中。
声明为 external 的函数,参数必须存储在 calldata。
在 storage 和 memory/calldata 之间进行复制,会创建独立的拷贝。
memory 和 calldata 之间相互赋值不会创建拷贝,而是创建引用。
storage 与本地 storage 之间的赋值也只会创建引用。
contract MyContract { uint256[] arr1; // arr1 存储在 storage 中 // arr2 存储在 memory 中 function fn1(uint256[] memory arr2) public { // memory 赋值到 storage 中,创建拷贝 arr1 = arr2; // stoarge 赋值到 本地 storage 中,创建引用 uint256[] storage arr4 = arr1; // pop 会同时影响 arr1 arr4.pop(); // 清空 arr1,同时会影响 arr4 delete arr1; // storage 是静态分配内存,所以不可以直接从 memory 赋值到本地 storage 中 // arr4 = arr2; // 因为没有指向存储位置,所以无法重置指针 // delete arr4; // storage 之间传递引用 fn3(arr1); // storage 到 memory 会拷贝 fn4(arr1); } // arr3 存储在 calldata 中 function fn2(uint256[] calldata arr3) external {} function fn3(uint256[] storage arr5) internal pure {} function fn4(uint256[] memory arr6) public pure {} }
在使用数据时,要优先考虑放在 memory 和 calldata 中。
因为 EVM 的执行空间有限。而且如果 storage 的占用很高,Gas 费也会很贵。
单位
solidity 中有两种单位。以太单位和时间单位。
以太单位
以太单位是以太坊独有的单位,在其他编程语言中没有这种单位。
1 wei 等于 1。
1 gwei 等于 1e9。
1 ether = 1e18。
用代码表示如下:
assert(1 wei == 1); assert(1 gwei == 1e9); assert(1 ether == 1e18);
时间单位
默认 1 等于 1 秒。
solidity 支持以下时间单位:
- seconds:秒
- minutes:分
- hours:时
- days:天
- weeks:周
- years:年,不推荐使用。
用代码表示如下:
assert(1 seconds == 1); assert(1 minutes == 60 seconds); assert(1 hours == 60 minutes); assert(1 days == 24 hours); assert(1 weeks == 7 days);
错误处理与异常
Solidity 使用状态恢复异常来处理错误。这种异常会撤销当前调用以及子调用中的状态变更,并且会向调用者标记错误。
外部调用的异常可以被 try/catch 捕获。
assert
assert 用在我们认为不会出现错误的地方,它返回 Panic(uint256) 类型的错误。
function buy(address payable addr) public { addr.transfer(1 ether); assert(addr.balance > 1 ether); }
require
require 通常用来条件判断,它会创建一个 Error(string) 类型的错误,或者是没有错误数据的错误。
function buy(uint amount) public { require(amount < 1, "amount must be greater than 1"); }
revert
可以用来标记错误并且退回当前调用。
require 本身也会去调用 revert。
function buy(uint amount) public { if(amount < 1) { revert(amount > 1, "amount must be greater than 1"); } }
区块和交易属性
区块和交易属性都是以全局变量或者全局函数的形式存在的。我们可以直接访问它们。常见的属性如下:
- blockhash(uint blockNumber) returns (bytes32):获取指定区块的区块哈希,可用于最新的 256 个区块,不包含当前区块。
- block.chainid:uint 类型,当前链的 id。
- block.coinbase:address 类型,当前区块的矿工地址。
- block.diffculty:uint 类型,当前区块的难度。
- block.gaslimit:uint 类型,当前区块的 gas 限额。
- block.number:uint 类型,当前区块号。
- block.timestamp:uint 类型,从 unix epoch 到当前区块以秒计的时间戳。
- gasleft() returns (uint256):剩余的 gas。
- msg.data:bytes 类型,完整的 calldata。
- msg.sender:address 类型,消息发送者(当前调用者)。
- msg.sig:bytes4 类型,calldata 的前 4 个字节,也就是函数标识符。
- msg.value:uint 类型,消息发送的 wei 数量。
- tx.gasprice:uint 类型,当前交易的 gas 价格。
- tx.origin:address payable 类型,交易发起者。
receive 和 fallback
receive 是一个特殊的函数,一个合约可以包含最多一个 receive 函数。
receive 没有 function 关键字,必须是 external payable 的。可以是 virtual 的,可以被重载,可以添加 modifier。
我们给合约转账时,会去执行 receive 函数。如果转账时 receive 函数不存在,会去调用 fallback 函数。如果 fallback 函数也不存在,那么合约不可以通过正常转账来接受 ether。
fallback 函数和 receive 类似,只能最多有一个 fallback 函数,必须是 external 的,可以是 virtual 的,可以被重载,可以添加 modifier。但 payable 是可选的。
fallback 方法可以接受参数,也可以返回数据。
如果调用某个合约的函数,但是这个函数不存在,会调用 fallback。
contract MyContract { receive() external payable {} fallback() external {} }
我是代码与野兽,一位长期专注于 Web3 的探索者,同时也非常擅长 Web2.0 中的前后端技术。
如果你对 Web3 感兴趣,可以关注我和我的专栏。我会持续更新更多 Web3 相关的高质量文章。