Solidity是一门静态类型语言,支持继承、库和复杂的用户自定义类型等特性。
(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;
}
}
int8 public c = -2;
uint8 public d = uint8(c);
// 此时的d为254
uint public e = uint(c);
// e = 115792089237316195423570985008687907853269984665640564039457584007913129639934
transfer从合约发起方向某个地址转入以太币,当地址无效或者发起方余额不足时,transfer将抛出异常。
// 向addressA转入一个以太币
addressA.transfer(1 ether);
// or // 附带 gas
addressA.transfer.gas(120000)(1 ether);
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。
固定长度字节数组是以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。
bytes:动态长度字节数组
一种特殊的数组。bytes类似于byte[],在外部函数作为参数时,会进行压缩打包以便节省空间,所以尽量用bytes。
bytes m;
string:动态长度字符数串
// 字符串是双引号
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类型间接将两个或多个字符串连接起来。
字符串连接的基本原理:
// 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);
}
枚举是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等。
//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;
}
(1) 如果不为函数指定访问权限,默认为public。
(2) 合约的状态变量不能用external修饰,否则无法编译通过。
(3) 如果不为状态变量指定访问权限,默认为internal。
4类可见性(访问权限) 指定:
用public声明的状态变量,会自动产生一个getter函数。
(1)external函数不能直接调用,前面需要加this, 如this.func( )。
(2)在接收大量数据时,external函数有时更有效率。
如果合约的状态变量使用public修饰,Solidity编译器会自动为状态变量生成一个与状态变量同名的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);
}
}
solidity中数组与大多数语言稍有不同。具体如下:
// 定义一个j行i列的二维数组(注意:定义的时候列在前面,行在后面)
int[i][j] arrayName;
// 为数组arrayName的第m行第n列元素赋值,赋值为20.( 注意:赋值的时候,行在前面,列在后面)
arrayName[m][n] = 20;
- (1)定义的时候列在前面,行在后面;
- (2)赋值的时候,行在前面,列在后面;
length
数组的成员变量length表示当前数组的长度。
push
storage的动态数组以及 bytes类型(字节数组)都有一个叫做 push 的成员函数,它用来添加新的元素到数组末尾。 这个函数将返回新的数组长度。
注意:string即字节数组是没有push方法的。
pop
storage的动态数组和bytes数组(字节数组)都有一个叫做pop的成员函数,用于从数组的末尾删除元素。
其在删除元素的时候隐式地调用了delete方法。
注意:string即字节数组是没有pop方法的。
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
结构体用于自定义数据类型,结构体成员可以是任何数据类型,甚至可以是结构体本身。
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"
映射与字典类似,通过key获取对应的value值。
mapping(keyType=>valueType) varName;
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"
实例中有提到两种不同的方式返回多个值
在函数中,如果某个参数未使用,只需保留参数类型,参数名可以省略。
函数返回值可以直接指定返回值类型,也可以为返回值指定变量名,声明返回值类型的方式与声明函数参数的方式相同,所以也可以将函数返回值称为函数输出和参数。
返回多个值的两种方法
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;
}
}
function getPerson(uint id) public view returns(string memory, uint ,string memory){
return (persons[id].name,persons[id].age,persons[id].job);
}
}
当前合约中的函数调用其他合约中的函数的两个前提条件:
// 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);
}
}
通过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合约。
在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});
}
}
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;
}
}
在Solidity 0.5.0之前,Solidity语言的作用域规则继承自JavaScript。
在if、while、for循环中定义的变量仍然作用于{...}外面,也就是说 {...}中声明的变量,在 {...}外仍然可以使用。
换句话说,就是无论{..}内还是{...}外,都不能有同名的变量。
在Solidity 0.5.0之后, 开始支持声明块({...})变量,也就是在 {...}中声明的变量只在{...}中有效,这就意味着在多个{...}中可以声明多个同名的变量。
Solidity语言有3种与错误处理相关的函数:
Solidity语言的错误处理与数据库中的事务回滚类似,一旦发生错误,以前做的所有操作都将回滚,因为合约很可能涉及到转账等敏感操作,所以一旦有任何异常,必须全部恢复到最初的状态,以避免数据不一致的情况发生。
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日开始)
}
}
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关键字修饰)
}
}
把上述合约函数中的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
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;
}
}
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;
}
}
使用pure关键字修饰的函数不允许读写 状态变量,否则会编译出错。
下面几种情况会被认为是读写状态变量,在这些情况下,用pure关键字修饰函数就会编译错误:
使用view关键字修饰函数时,表示该函数不会修改状态变量。
下面几种情况表明函数会修改合约的状态变:
需要注意的是:用view修饰的函数并不会阻止函数中修改状态变量,只是在用view修饰的函数中修改状态变量会出现警告。(不报错,只出现警告)
fallback函数:一个没有函数名、参数和返回值的函数。必须用external进行修饰。
在下面两种情况下会调用fallback函数:
换句话说,就是
- 该合约没有其他函数;
- 调用合约时,如果没有匹配上该合约中的任何一个函数,就会调用回调函数。
注意:
- 这种情况下,fallback函数要使用payable关键字修饰,否则给包含fallback函数的合约发送以太币时会出现编译错误。
- 即使 fallback 函数不能有参数,仍然可以使用 msg.data 来获取随调用提供的任何有效数据。
另外,还需注意以下几点:
一个没有 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);
}
}
参考:Fallback Function
函数重载是指一个合约中定义了多个函数名相同,但参数个数和类型不同的函数。(不考虑返回值)
需要注意的是:
如果函数参数类型是可以转换的,例如合约和address,Solidity编译器就会认为它们是同一个数据类型,因此会产生编译错误。
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);
}
}
如果将合约部署在TestRPC环境或者以太坊网络上,在执行以太坊函数时是无法直接获得函数的返回值的,但是可以通过事件将计算结果返回给客户端。
event EventName( typeName parameter,... );
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;
}
}
合约继承,使用is关键字指定父合约。
这样做的好处是,一旦改变了合约的名字,也不用修改其构造函数的名字。
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;
}
}
抽象合约: 至少有一个函数没有实现的合约。
如果合约从一个抽象合约继承,而且没有全部实现抽象合约中的函数,那么这个合约就会继承这些未实现的函数,所以这个合约也是抽象合约。(说白了,就是这个合约继承了一个抽象合约,但是还有些继承自抽象合约的函数没有实现,于是这个合约也就有了一些函数没有实现,所以这个合约也就是抽象合约了。)
抽象合约通常来实现多态,也就是用抽象合约的多个子合约创建多个实例,将这些实例赋给抽象合约类型的变量。
由于这些子合约都实现了抽象合约中的函数,所以调用抽象合约中的函数会根据抽象合约类型变量的值不同,调用结果也不同,这就是称为多态。(调用同一个函数,会有多种不同表现形态)
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);
}
}
接口与抽象合约类似,但是不能实现任何函数。(即所有接口中的方法都是未实现的)
此外,接口还有如下限制:
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;
}
}
1 gwei = 10^9 wei
付款金额(单位 wei)= Gas数量 × GasPrice