Solidity 是一门类似 JavaScript 的编程语言,用于编写智能合约,与其他智能合约语言相比,它使用最为广泛,相关编程的技巧和误区的研究也较成熟,是入门智能合约编程的首选。
pragma solidity >=0.5.9 <0.6.0;// The Store Contractcontract Store {uint256 internal value;function setValue(uint256 v) external {value = v;}function getValue() external view returns (uint256) {return value;}}
如上代码,便是一个最简单的 Solidity 合约。
L1: pragma solidity >0.5.9
表示当前合约应该使用的编译器版本,因为 Solidity 每个版本都有所更新,尤其是有一些语法不兼容的更新。所以需要在合约的开头通过 pragma
限制版本号。版本号限制语法同 npm 。
L3: 注释,注释语法同 JavaScript 。
L4-L14: 合约的主体,所有合约的储存变量接口函数都在此定义。
L5: 这里定义了一个 uint256
类型的合约状态变量 value
, internal
表示它是内部的,外部无法直接通过 value
这个名字访问。
L7-L9: 定义了 setValue(uint256)
方法,external
表示它只能被外部调用,即通过区块链相关 API 调用合约的该方法。在方法实现中,状态变量 value
, 被设置为函数的入参。
L11-L13: 定义了 getValue()
方法, view
表示它不可以更改合约状态变量, returns
表示其返回类型,合约调用的发起者可以获得该返回值。
由此,我们得到一个合约的标准结构,首先是 pragma
表示编译器版本,其次是定义了合约方法,合约状态变量的合约主体结构。部分较复杂的合约还会在 pragma
之后有 import
语句引入其他库函数。
从上一节的合约例子,我们已经见到了最基础了类型 uint256
. Solidity 还支持如下基础类型:
bool
: 即 true
和 false
, Big endian 扩充 256 位。
整数类型: int
和 uint
, 支持后缀数字表示位长,最小从 uint8
到最长 uint256
. Big endian.
address
: 地址类型,表示合约或普通账户地址,为 20 字节。内部处理时高位补齐到 32 字节。同时在 TVM 优化中,也处理了 21 字节地址,即支持前缀 0x41
定长字节: bytes1
, bytes2
, bytes3
, …, bytes32
. byte
是 bytes1 的别名
,高位补齐到 32 字节。
地址,数值。 Solidity 没有八进制数值常量。数值常量可以用科学计数法表示,例如 2.5e1
。
字符串字面常量。支持单引号或双引号。支持常见转义,支持 \uNNNN
转义。
十六进制字面常量。hex"00112233cafe"
。
枚举类型可以通过显式类型转换语法和整数类型互相转换。不支持隐式类型转换。例如 uint(val)
.
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
由于 ABI 不支持自定义类型,所以出现在 ABI 中的枚举,会自动转换为合适的 uintN
类型。例如 function getChoice() public view returns (ActionChoices)
会变为 getChoice() returns (uint8)
.
以上的可以放入一个 uint256
的类型都叫值类型 value type. 与此对应的,是引用类型。
引用类型一般被放入 storage
或 memory
。默认情况下函数参数和返回值中的引用类型是 memory
,合约状态变量是 storage
。(data location)
另外有种特殊的 data location 叫 calldata
隶属于外部函数调用时候传递的参数,类似 memory
但是不可修改。
bytes
和 string
这样的长度动态指定的类型,被叫做 dynamic size type. 虚拟机在操作时代价高于定长类型,应避免使用。两者不同的地方在于,虽然底层表示都是字节,但 string
不允许索引操作(utf8 string)。
数组可以是编译时确定大小,或动态大小。支持高维数组。例如 uint[][5]
。
bytes
类型类似 byte[]
但代价更小,需要尽量避免使用 byte[]
。
创建 array 使用 uint[] memory a = new uint
。
支持 .length (rw) .push() 操作。
struct Funder {address addr;uint amount;}x = MyStruct({a: 1, b: 2});
java-tron ABI 无法支持结构体的描述,所以出现在 ABI 中的结构体统一表述为 tuple
结构。实际合约中应避免在 ABI 中出现结构体。
参考: StackOverflow
即 k-v 哈希字典。mapping(_KeyType => _ValueType) .
key 可以是除 mapping 动态数组,枚举,结构体之外的类型。value 可以是任意类型。
需要注意的是,和其他语言不同, Solidity 中的映射没有 size 的概念,一旦初始化,所有 key 对应的 value 都有默认值(全0值)。所以 mapping 是不可迭代结构。这是由于:
mapping 的内部实现是通过对 key 进行 hash 运算得到一个储存位置,返回这个储存位置对应的值。而对于合约储存而言,储存位置默认都是 0.
有了类型,就有类型间的转换。值域属于子集的类型,可以隐式类型转换,否则需要显式类型转换。例如:
int8 a = -3;uint x = uint(a);
此时, x 将等于 -3 的二进制补码表示。
合约的 Storage 生命周期同合约本身。同 state variable.
private public external internal
自动为 public storage 变量创建 getter.
程序的功能和逻辑,离不开基础表达式和控制结构。
Solidity 的表达式基本同 C/JavaScript 系语言,相关参考见: 运算符优先级
Solidity 的基本控制结构同样来自 C/JavaScript 系语言,即: if
, else
, while
, do
, for
, break
, continue
, return
.
此外,对于外部调用和合约创建调用, Solidity 还支持 try/catch
语句。(TRON 目前对合约创建合约的支持较弱)
TRON 目前(4.0.1)只支持合约创建合约的 CREATE
调用, CREATE2
调用并没有实现。
合约的主要逻辑在函数中定义。函数定义的语法如下:
function [name](<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)] {function_body}
如果函数作为类型出现,那么只需要 function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
。
这里的 internal/external 是访问控制关键字,四种访问控制关键字如下:
private
: only from this contract
internal
: this contract and derived contract
external
: can not be accessed internally
public
: may be called internally
实际使用中,应选择最小范围的访问控制,例如使用 external
而非 public
.
constant
: 不需要网络确认的操作,例如查询 storage, 新版 Solidity 已经废弃了该关键字
view
: 用于替换原有的 constant
关键字,表示函数不会修改 storage
pure
: 比 view
更严格,不允许查询 storage
而 payable
是单独的概念,用于表示该合约函数可以处理代币转账。其中无函数名的特殊 payable
函数叫 fallback function:
function() public payable {}
this.f.selector: byte4// internal formf()// external formthis.f()
函数修饰符用于改变函数的行为,例如自动权限检查。
modifier onlyOwner {require(msg.sender == owner,"Only owner can call this function.");_;}function close() public onlyOwner {selfdestruct(owner);}
Event 通过 EVM/TVM 的 logging 机制,提供了高效的事件记录机制。
contract SimpleAuction {// 定义事件event HighestBidIncreased(address bidder, uint amount);function bid() public payable {// 触发事件emit HighestBidIncreased(msg.sender, msg.value);}}
Solidity 支持合约的多重继承机制。父合约可以定义虚函数,由子合约实现,虚合约,抽象合约。
多重继承不会导致创建多个合约,最终只有一个合约被创建,相关父合约的代码会被 copy 到最终合约。
contract owned {constructor() { owner = msg.sender; }address owner;}contract mortal is owned {function kill() {if (msg.sender == owner) selfdestruct(owner);}}/// ...contract named is owned, mortal {constructor(bytes32 name) {Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);NameReg(config.lookup(1)).register(name);}}// 继承并提供构造函数contract PriceFeed is owned, mortal, named("GoldFeed") { ... }
默认构造函数 contructor() public {}
.
构造函数可以被标记为 public
或 internal
. internal
相当于抽象合约(类)。
父类构造函数也可以通过如下方式传递:
contract Derived2 is Base {constructor(uint _y) Base(_y * _y) public {}}
接口相当于抽象类。
Cannot inherit other contracts or interfaces.
Cannot define constructor.
Cannot define variables.
Cannot define structs.
Cannot define enums.
库的存在,避免了自己实现轮子的麻烦。保证了多个合约共享逻辑的一致性。
需要区分库和父合约。库其实是一个单独的合约。代码独立与调用者存在,需要通过 DELEGATECALL
指令调用。
库和合约必须单独部署,且库不产生 ABI 文件。库先部署完毕后,得到一个合约地址, 将该合约地址转换为 20 字节地址,填入目标合约 .bin
文件的占位符后。再部署最终合约。
占位符格式在不同的版本的 Solidity 编译器下有所不同。
参考: Libraries.