Solidity是一门静态类型的脚本语言,我们可以对照C++的语法进行快速记忆。
- contract:类似于class,定义一个合约,具有构造函数,仅在创建合约时被调用。
- function:定义一个函数。
- event:定义一个事件,外部Dapp可以监控这些事件,以获知合约内的变化。
- var:声明变量,类似于C++11的auto,可以在初始化时进行自动类型推导,之后不能更改类型。
- bool: 布尔类型,有true跟false两种值。
- uint8、uint16、。。。uint256:无符号整型,uint是uint256的别名。
- nint8、int16、。。。int256:有符号整形,int是int256的别名。
- ufixedMxN:无符号定点小数,M表示整个类型占用的bit数,N表示小数位数。ufixed64x7:7位小数,剩下的是整数部分
- fixedMxN:定点小数。
- address:地址类型,表示账户地址或者合约地址。20Byte。
addr.balance:返回uint256类型的值,表示addr账户的余额(Wei)。
addr.send(uint256 N):转给addr地址N Wei数量的以太币,失败是返回false。
addr.transfer(uint256 N):包装了send方法,失败时直接抛出异常,会导致整个交易回退。
addr.call、addr.callcode、addr.delegatecall:调用addr合约的指定的方法,区别稍后详述。
- 逻辑操作符:!(逻辑非)、&&(逻辑与)、||(逻辑或)。跟C++完全一致,并且&&与||同样存在短路求值。
- 比较操作符:<、<=、>、>=、==、!=,跟C++完全一致。
- 算术操作符:+(正号)、-(负号)、 +、-、*、/、%(取余)、<<(左移)、>>(右移)、**(幂)。除了幂,其余的都跟C++一致。
- 位操作符:&(与)、|(或)、~(非)、^(异或)。
数组
- 定长数组:编译期长度就固定下来的数组是定长数组,这样定义一个定常数组:T[k](例如 uint8[ 5 ] arr)。bytes1、bytes2, bytes3, …, bytes32,这些也是定长数组。bytes1可以简写成byte。定长数组是值类型(value-type),可以进行比较操作、位操作、索引操作。
- 动态数组:编译期长度不固定,类似于C++中的vector,这样定义一个动态数组:T[] (例如 int256[] arr)。
string是特殊的动态数组。普通的定长数组、动态数组都可以进行取长度操作:arr.length,以及下标索引操作,但是string暂时不支持这两种操作。bytes也是动态数组,相当于byte[],但是比byte[]要更、廉价一些,应该尽量使用bytes。另外,动态数组、bytes还可以调用push方法,在数组末尾添加数据,返回最新的长度。字符串
string本质上是经过UTF8编码的byte数组。当前版本的solidity对string的实现十分不完整,无法支持串联、比较、下标索引等操作,甚至连取长度都不支持。当前的string仅可以用来做mapping的key。结构体
跟C++很像,这样定义一个结构体:struct MyStruct{ bool flag; string name; } MyStruct a;
结构体、数组里未被初始化的元素,都是0。
枚举
mapping
solidity里使用频率比较高的类型。mapping (address => uint256) balanceOf; 定义了一个map,使用地址做索引,值位uint256.
- 以太币单位
1 Ether = 1000 Finny
1 Finny = 1000 Szabo
1 Szabo = 1000 Gwei
1 Gwei = 1000 Mwei
1 Mwei = 1000 Kwei
1 Kwei = 1000 wei
1 Ether = 10^18 wei- 时间单位
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
1 years == 365 days- block对象
block.blockhash(uint blockNumber) returns (bytes32):返回指定高度的块的hash值,仅限最近256块。
block.coinbase (address):当前块的矿工
block.difficulty (uint):当前块的难度
block.gaslimit (uint):当前块的gaslimit
block.number (uint):当前块高度
block.timestamp (uint): 当前块时间戳- msg对象
msg.data (bytes):当前调用完整的原始数据
msg.gas (uint): 剩余的gas,0.4.21版本之后弃用,替换为gasleft。
msg.sender (address): 当前调用的发起者。
msg.sig (bytes4):调用数据的头四字节
msg.value (uint):当前消息携带的以太币,单位wei。- tx对象
tx.gasprice (uint):当前交易的gas price
tx.origin (address): 当前交易的发起者。- 数学函数、hash函数
addmod(uint x, uint y, uint k) returns (uint):
mulmod(uint x, uint y, uint k) returns (uint):
keccak256(…) returns (bytes32):
sha256(…) returns (bytes32):
sha3(…) returns (bytes32):
ripemd160(…) returns (bytes20):
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):- 异常处理函数
assert(bool condition):
require(bool condition):
revert():- 其他
gasleft() returns (uint256)
now (uint):
this
suicide
contract类似于class,他有:
- 构造函数。跟contract名称相同的function即为构造函数,在合约创建时被调用,可以有参数,无返回值。
- 自杀函数。selfdestruct(address)销毁合约,并把合约账户里的ether转移到指定的地址,花费比调用transfer小。suicide是selfdestruct的别名。
- this。在合约内,this可以转化成address。在合约内部调用自己的external函数也需要用到this。
- 成员变量。
- 成员函数。
- 继承、被继承。一个合约可以继承其他合约。
合约内的成员变量、成员函数需要使用可见性来修饰,不修饰默认为public(跟C++刚好相反)。
- external:只能用来修饰成员函数,这样的函数只能被外部合约调用,合约内部想要调用该函数,需要使用this.func();
- public:修饰变量,则编译器会自动生成一个同名的getter。修饰函数,则外部可以调用该函数。
- internal:类似于protect,可被合约内部以及子类合约访问。
- private:私有的,仅合约内部可以访问,子类不可访问。
函数可以有多个返回值:
直接通过一个稍微复杂点的例子来看下solidity的函数是啥样子的。
uint16 uCount; //合约内的成员变量
function add(uint8 a, uint8 b) public pure returns(uint8){
return a + b; //先忽略溢出。
}
function GetCount() public view returns(uint16){
return uCount;
}
function setCount(uint16 count) public {
uCount = count;
}
modifier validAddress(address addr){
assert(addr != address(0));
_;
}
modifier validAmount(uint256 amount){
reqire(amount > 0);
_;
}
function myTransferFunc(address to, uint256 amount) public validAddress(to) validAmount(amount){
//无需再检查参数的合法性,如果不合法,会在modifier中抛出异常,进不到函数体中。
//........
//........
}
function deposit(uint256 amount) public payable returns(bool, string){
//........
return (true, "successfull");
}
function () public payable{
//.......
}
从上往下依次看,add方法很简单,实现了两个数相加,但是pure是啥意思?GetCount方法的view又是啥意思?SetCount为啥没有这两个东西?pure:不改变合约状态,也不读取合约状态的函数,开发者应当主动使用pure修饰;读取合约状态但是不修改合约状态的函数,使用view修饰;SetCount改变了合约状态,不能被这两个中的任何一个修饰。(老版本的solidity没有view与pure,只有一个constant,凡是不改变状态的函数需要被constant修饰,后来细化成两部分view + pure)。
接下来的modifier,业界普遍翻译成函数修改器,我觉得应该叫 函数卫词,它就像是一个卫语句,在函数运行之前过滤参数的合法性。不合法直接抛出异常,退出函数体,整个交易都不会被执行。
再往下,一个新关键词:payable。被这个修饰的函数,才能够被转账,否则只能是普通调用。
最后,一个没有名字的函数,叫回退函数(fallback function)。当且仅当一个合约的回退函数被实现了且被payable修饰了,才能向这个合约地址直接转账。
事件是另一个新东西,可以让外部dapp监控合约内的变化。这里先简单介绍下事件的定义以及触发,事件的监听后面补上。
event OnTrasnsfer(address from, address to, uint value);
function myTransferFunc(address to, uint256 amount) public validAddress(to) validAmount(amount){
//转账
。。。
//新版本的solidity触发事件需要使用emit关键字,之前的版本不用,但是看起来像是个函数调用。
emit OnTrasnsfer(address(this), to, amount);
}
solidity支持继承,并且支持多重继承。
下面的代码展示了基本的继承的作用:代码重用。
contract ERC20Token{
string public name;
string public symbol;
uint256 public decimals;
uint256 public totalSupply;
mapping (address => uint256) public balanceOf;
mapping (address => mapping (address => uint256)) public allowance;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
//基类里可以不给实现,只做函数声明。也可给一个实现,子类根据情况,选择直接使用用或者重新写一个实现覆盖父类的
function transfer(address _to, uint256 _value) public returns (bool success); function transferFrom(address _from, address _to, uint256 _value) public returns (bool success); function approve(address _spender, uint256 _value)public returns (bool success); } contract TokenA is ERC20Token {
function MyToken(string _name, string _symbol, uint256 _decimals) public{
name = _name;
symbol = _symbol;
decimals = _decimals;
}
//如果父类只声明了该方法,此处为实现。若父类实现了,此处会覆盖父类的实现。
function transfer(address _to, uint256 _value) public returns (bool success){
}
}
contract TokenB is ERC20Token {
.....
.....
}
假如ERC20Token里的方法只给了声明,那ERC20Token就是一个抽象合约,该合约不能被部署只能做父类。
接下来就是由继承+重写(override)而引出的多态:
contract Base{
function Func() public pure returns(uint256){
return 11112222;
}
}
contract Drived is Base{
function Func() public pure returns(uint256){
return 33334444;
}
}
contract TestContract{
function TestContract() public{
}
function TestPolymorphism(address subClassAddr) public pure returns(uint256, uint256){
Drived objDrived = Drived(subClassAddr);
Base objBase = Base(subClassAddr);
return (objDrived.Func(), objBase.Func());
}
}
我们先部署Base、Drive两个合约,分别得到合约地址baseAddr、drivedAddr,再部署TestContract合约,分别把两个地址做参数调用TestPolymorphism方法,结果如下:
- 使用baseAddr做参数,输出为:
11112222
11112222
*使用drivedAddr做参数,输出为:
33334444
33334444
有多重继承,及不可避免地会引起棱形继承,看下面代码:
contract Ancestor{
function Func(uint256 val)public pure returns(uint256){
return val;
}
}
contract BaseA is Ancestor{
function Func(uint256 val)public pure returns(uint256){
return Ancestor.Func(val * 2);
}
}
contract BaseB is Ancestor{
function Func(uint256 val)public pure returns(uint256){
return Ancestor.Func(val * 4);
}
}
contract Final is BaseA, BaseB {
}
我们部署了Final合约之后,该合约对外只有一个可以调用的方法:Func,使用一个数字N做参数,输出会是多少呢?答案是4N。如果把继承的顺序改为 contract Final is BaseB, BaseA ,则输出的结果便是2N。solidity在处理多继承的时候,有一个结构叫继承图(inheritance graph),对于第一种情况,继承图是这样的:Final-BaseB-BaseA-Ancestor。所以调用Final.Func实际上调用的是BaseB里的Func。
接下来看看神奇的super
contract BaseA is Ancestor{
function Func(uint256 val)public pure returns(uint256){
return super.Func(val * 2);
}
}
contract BaseB is Ancestor{
function Func(uint256 val)public pure returns(uint256){
return super.Func(val * 4);
}
}
contract Final is BaseA, BaseB {
}
部署了Final之后调用Func方法,传入N,结果是8N。为啥?super不是简单的调用所在的合约的父合约,而是调用继承图中的下一个合约里的方法:继承图为Final-BaseB-BaseA-Ancestor,BaseB中的super.Func实际上调用的是BaseA.Func,所以最终的N被放大了4*2=8倍。
require与assert都是用来进行条件过滤的,当里面的条件表达式为false时,抛出异常回退整个交易。二者本质的区别在于:
- require(opcode为0xfd)会直接异常,已用掉的gas送给旷工,未使用的gas返回给交易发起者。
- assert(opcode为0xfe)也是抛出异常,同时消耗掉交易发起者提供的 所有 gas。
使用者可根据以上特点酌情使用两者。比如,在需要过滤恶意调用的地方使用assert,能够增加hacker的攻击成本。
throw和revert两个同样使用的是0xfd操作码,跟require是一样的。但是在0.4.10版本之前,throw用的是0xfe。所以不推荐使用throw而推荐用revert代之,因为throw会在不同版本的编译器上体现不同的行为。