-
一、基本介绍
Solidity是一门静态类型语言,支持继承、库和复杂的用户自定义类型等特性。
二、基础语法
1. 地址(address)
- 以太坊中的地址为160位,即20个字节大小,所以可以用一个uint160表示。
- eip-55:有关验证地址的合法性
- 在合约内部,this表示当前合约本身,由于合约也是一个address类型(contract是从address继承的),所以this其实也是address类型的值。
2. 类型转换
(1) 隐式转换
(1)无符号整数(uint)可以转换为相同或更大尺寸的字节类型(bytes),但是反过来不可以转换。
(2)任何可以转换为uint160类型的变量都可以转换为地址类型(address)。pragma solidity ^0.4.17; contract convert{ int8 a = 1; int16 b = 2; // 返回的类型必须是int16,如果返回int8类型会报错,这边运算符已经帮我们隐式转换了类型 function test() public view returns(int16) { // a+b 的结果会隐式的转换为int16 return a + b; } }
(2)显式转换
int8 public c = -2; uint8 public d = uint8(c); // 此时的d为254 uint public e = uint(c); // e = 115792089237316195423570985008687907853269984665640564039457584007913129639934
3. 发送以太币的两种方法
(1)transfer
transfer从合约发起方向某个地址转入以太币,当地址无效或者发起方余额不足时,transfer将抛出异常。
// 向addressA转入一个以太币 addressA.transfer(1 ether); // or // 附带 gas addressA.transfer.gas(120000)(1 ether);
(2)send
send是transfer的低级版本。当合约执行失败时,send会返回false。当转账成功,则返回true。
owner.send(SOME_BALANCE); // 失败时返回false if(owner.send(SOME_BALANCE)){ ... }
使用send时需注意以下三点:
- (1)send方法需包裹在if中,因为在调用send函数时,合约可能会有函数执行,这些函数可能会执行失败。
- (2)在用send方法发送以太币之前,请先执行减少账户余额的操作,因为可能会有递归调用消耗完合约余额的风险。
- (3)用户可以重载send方法。
总结:
x.transfer(y)等价于if(!x.send(y)) throw; ,send是transfer的底层实现,建议尽可能使用transfer。4. 字节数组
(1)固定长字节数组
固定长度字节数组是以bytes加上数字后缀的方式定义的。
byte a; // byte 等同于bytes1 a bytes2 b; ... bytes32 c;
索引访问:bytes1~bytes32 支持索引访问,但是这种索引访问是只读的,不能使用进行赋值。
bytes10 b b[0] // 获取第1个字节 b[1] // 获取第2个字节 ... b[9] // 获取第9个字节 b[0] = x // 不能使用索引的形式进行赋值,因为这种索引访问是只读的。
可以将 byte[] 当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费 31 字节。 更好地做法是使用 bytes。
(2)动态长度字节数组
bytes:动态长度字节数组
一种特殊的数组。bytes类似于byte[],在外部函数作为参数时,会进行压缩打包以便节省空间,所以尽量用bytes。
bytes m;
string:动态长度字符数串
- (1)字符串以UTF-8的形式编码
- (2)类似bytes,但不提供长度和按索引的访问方式。
// 字符串是双引号 string n = "hello"; // 不支持长度访问 n.length // 不支持按索引访问 n[0]
- (1) string不支持通过索引访问,但可以通过string类型的值转换为bytes类型的值,然后就可以使用索引访问字符串的特定字节。
- (2) 由于bytes类型的值是可读写的,所以要修改string类型的值,可以先将string类型的值转换为bytes类型的值,修改完后,再将bytes类型的值转换为string类型的值。
value2 = typename ( value1 ); //类型转换
其中typename表示类型名,如string、bytes等。
string类型有一些缺陷:如,不能直接使用加号(+)进行连接,但可以通过bytes类型间接将两个或多个字符串连接起来。
字符串连接的基本原理:
- (1)创建一个尺寸与所有参与连接的字符串尺寸之和相同的大字符串;
- (2)将该大字符串转换为bytes类型;
- (3)依次迭代参与连接的字符串,将字符串中的字符逐一添加到bytes类型的值中;
- (4)将这个bytes类型的值转换为string类型的值。
// internal表示函数只能被合约内部调用,函数在合约外不可见 function strConcat(string memory _str1,string memory _str2) internal pure returns(string memory) { // 先将string转化为bytes类型的值 bytes memory _bytesValue1 = bytes(_str1); bytes memory _bytesValue2 = bytes(_str2); // 创建一个能容纳_str1和_str2的string对象 string memory resultStr = new string(_bytesValue1.length + _bytesValue2.length); // 创建与_str1和_str2总和同样尺寸的bytes对象 bytes memory resultBytes = bytes(resultStr); uint index = 0; for(uint i=0;i<_bytesValue1.length;i++){ resultBytes[index++] = _bytesValue1[i]; } for(uint i=0;i<_bytesValue2.length;i++){ resultBytes[index++] = _bytesValue2[i]; } return string(resultBytes); }
5. 枚举类型(enum)
枚举是Solidity中的自定义数据类型。枚举可以显式转为整型,但是不能与整型隐式转换,枚举在一般程序中可以当作状态机使用。
// 定义枚举类型(类型名为enumName) enum enumName{ value1, value2, ... , valueN}
例如:
// 定义一个枚举类型名为Country的枚举类型,每一个枚举值都对应一个整数索引,China表示0,America表示1,以此类推。(不用添加分号) enum Country {China,America,Japan,Australia,Canada,South_Korea} // 定义枚举Country类型的变量 Country country; // 赋值 country = Country.China; //使用枚举值进行赋值 // or country = Country(0); //使用整数值进行赋值(必须显式类型转换) // 枚举可以显式转化为整型 uint currentCountry = uint(Country.China); // 0
注意:
- (1)在合约中可以使用枚举值设置枚举变量,也可以使用整数值设置枚举变量,后者必须显式类型转换。
- (2)在remix环境中测试智能合约时,再输入测试数据时不能直接输入Country.China或其他枚举值,而需要输入整数,如0,1,2,3等。
6. 函数类型
- (1)函数声明的类型称为函数类型。(注意:强调的是类型,类似我们常见的uint,string类型)
- (2)函数类型可以作为函数参数的类型和函数返回值的类型。
- (3)只要函数的参数个数、参数类型和函数返回值与函数类型一样,该函数就可以赋给一个函数类型的变量。
//func是一个函数类型变量 function (uint,uint) returns(uint) func; function add(uint x, uint y) public returns(uint){ return x+y; } function test() public{ // 将add函数赋给函数类型变量func func = add; }
7. 函数和状态变量的4种可见性(访问权限)
(1)访问权限
(1) 如果不为函数指定访问权限,默认为public。
(2) 合约的状态变量不能用external修饰,否则无法编译通过。
(3) 如果不为状态变量指定访问权限,默认为internal。4类可见性(访问权限) 指定:
- public:智能合约外部和内部都可使用的方法;
用public声明的状态变量,会自动产生一个getter函数。
- internal:智能合约(包括派生合约,子合约)内部才可调用的方法;internal修饰的状态变量只能通过在内部的方式访问;
- external:可通过其他合约和交易进行调用的方法;
(1)external函数不能直接调用,前面需要加this, 如this.func( )。
(2)在接收大量数据时,external函数有时更有效率。- private:只有在定义的合约中才可以调用,即使派生的合约也无法调用;用private修饰的状态变量也是如此,只在当前合约中可以访问(子合约也不能访问)。
(2)getter函数
如果合约的状态变量使用public修饰,Solidity编译器会自动为状态变量生成一个与状态变量同名的getter函数,用于获取状态变量的值。
- (1)如果状态变量的数据类型比较复杂,如mapping,生成的getter函数会带一些参数。
- (2)尽管自动生成的getter函数使用external修饰,但是不能在合约内使用this调用。(尽管不会产生编译错误,但是无法成功部署合约)
- (3)在合约内部,getter函数会解析为状态变量。
例如,如果状态变量persons是mapping类型,在合约内部应该使用persons[key], 而不是this.persons(key)形式。
不过在合约外部必须使用getter函数形式引用persons。(如:my.persons(key), my为创建的合约对象名,persons(key)即为状态变量persons的getter函数形式)pragma solidity >=0.4.20 <=0.7.0; contract MyContract{ uint public data = 115; string public personName; uint public personAge; struct Person{ string name; uint age; } mapping(uint=>Person) public persons; constructor() public{ data = 200; // 创建Person结构体的实例 Person memory person = Person({ name:"Lebron James", age:34 }); // 将person添加到persons映射中 persons[10] = person; // 在合约内部不能使用persons的getter函数形式引用persons映射, // 所以尽管下面的代码编译不会出错,但无法成功部署在以太坊上。 // (string memory name,uint age) = this.persons(10); string memory name = persons[10].name; uint age = persons[10].age; personName = name; personAge = age; } } contract GetterContract{ MyContract my = new MyContract(); function getData() public view returns(uint){ // 调用MyContract合约中的data状态变量对应的getter函数(data函数) return my.data(); } function getPerson(uint id) public view returns(string memory,uint){ // 调用MyContract合约中persons状态变量对应的getter函数(persons函数) // 该函数返回了多个值,这些值都是Person结构体的成员, // 如果这个结构体的某个成员的数据类型无法通过函数返回(如mapping),那么系统就会忽略这个结构体成员。 (string memory name,uint age) = my.persons(id); return (name,age); } }
8. 数组(稍有不同)
(1)不同之处
solidity中数组与大多数语言稍有不同。具体如下:
// 定义一个j行i列的二维数组(注意:定义的时候列在前面,行在后面) int[i][j] arrayName; // 为数组arrayName的第m行第n列元素赋值,赋值为20.( 注意:赋值的时候,行在前面,列在后面) arrayName[m][n] = 20;
- (1)定义的时候列在前面,行在后面;
- (2)赋值的时候,行在前面,列在后面;
(2)注意事项(一)
- 对于storage数组,可以保存任意类型的数据,包括另一个数组、映射或结构体。
- 但对于memory数组,不能存储映射类型的数据。
- 如果作为 public 函数的参数,它只能是 ABI 类型。
(3)注意事项(二)
- (1) 如果你在一个空数组中使用.length,这将会造成向下溢出(不减小反而增大),导致长度变为2^256-1。
- (2) 增加一个storage数组的长度花费的gas成本是一个常量值,因为storage变量被当作是zero-initialised(领初始化)的;
而减少storage数组的长度花费的gas成本至少是线性增长的(但事实上,大多数都要高于线性增长),因为其包含了显式清除要被删除的元素(类似于调用delete方法)。 - (3) 外部函数中暂时还不支持使用多维数组(但在public函数是支持的)。
(4)数组成员
length
数组的成员变量length表示当前数组的长度。
- (1)动态数组可以在storage中通过改变成员变量 .length 改变数组大小(在memory中是不可以的)。
- (2)并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度。
- (3)一经创建,memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。
- (4)如果你尝试改变一个不在storage中的非动态数组的大小,你将会收到一个“Value must be an Ivalue”的错误。
push
storage的动态数组以及 bytes类型(字节数组)都有一个叫做 push 的成员函数,它用来添加新的元素到数组末尾。 这个函数将返回新的数组长度。
注意:string即字节数组是没有push方法的。
pop
storage的动态数组和bytes数组(字节数组)都有一个叫做pop的成员函数,用于从数组的末尾删除元素。
其在删除元素的时候隐式地调用了delete方法。注意:string即字节数组是没有pop方法的。
(5)实例
pragma solidity >=0.4.16 <0.7.0; contract ArrayContract { uint[2**20] m_aLotOfIntegers; // 数组大小为2的20次方 // m_pairsOfFlags不是一对动态数组,而是一个数组元素为两个变量的动态数组(说白了就是其每个元素是一个长度为2的数组) bool[2][] m_pairsOfFlags; // 列数为2,行数为动态的 // newPairs是一个数组元素为两个bool类型变量的动态数组(其每个元素是一个包含两个bool变量的数组) function setAllFlagPairs(bool[2][] memory newPairs) public { // 将newPairs数组赋值给storage数组的m_pairsOfFlags,m_pairsOfFlags的值将会被newPairs中的值替换。 m_pairsOfFlags = newPairs; } struct StructType { uint[] contents; uint moreInfo; } StructType s; function f(uint[] memory c) public { // 将类型为StructType结构体变量s的指针(引用)赋值给g StructType storage g = s; // 改变结构体变量g中的成员属性值,其实也在改变s中的成员属性值(因为s和g指向同一块数据区域) g.moreInfo = 2; // 将c的值赋值给g.contents(虽然g.contents不是一个局部变量,但它是某个局部变量的一个成员) g.contents = c; } function setFlagPair(uint index, bool flagA, bool flagB) public { // 访问一个不存在数组下标会抛异常 m_pairsOfFlags[index][0] = flagA; // 将flagA赋值给第index行第0列的元素 m_pairsOfFlags[index][1] = flagB; // 将flagB赋值给第index行第1列的元素 } function changeFlagArraySize(uint newSize) public { // 如果所赋给的新长度值小于原数组长度值,则会把原数组在新长度之外的元素删除。 m_pairsOfFlags.length = newSize; } function clear() public { // 将数组清空 delete m_pairsOfFlags; delete m_aLotOfIntegers; // 与上面效果相同(清空数组) m_pairsOfFlags.length = 0; } bytes m_byteData; function byteArrays(bytes memory data) public { // 字节数组(bytes)是不一样的,因为它们不是填充式存储,但是它们可以被当作和uint8[]一样对待。 m_byteData = data; m_byteData.length += 7; m_byteData[3] = 0x08; delete m_byteData[2]; } function addFlag(bool[2] memory flag) public returns (uint) { return m_pairsOfFlags.push(flag); // 向二维动态数组添加新元素(这里添加的元素是一个长度为2的数组),给二维数组增加一行 } function createMemoryArray(uint size) public pure returns (bytes memory) { // 使用new关键字进行动态数组的创建 uint[2][] memory arrayOfPairs = new uint[2][](size); // 内联数组总是静态大小的,如果你只是使用字面量,则你必须提供至少一种类型。 arrayOfPairs[0] = [uint(1), 2]; // 创建一个动态数组 bytes memory b = new bytes(200); for (uint i = 0; i < b.length; i++) b[i] = byte(uint8(i)); return b; } }
参考:Array
9. 结构体(struct)
(1)定义
结构体用于自定义数据类型,结构体成员可以是任何数据类型,甚至可以是结构体本身。
- (1)结构体可用于函数返回值,但是要在智能合约内部调用,否则会抛出异常。
- (2)如果要返回结构体中成员的值,可以使用返回多个值的函数。
(2)实例
pragma solidity >=0.4.16 <= 0.7.0; contract StructContract_1{ // 定义结构体类型 struct Job{ uint id; string name; string company; } struct Person{ uint id; string name; uint age; Job job; // 结构体类型中引用结构体变量(结构体变量作为结构体类型的成员) } // Job public job; // 声明一个Person类型的变量 Person person; // 初始化结构体 // 方法一:按照结构体中命名参数进行初始化 Person personA = Person({ id:10002, name:"Kobe Bryant", age:39, job:Job({ //结构体中包含结构体 id:102, name:"Basketball Player", company:"NBA" }) }); // 方法二:按照结构体中定义的顺序初始化 Job jobA = Job(103,"NBA Retired Players","Home"); Person personB = Person(10003,"Dwyane Wade",36,Job(104,"LiNing Spokeman","LiNing")); Person personC = Person(10004,"Chris Bosh",35,jobA); //通过构造函数初始化结构体类型变量 constructor (uint personId,string memory name,uint age) public{ // 初始化结构体变量 Job memory job = Job({ id:101, name:"Software Engineer", company:"Google" }); person = Person({ id:personId, name:name, age:age, job:job }); } // 修改工作属性(修改结构体变量的值) function setJob(string memory jobName,string memory company) public{ // job.name = jobName; // job.company = company; person.job.name = jobName; person.job.company = company; // 重置为初始值,把struct中的所有变量的值设置为0,除了mapping类型 // delete person; //也须写在函数内部 } // 要用结构体当作返回值,必须将函数定义为internal,即合约内部可见(函数仅在合约内部可调用) // 必须在内部调用(需要使用internal声明函数),否则会抛出异常 function getPerson() internal view returns(Person memory){ return person; // 返回构造体类型的值 } // 获取人员的姓名、年龄、工作等信息(获取结构体的成员值) function callGetPerson() public returns(string memory,uint,string memory,string memory){ person = getPerson(); return (person.name,person.age,person.job.name,person.job.company); } } // 1,"Lebron James",34 "BasketBall Player","NBA"
10. 映射(mapping)
(1)定义
映射与字典类似,通过key获取对应的value值。
- key:可以是除了映射外的任何数据类型;
- value:任何数据类型;
mapping(keyType=>valueType) varName;
(2)实例
pragma solidity >=0.4.16 <=0.7.0; contract MappingContract{ //声明映射类型的变量names mapping(uint=>string) public names; // 定义Person结构体类型 struct Person{ string name; uint age; string job; } //声明映射类型的变量persons mapping(uint=>Person) public persons; // 通过合约的构造函数向映射变量names添加值 constructor (uint id,string memory name) public{ names[id] = name; //映射变量的赋值 } // 根据key值从映射类型变量中获取相应的value值 function getValue(uint id) public view returns(string memory){ return names[id]; } // 向映射类型变量中添加值 function addPerson(uint id,string memory name,uint age,string memory job) public{ // 先初始化结构体 Person memory person = Person({ name:name, age:age, job:job }); persons[id] = person; //增加一个person(向映射类型变量中添加值) } // 根据id(key)从persons映射获取Person对象,并通过返回多值函数返回Person结构体的成员 function getPerson(uint id) public view returns(string memory name,uint age,string memory job){ // 返回多个值 // 方法一:多返回值函数可以通过定义具体的函数返回值接收多个返回值,而不使用return关键字 name = persons[id].name; age = persons[id].age; job = persons[id].job; // 方法二:使用return关键字(多个返回值,需用括号括起来) // return (persons[id].name,persons[id].age,persons[id].job); } } // 测试数据 // 1001,"Lebron James" // 1002,"Dwyane Wade",36,"NBA Player" // 1003,"Kobe Bryant",39,"World Cup Spokeman"
实例中有提到两种不同的方式返回多个值
11.函数参数和函数返回值
(1)函数参数
在函数中,如果某个参数未使用,只需保留参数类型,参数名可以省略。
(2)函数返回值
函数返回值可以直接指定返回值类型,也可以为返回值指定变量名,声明返回值类型的方式与声明函数参数的方式相同,所以也可以将函数返回值称为函数输出和参数。
- 返回值类型要使用returns指定,多个返回值类型中间用逗号( , )分隔;
- 如果为函数返回值指定变量名,可以不使用return返回,直接为函数输出参数变量赋值即可。
返回多个值的两种方法
- 方法一: 函数可以通过设置多个具体的函数返回值变量接收多个返回值,而不使用return关键字,就可实现多个值的返回。
function getPerson(uint id) public view returns(string memory name,uint age,string memory job){ name = persons[id].name; age = persons[id].age; job = persons[id].job; } }
- 方法二: 使用return关键字(多个返回值,需用括号括起来)
function getPerson(uint id) public view returns(string memory, uint ,string memory){ return (persons[id].name,persons[id].age,persons[id].job); } }
12. 调用其他合约中的函数
(1)定义
当前合约中的函数调用其他合约中的函数的两个前提条件:
- (1)被调用函数所在的合约必须已经成功部署在以太坊网络上(或在本地的测试环境)。
- (2)需要知道被调用函数所在的合约的地址。
(2)实例
// CallOtherContract.sol pragma solidity >=0.4.16 <=0.7.0; /** 注意: (1)在部署FunCallContract之前,必须先部署FactorialContract合约,否则就无法获得FactorialContract的地址。 (2)部署完FactorialContract合约之后,将FactorialContract合约的地址作为FunCallContract合约的构造参数 传入FunCallContract合约,然后部署FunCallContract合约。 */ // 用于计算阶乘的合约 contract FactorialContract{ // 计算阶乘的函数 function getFactorial(uint n) public returns(uint){ if(n==0 || n==1){ return 1; } else{ return getFactorial(n-1)*n; } } } // 调用FactorialContract.getFactorial函数计算阶乘 contract FunCallContract{ FactorialContract factorial; //在构造函数中创建FactorialContract合约的实例, // 必须通过FunCallContract构造函数的参数指定FactorialContract合约的地址。 constructor(address addr) public{ factorial = FactorialContract(addr);//实例化合约实例的时候需要传入其合约的地址 } // 计算阶乘 function jiecheng(uint n) public returns(uint){ return factorial.getFactorial(n); } }
12. 通过new关键字创建合约对象
通过new关键字创建合约对象最大的优势:
不需要先部署被调用函数所在的合约,并先获取被调用函数所在合约的地址,然后才能部署调用函数的合约。换句话说就是,合约A调用合约B中的函数还需要先部署合约B是比较麻烦的。但是通过new关键字创建合约对象,则不需要部署合约B就可以调用B中的函数。
相对于上面CallOtherContract.sol的代码,只需将FunCallContract的构造函数
constructor(address addr) public{ factorial = FactorialContract(addr);//实例化合约实例的时候需要传入其合约的地址 }
修改为
// CallOtherContract_1.sol constructor() public{ // 通过new关键字创建合约对象(此时不需要传入该合约对象的合约地址) factorial = new FactorialContract(); }
其他不用变化。
这样使用new关键字创建合约对象,就不需要先部署FactorialContract合约,并获取其合约的地址后,然后才能部署FunCallContract合约,在其合约内部调用其FactorialContract合约中的函数。
这里可以直接部署FunCallContract合约。13. 函数的命名参数
在solidity语言中调用函数时可以指定命名参数,通过命名参数,可以不按被调用函数的参数的定义的顺序传入参数值。
pragma solidity >=0.4.16 <=0.7.0; // 命名参数的使用 contract NamedParameter{ function sub(int n1,int n2) public pure returns(int) { return n1-n2; } function fun() public pure returns(int){ // 通过函数的命名参数,可以不按被调用函数中的参数的定义顺序进行赋值 // 命名参数要通过{...}传递,有点类似于javascript中的对象 return sub({n2:66,n1:32}); } }
15. 函数多返回值解构和元组赋值
- (1)多返回值解构:如果函数返回多个值,可以支持将多个返回值分别赋给相应数目的变量。
- (2)元组赋值:指赋值运算符(=)左侧和右侧都有n个变量。
pragma solidity >=0.4.24 <=0.7.0; //注意:只有0.4.24及以上版本才支持多返回值解构和元组赋值 contract AssignmentContract{ uint[] data; function mulValueFun() public pure returns(uint,bool,uint){ return (2018,true,2019); } function assignment() public returns(uint xx,uint yy,bool bb,uint length){ // 多返回值解构赋值,x、b和y分别等于mulValueFun函数的3个返回值 (uint x,bool b,uint y) = mulValueFun(); // 交换x和y的值 (x,y)=(y,x); //元组赋值 // 这里只指定了一个变量(data.length),所以mulValueFun函数的其他返回值会被忽略 (data.length,,) = mulValueFun(); //未指定的变量,通过逗号(,)将位置留着 // 重新设置y变量的值 y = 123; // 设置返回值 xx = x; yy = y; bb = b; length = data.length; } }
16. 变量声明和作用域
(1)0.5.0版本之前
在Solidity 0.5.0之前,Solidity语言的作用域规则继承自JavaScript。
在if、while、for循环中定义的变量仍然作用于{...}外面,也就是说 {...}中声明的变量,在 {...}外仍然可以使用。
换句话说,就是无论{..}内还是{...}外,都不能有同名的变量。(2)0.5.0版本之后
在Solidity 0.5.0之后, 开始支持声明块({...})变量,也就是在 {...}中声明的变量只在{...}中有效,这就意味着在多个{...}中可以声明多个同名的变量。
17. 错误处理
Solidity语言有3种与错误处理相关的函数:
- (1)require:用于校检外部输入,如函数的参数、调用外部函数的返回值等。
- (2)assert:用于校检合约的内部错误。
- (3)revert:抛出错误。
Solidity语言的错误处理与数据库中的事务回滚类似,一旦发生错误,以前做的所有操作都将回滚,因为合约很可能涉及到转账等敏感操作,所以一旦有任何异常,必须全部恢复到最初的状态,以避免数据不一致的情况发生。
18. 全局变量
(1)block变量
pragma solidity >=0.4.20 <=0.7.0; contract BlockContract{ function getBlockInfo() public view returns(address coinbase,uint difficulty, uint gaslimit,uint number,uint timestamp){ coinbase = block.coinbase; //获取挖出当前区块的矿工的地址; difficulty = block.difficulty; //获取当前区块的挖矿难度; gaslimit = block.gaslimit; //获取当前区块的gas限制; number = block.number; //获取当前区块的编号 timestamp = block.timestamp; //获取当前区块的时间戳(从Unix epoch即Unix纪元,从1970年1月1日开始) } }
(2)msg变量
- (1)执行函数包含参数:
pragma solidity >=0.4.20 <=0.7.0; contract MsgContract{ // 获取相关的系统信息 function getMsgInfo(uint x) public payable returns(bytes memory data,uint gas,address sender,bytes4 sig,uint value){ data = msg.data; //获取当前执行函数的调用数据(包含函数标识,即sha3散列值的前8位,若执行函数有参数,则还包含参数值) // gas = msg.gas; // msg.gas已经被gasleft()函数取代 gas = gasleft(); // 获取剩余的gas sender = msg.sender; // 获取当前执行函数的调用地址 sig = msg.sig; // 获取当前执行函数的标识(sha3散列值的前8位) value = msg.value; // 当前被发送的wei的数量(使用该属性的函数要使用payable关键字修饰) } }
结果:
- (2)执行函数不包含参数:
把上述合约函数中的getMsgInfo(uint x)修改为getMsgInfo( ), 即去掉函数的参数。
结果:- msg.data表示当前执行函数的调用数据,包含函数标识(即sha3散列值的前8位)。如果执行函数包含参数,则其还包含参数值。
- msg.sig表示当前执行函数的标识(即sha3散列值的前8位)。
- 换句话说,如果执行函数不包含参数,则msg.data(只包含函数标识)与msg.sig(函数标识)是一样的。
例如,若当前执行的函数是getMsgInfo( ),那么可以使用下面的Node.js代码获取该函数sha3散列值的前8位。该值与msg.data属性返回的值相同(即都是只包含函数标识)。
var Web3 = require('Web3'); web3 = new Web3( ); // 由于sha3函数返回的值前两位是表示十六进制的0x,所以从第3个字符开始截取,截取的长度为8位 sign = web3.sha3("getMsgInfo( )").substr(2,8); console.log(sign); //输出 4c668374
(3)其他全局变量
pragma solidity >=0.4.22 <=0.7.0; // 其他全局变量 contract OtherGlobalContract{ // 获取其他全局变量的值 function getOtherGlobal() public view returns(bytes32 hash,uint nowTime,uint gasPrice,address origin){ // 获取指定区块的哈希值(要传入区块号) hash = blockhash(1001); // 获取当前区块的时间戳(与block.timestamp属性返回的值相同) nowTime = now; // 获取交易的gas价格 gasPrice = tx.gasprice; // 获取发送交易的地址 origin = tx.origin; } }
19. 自定义修饰符(modifier)
modifier常用于在函数执行前检查某种前置条件是否满足,modifier是一种合约属性,可以被继承(子合约可以使用父合约中定义的modifier),同时还可被派生的合约重写(override)。
modifier modiferName{ //校检代码 _; }
校检代码用于校检使用自定义修饰符的函数,后面必须跟一个下划线(_),而且下划线后面跟分号( ; )。如果通过校检,将使用该定义修饰符的函数的函数体插入到下划线的位置。也可以认为自定义修饰符其实就是多个函数相同代码的抽象,除了校检代码。
pragma solidity >=0.4.20 <0.7.0; contract OwnerContract{ address owner; // 保存部署合约的账号 constructor() public{ owner = msg.sender; } // 定义用于检测msg.sender是否为部署合约的账号,如果不是,终止执行函数 modifier onlyOwner{ require(msg.sender == owner,"Only owner can call this function."); _; // 如果校检通过,会将使用onlyOwner函数的函数体插到这个位置。 } // 校检地址是否可以为空 // 当输入的_address为0x0000000000000000000000000000000000000000(0x后40个0),会抛出“_address can not be 0!” modifier notNull(address _address){ require(_address != address(0),"_address can not be 0!"); _; } // 一个函数可以有多个修饰符,多个修饰符之间用空格或回车分隔,修饰符的生效顺序与定义顺序是一样的 // 修改合约所有者 function changeOwner(address newOwner) notNull(newOwner) onlyOwner() public{ owner = newOwner; } } //从OwnerContract继承 contract AddContract is OwnerContract{ // 使用onlyOwner修饰函数 function add(uint m,uint n) public view onlyOwner() returns(uint){ return m+n; } } contract RestrictContract{ uint public mm; uint public nn; // 用于校检 m是否大于或等于n,如果不满足条件,相当于将使用restrict1函数的函数体删除 modifier restrict1(uint m,uint n){ if(m>=n){ //如果不满足条件,相当于将使用restrict1函数的函数体删除 _; } } // 除了校检m是否大于n外,还将m和n分别保存在mm和nn变量中 modifier restrict2(uint m,uint n){ require(m>=n,"m can not less than n"); mm = m; nn = n; _; } } // 从RestrictContract合约继承 contract SubContract is RestrictContract{ // 使用restrict1修饰sub1函数 function sub1(uint m,uint n) public pure restrict1(m,n) returns(uint){ return m-n; } // 使用restrict2修饰sub2函数 function sub2(uint m,uint n) public restrict2(m,n) returns(uint){ return m-n; } }
20. pure和view
(1)pure
使用pure关键字修饰的函数不允许读写 状态变量,否则会编译出错。
下面几种情况会被认为是读写状态变量,在这些情况下,用pure关键字修饰函数就会编译错误:- (1)直接读取状态变量;
- (2)访问 this.balance或 <address>.balance;
- (3)访问任何block、tx、msg变量中的成员,但msg.sig和msg.data除外。
- (4)调用任何没有使用pure修饰的函数,哪怕是这个函数中确实没有读写任何状态变量。
- (5)内嵌用于操作状态变量的汇编代码的函数。
(2)view
使用view关键字修饰函数时,表示该函数不会修改状态变量。
下面几种情况表明函数会修改合约的状态变:- (1)只写修改状态变量;
- (2)触发事件;
- (3)创建其他合约的实例;
- (4)调用selfdestruct函数销毁合约;
- (5)通过call函数方发送以太币;
- (6)调用任何未标记view或pure函数;
- (7)使用底层的call函数;
- (8)内嵌用于操作状态变量的汇编代码的函数;
需要注意的是:用view修饰的函数并不会阻止函数中修改状态变量,只是在用view修饰的函数中修改状态变量会出现警告。(不报错,只出现警告)
21. fallback函数(回调函数)*
fallback函数:一个没有函数名、参数和返回值的函数。必须用external进行修饰。
在下面两种情况下会调用fallback函数:- (1) 合约中没有匹配的函数标识。
换句话说,就是
- 该合约没有其他函数;
- 调用合约时,如果没有匹配上该合约中的任何一个函数,就会调用回调函数。
- (2) 合约接收到以太币(交易中没有附带任何其他数据),也会调用回调函数。
注意:
- 这种情况下,fallback函数要使用payable关键字修饰,否则给包含fallback函数的合约发送以太币时会出现编译错误。
- 即使 fallback 函数不能有参数,仍然可以使用 msg.data 来获取随调用提供的任何有效数据。
另外,还需注意以下几点:
- (1) 如果调用者想调用一个不存在的函数,fallback函数将会被执行。
- (2) 如果你只想为了接收以太币而实现fallback函数,你需要增加一些校检(如 require(msg.data.length == 0 ) )去避免一些无效的调用。
- (3) 一个没有定义fallback函数(回调函数)的合约直接接收以太币(没有函数调用,如使用send或transfer),则会抛出一个异常,并返还以太币(有些行为在Solidity V0.4.0之前有些不同)。因此如果你要使你的合约接收以太币,你就必须实现一个被payable修饰的fallback函数。
一个没有 payable fallback 函数的合约,可以作为 coinbase transaction (又名 miner block reward )的接收者或者作为 selfdestruct 的目标来接收以太币。
pragma solidity >=0.5.0 <=0.7.0; contract Test{ uint x; // (1)给这个合约发送任何消息都会调用这个函数(因为合约没有其他函数) // 定义一个fallback函数,在该函数中设置了状态变量x。 // (2)向这个合约发送以太币将会抛出一个异常,因为这个回调函数没有用“payable”修饰符修饰。 function() external{ x=101; } } contract Sink{ // 定义了一个fallback函数,该函数使用payable修饰,表明可以接受其他地址发过来的以太币。 function() external payable{ } } contract Caller{ function callTest(Test test) public returns(bool){ // 这里调用一个不存在的函数,由于匹配不到函数,所以将调用Test合约中的回调函数。 (bool success,) = address(test).call(abi.encodeWithSignature("nonExitingFunction()")); require(success); // address(test)不允许直接调用“send”方法,因为“test”没有被“payable”修饰的回调函数。 // 其必须通过“uint160”进行一个中间转换,然后再转换为“address payable”类型才能调用“send”方法。 address payable testPayable = address(uint160(address(test))); // 如果某人发送以太币给那个合约,这笔交易将会失败(例如,这里将会返回false) return testPayable.send(2 ether); } function callSink(address payable sinkAddress) public returns(bool){ Sink sink = Sink(sinkAddress); // 如果向Sink合约发送以太币时发送成功,Sink中的fallback函数会被调用 return address(sink).send(5 ether); } }
22. 函数重载
(1)定义
函数重载是指一个合约中定义了多个函数名相同,但参数个数和类型不同的函数。(不考虑返回值)
需要注意的是:
如果函数参数类型是可以转换的,例如合约和address,Solidity编译器就会认为它们是同一个数据类型,因此会产生编译错误。(2)实例
pragma solidity >=0.4.20 <=0.7.0; // 拥有4个同名的重载函数 contract OverloadContract1{ // 拥有2个uint类型的参数 function add(uint m,uint n) public pure returns(uint){ return m+n; } // 没有参数 function add() public pure returns(uint){ return 11+22; } // 有一个bool类型参数 function add(bool b) public pure returns(bool){ return b; } // 有3个uint类型的参数 function add(uint l,uint m,uint n) public pure returns(uint){ return l+m+n; } } contract A{ } // 从表面上看第一个和第二个test函数的参数不一样,其实是一样的。因为合约A本身就是一个address类型 // 所以OverloadContract2合约编译会失败,因为前两个test函数无法实现函数重载 contract OverloadContract2{ // 函数重载失败 function test(address addr) public view returns(uint){ return addr.balance; } // 函数重载失败,具体报错:Function overload clash during conversion to external types for arguments. // function test(A a) public view returns(uint){ // return address(a).balance; // } // 函数重载成功 function test(A a,uint b) public view returns(uint,uint){ return (address(a).balance,b); } }
23. 事件(event)
(1)定义
如果将合约部署在TestRPC环境或者以太坊网络上,在执行以太坊函数时是无法直接获得函数的返回值的,但是可以通过事件将计算结果返回给客户端。
event EventName( typeName parameter,... );
(2)实例
pragma solidity >=0.4.20 <=0.7.0; contract EventContract{ // 定义MyEvent事件 event MyEvent( uint m, uint n, uint results ); function add(uint m,uint n) public returns(uint){ uint results = m+n; // 使用emit指令触发MyEvent事件,并通过事件参数传递m、n和m+n的计算结果(传递到客户端) emit MyEvent(m,n,results); return results; } }
24. 合约继承
合约继承,使用is关键字指定父合约。
- (1) Solidity合约支持多继承,如果要指定多个合约,合约之间用逗号( , )分隔。
- (2) 尽管可以指定多个父合约,但是只会创建一个合约实例,将其他父合约中的代码复制到这个合约实例中。
- (3) 如果多个父合约实现了同样的函数,那么以最后一个父合约的函数为准。
25. 合约构造函数
- (1)老版本的solidity语言中,合约的构造函数与普通函数类似,只是函数名与合约名相同。
- (2)新版本的solidity语言中,使用constructor作为构造函数的名字。
这样做的好处是,一旦改变了合约的名字,也不用修改其构造函数的名字。
- (3)合约构造函数允许使用public或internal修饰。
pragma solidity >=0.4.20 <=0.7.0; contract Contract1{ uint public a; // 带参数的构造函数,假设用internal修饰 constructor(uint _a) internal{ a = _a; //用来初始化状态变量 } } // 从Contract1继承,并将构造函数重新用public修饰,变成外部可访问的构造函数。 // 由于Contract1合约的构造函数有一个参数,所以在继承时需要指定Contract1合约构造函数的参数值。 contract Contract2 is Contract1(100){ constructor() public{ } } contract Contract3 is Contract1{ uint aa; uint bb; // 如果构造参数的参数需要用某些变量设置,如构造函数的参数,可以在构造函数后面指定父合约构造函数的参数值 constructor(uint _a,uint _b) Contract1(_a*_b) public{ aa = _a; bb = _b; } }
26. 抽象合约
抽象合约: 至少有一个函数没有实现的合约。
如果合约从一个抽象合约继承,而且没有全部实现抽象合约中的函数,那么这个合约就会继承这些未实现的函数,所以这个合约也是抽象合约。(说白了,就是这个合约继承了一个抽象合约,但是还有些继承自抽象合约的函数没有实现,于是这个合约也就有了一些函数没有实现,所以这个合约也就是抽象合约了。)
抽象合约通常来实现多态,也就是用抽象合约的多个子合约创建多个实例,将这些实例赋给抽象合约类型的变量。
由于这些子合约都实现了抽象合约中的函数,所以调用抽象合约中的函数会根据抽象合约类型变量的值不同,调用结果也不同,这就是称为多态。(调用同一个函数,会有多种不同表现形态)pragma solidity >=0.5.0 <=0.7.0; /** 在MyContract合约中的test1和test2函数中分别创建了 MyContract1和MyContract2的实例, 且将这两个合约的实例都赋值给了AbstractContract类型(抽象合约类型)的变量。 在test1和test2函数中都调用了AbstractContract合约(父合约)中的add函数,且输入相同的实参值, 不过返回结果却不一样,这就是多态。 实际上,本质上调用的是MyContract1(子合约)和MyContract2合约(子合约)中的add函数。 */ contract AbstractContract{ // add函数没有实现 function add(uint m,uint n) public returns(uint); // 完整实现了sub函数 function sub(int m,int n) public pure returns(int){ return m-n; } } // 该合约从AbstractContract继承(即MyContract1是AbstractContract的一个子合约) contract MyContract1 is AbstractContract{ // 实现了抽象合约中的add函数 function add(uint m,uint n) public returns(uint){ return m+n; } } // 该合约从AbstractContract继承(即MyContract2是AbstractContract的另一个子合约) contract MyContract2 is AbstractContract{ // 实现了抽象合约中的add函数 function add(uint m,uint n) public returns(uint){ return 4*(m+n); //不同于MyContract1中add函数的实现 } } // 该合约从MyContract1 继承,即继承了add函数和sub函数 contract MyContract is MyContract1{ function test1(uint m,uint n) public returns(uint){ // 创建MyContract1 合约的实例 AbstractContract abstractContract = new MyContract1(); // 实际是调用了MyContract1 合约中的add函数 return abstractContract.add(m,n); } function test2(uint m,uint n) public returns(uint){ // 创建MyContract2 合约的实例 AbstractContract abstractContract = new MyContract2(); // 实际是调用了MyContract1 合约中的add函数 return abstractContract.add(m,n); } }
27. 接口
接口与抽象合约类似,但是不能实现任何函数。(即所有接口中的方法都是未实现的)
此外,接口还有如下限制:- (1)不能继承其他合约或接口;
- (2)不能定义构造函数;
- (3)不能定义变量;
- (4)不能定义结构体;
- (5)不能定义枚举类型。
interface interfaceName{ //抽象方法(未被实现的方法) }
注意:
(1)接口应该定义在合约的外部(与合约是同一等级);
(2)接口中定义的方法必须被external修饰;合约实现接口的方法与继承合约或抽象合约的方法类似, 使用is关键字.
pragma solidity >=0.5.0 <=0.7.0; // 定义接口(定义在合约外面) interface MyInterface{ function add(uint m,uint n) external returns(uint); function sub(int m,int n) external returns(int); } // InterfaceContract实现了MyInterface contract InterfaceContract is MyInterface{ function add(uint m,uint n) public returns(uint){ return m+n; } function sub(int m,int n) public returns(int){ return m-n; } }
28. gas limit和gas price
(1)gas limit
- (1) gas limit 表示完成转账交易最大消耗的gas数,如果超过这个gas数,交易就会失败,整个交易过程都会回滚。
- (2) gas limit 主要是为了防止由于发布交易消耗过多的gas。
(2)gas price
- 表示你愿意为单位gas支付的费用,以gwei为单位表示。
1 gwei = 10^9 wei
(3)两者的作用
- (1)在交易中gasPrice是由发起交易人来决定的,每个矿工接收到交易请求之后,会根据gasPrice的高低来决定是否要打包进区块。
- (2)每个交易中必须包含gas limit和gas price的值。gas limit代表了这个交易执行过程中最多被允许消耗的gas数量。
- (3)gas limit和gas price 代表着交易发送者愿意为执行交易支付的wei的最大值。
付款金额(单位 wei)= Gas数量 × GasPrice
- (4)交易执行完成后,如果实际消耗的gas小于gaslimit,那么剩余的gas会以Ether的方式返回给交易发起者。
- (5)如果在交易过程中,实际消耗的gas大于gas limit,那么就会出现“gas不足”的错误,这种情况下交易会被终止,交易之前的所有修改的状态会被回滚,同时在交易执行过程中所消耗的gas是不会回退给交易发起者的。
Solidity学习笔记
最新推荐文章于 2023-10-20 23:33:34 发布