if语句
同c语言if
pragma solidity ^0.8.4;
contract Cis {
function example(uint x) external pure returns (uint) {
if ( x < 10) {
return 1;
} else if (x>20){
return 2;
}else {
return 3;
}
}
}
三元运算符
同c语言
(x<10)? 1:2;
使用soiidity进行插入排序
solidity
中最常用的变量类型是uint
,也就是正整数,取到负值的话,会报underflow
错误。而在插入算法中,变量j
有可能会取到-1
,引起报错。
这里,我们需要把j
加1,让它无法取到负值。正确代码:
// 插入排序 正确版
function insertionSort(uint[] memory a) public pure returns(uint[] memory) {
// note that uint can not take negative value
for (uint i = 1;i < a.length;i++){
uint temp = a[i];
uint j=i;
while( (j >= 1) && (temp < a[j-1])){
a[j] = a[j-1];
j--;
}
a[j] = temp;
}
return(a);
}
循环
for循环
同c语言
pragma solidity ^0.8.4;
contract XUN {
function loops() external pure {
for (uint i=0;i<10;i++) {
continue ;//或者break
}
}
}
while循环
同c语言
uint j=0;
while (j<10) {
j++;
}
}
报错控制
报错控制有三种:require,revert,assert。具有退还与状态回滚的特性,在8.0版本上新自定义错误的功能,这个功能具有save gas特性。
require:
function testRequire(uint _i) public pure {
require(_i <=10, "bao cuo xin xi");//只有式子为真才能执行后面的代码,不为真显示报错信息
}
revert:
不能包含表达式的,只能通过条件语句进行判断,为真执行revert报错语句;
function testRevert(uint _i) public pure {
if (_i>10){
revert("bao cuo xin xi");//可以嵌套多层的判断语句
}
}
assert:
断言,可以包含表达式,但是没有报错语句,只能进行断言判断的作用
uint public num = 123;
function testAssert() public view {
assert(num==123);
}
function foo() public {
num+=1;//这样num就不等于123,assert会报错
}
回混:
会判断_i是否是小于10,如果不小于10,前面运行的代码数据也会回混,前面的状态不会更改,所花费的费用也会退还。
uint public num = 123;
function testAssert() public view {
assert(num==123);
}
function foo(uint _i) public {
num+=1;
require( _i<10);//数据回混
}
自定义错误:
防止很长的字符串浪费费用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 声明一个自定义错误类型 MyError
error MyError(address caller, uint value);
contract MyContract {
function testError1(uint t) public view {
if (t > 10) {
// 使用自定义错误类型 MyError 并传递正确的参数
revert MyError(msg.sender, t); // 使用msg.sender作为调用者地址,t作为值
}
// 如果 t <= 10,则函数正常结束,不会触发 revert
}
}
映射(mapping)
声明映射的格式为mapping(_KeyType => _ValueType)
,其中_KeyType
和_ValueType
分别是Key
和Value
的变量类型。例子:
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址
映射的定义
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Mapping {
mapping(address=> uint) public balances;
mapping(address => mapping(address => bool))public isFriend;
}
映射的规则
- 规则1:映射的
_KeyType
只能选择solidity
默认的类型,比如uint
,address
等,不能用自定义的结构体。而_ValueType
可以使用自定义的类型。下面这个例子会报错,因为_KeyType
使用了我们自定义的结构体:
// 我们定义一个结构体 Struct
struct Student{
uint256 id;
uint256 score;
}
mapping(Student => uint) public testVar;
-
规则2:映射的存储位置必须是
storage
,因此可以用于合约的状态变量,函数中的storage
变量,和library函数的参数(见例子)。不能用于public
函数的参数或返回结果中,因为mapping
记录的是一种关系 (key - value pair)。 -
规则3:如果映射声明为
public
,那么solidity
会自动给你创建一个getter
函数,可以通过Key
来查询对应的Value
。 -
规则4:给映射新增的键值对的语法为
_Var[_Key] = _Value
,其中_Var
是映射变量名,_Key
和_Value
对应新增的键值对。例子:
function writeMap (uint _Key, address _Value) public{
idToAddress[_Key] = _Value;
}
映射的原理
-
原理1: 映射不储存任何键(
Key
)的资讯,也没有length的资讯。 -
原理2: 映射使用
keccak256(key)
当成offset存取value。 -
原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(
Value
)的键(Key
)初始值都是各个type的默认值,如uint的默认值是0。
映射的基本操作
function exampla() external {
balances[msg.sender] =123;//代表调用者有了123的余额
uint bal = balances[msg.sender];//获取余额
uint bal2 = balances[address(1)];//不存在就返回uint默认值(整数0)
balances[msg.sender]+=456;//123+456=579
delete balances[msg.sender];//删除数据并不会影响长度,只会回复到默认值(0)
}
映射的嵌套
isFriend[msg.sender][address(this)]=true;//因为有2个主键所以赋值两次,第一个是调用者,第二个是当前合约的地址
映射迭代
映射不能遍历,但是映射迭代就是既可以查找又可以遍历
contract Mapping {
mapping(address=> uint) public balances;
mapping(address => bool) public inserted;
address[] public keys; //所有存在的地址
function set(address _key,uint _val) external {
balances[_key] = _val;
//查找有没有想要的地址,没有则添加上
if (!inserted[_key]){
inserted[_key] = ture;
keys.push(_key);
}
}
function getSize() external view returns (uint){
return keys.length;
}
function first() external view returns (uint ) {
return balances[keys[0]];//返回第0个元素的余额
}
function last() external view returns (uint ) {
return balances[keys[key.length -1]];//最后元素返回到映射中的主键
}
function get(i) external view returns (uint ) {
return balances[keys[i]];//第i个元素返回到映射中的主键
}
}
枚举
枚举的定义
contract Enum{
enum Status{
None,
Pending,
Shipped
}
Status public status;//用枚举定义一个类型
枚举的操作
//操作枚举
function get() view returns (Status) {
return status;//返回当前状态所在位置的索引,并不会返回字符串
}
function set(Status _status) external {
status = _status;//修改枚举变量状态值的操作
}
function ship() external {
status =Status.Shipped;//状态的值修改为状态类型地址的值
}
function reset() external {
delete status;//恢复枚举的默认值
}
部署合约
//代理合约
contract TestContractl {
address public owner = msg.sender;
function setOwner(address owner) public{
require(msg.sender == owner, "not owner");
owner = _owner;
}
}
contract TestContract2 {
address public owner = msg.sender;
uint public value = msg.value;
uint public x;
uint public y;
constructor(uint _x, uint _y) payable {
x=x;
y=_y;
}
}
contract Proxy {
function deploy(bytes memory _code) external payable {
new TeştContract1();//在部署方法中采用new方法就能部署合约
//如果想部署测试合约2就要重新部署代理合约
}
}
未完待续
存储位置
solidity存储位置有3个,storage,memory,calldata。
storage:
存储在这上面的是状态变量。
memory:
存储的是局部变量。
calldata:
与memory类似,只能用在参数中,可以节约gas。
参考代码:
contract DataLocations {
struct MyStruct {
uint foo;
string text;
}
mapping(address => MyStruct) public myStructs;
function examples(uint[] calldata y,string calldata s) external returns (MyStruct memory) {//在函数输入参数中如果定义数组类型就要定义stotage或者calldata
myStructs[msg.sender] = MyStruct({foo: 123, text: "bar"});
MyStruct storage myStruct = myStructs[msg.sender];//将状态变量读取到myStructs变量中,这样就可以进行读取和写入操作,状态变量的值也会发生变化
myStruct.text="foo";//状态变量的值被更改
MyStruct memory readOnly = myStructs[msg.sender];//更改内存位置,也能更改存储位置,但是函数调用结束就消失,并不会记录在链上
readOnly.foo = 456;//只读的变量值也能修改,不能修改链上的状态
_internal(y);//调用函数,可以传递参数
uint[] memory memArr = new uint[](3);
memArr[0] =234;
return memArr;
}
function _internal(uint[] calldata y) private partial {
uint x=y[0];
}
}
通过代码的存储实例:
contract SimpleStorage{
string public text;
function set(string calldata _text) external {//输入函数
text = _text;
}
function get() external view returns (string memory){//设置view是这个函数要调取状态变量的值
return text;//智能合约将状态变量拷贝到内存中然后返回过来
}
}
事件
是一种记录当前智能合约运行状态的方法。它并不记录在状态变量中而是在区块链浏览器上或者提现在交易记录的log,事件可以查询改变过的状态。
Solidity
中的事件(event
)是EVM
上日志的抽象,它具有两个特点:
- 响应:应用程序(ethers.js)可以通过
RPC
接口订阅和监听这些事件,并在前端做响应。 - 经济:事件是
EVM
上比较经济的存储数据的方式,每个大概消耗2,000gas
;相比之下,链上存储一个新变量至少需要20,000gas
。
事件的声明
contract Event {
event Log(string message,uint val);//()内是事件要报告的数据类型
event IndexedLog(address indexed sender, uint val );
//可以在链外进行搜索查询,被标记为indexed可以通过索引参数的值来高效地查询特定的事件,有索引的变量不能超过3个,他们会保存在以太坊虚拟机日志的topics中,方便之后检索。
function example() external {//没有写入和读取,也不能用view和pure,改变了链上(事件)的状态
emit Log("foo",1234);//事件触发
emit IndexedLog(msg.sender,789 );
//函数在调用会触发事件,事件会汇报到Log,提现在区块链浏览器
事件的使用
//用法
event Message(address indexed _from,address indexed _to,string message);
function sendMessage(address _to,string calldata message) external {
emit message(msg.sender,_to,message);
}
}
}
EVM日志 Log
以太坊虚拟机(EVM)用日志Log
来存储Solidity
事件,每条日志记录都包含主题topics
和数据data
两部分。
主题 Topics
日志的第一部分是主题数组,用于描述事件,长度不能超过4
。它的第一个元素是事件的签名(哈希)。对于上面的Transfer
事件,它的签名就是:
keccak256("Transfer(addrses,address,uint256)")
//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
除了事件签名,主题还可以包含至多3
个indexed
参数。
indexed
标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 indexed
参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。
数据 Data
事件中不带 indexed
的参数会被存储在 data
部分中,可以理解为事件的“值”。data
部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data
部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topic
部分中,也是以哈希的方式存储。另外,data
部分的变量在存储上消耗的gas相比于 topic
更少。
在etherscan上查询事件
我们尝试用_transfer()
函数在Rinkeby
测试网络上转账100代币,可以在etherscan
上查询到相应的tx
:网址。
点击Logs
按钮,就能看到事件明细:
Topics
里面有三个元素,[0]
是这个事件的哈希,[1]
和[2]
是我们定义的两个indexed
变量的信息,即转账的转出地址和接收地址。Data
里面是剩下的不带indexed
的变量,也就是转账数量。
继承
包括简单继承,多重继承,以及修饰器(modifier
)和构造函数(constructor
)的继承。
继承规则
-
virtual
: 父合约中的函数,如果希望子合约重写,需要加上virtual
关键字。 -
override
:子合约重写了父合约中的函数,需要加上override
关键字。
注意:用
override
修饰public
变量,会重写与变量同名的getter
函数,例如:
mapping(address => uint256) public override balanceOf;
简单继承
a合约是父合约,b合约是子合约,子合约会直接继承父合约,以及父合约的父合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract A {//父合约
//foo和bar覆盖了子合约的函数逻辑
function foo() public pure virtual returns (string memory) {//希望子合约重写加virtual
return "A";
}
function bar() public pure virtual returns (string memory) {
return "A";
}
// more code here
function baz() public pure returns (string memory){//完全会被b合约直接继承
return "A";
}
}
contract B is A {//子合约
function foo() public pure override returns (string memory) {//被改写加override
return "B";
}
function bar() public pure virtual override returns (string memory) {//c合约重写
return "B";
}
// more code here
}
c合约继承b合约,也完全继承a合约
contract C is B{
function bar() public pure override returns (string memory) {
return "C";
}
}
多线继承
多线继承要遵循基类到派生的原则,把继承最少的放在前面
contract X {
function foo() public pure virtual returns (string memory) {
return "X";
}
function bar() public pure virtual returns (string memory) {
return "X";
}
function x() public pure returns (string memory) {
return "X";
}
}
contract Y is X {
function foo() public pure virtual override returns (string memory){
return "Y";
}
function bar() public pure virtual override returns (string memory) {
return "Y";
}
function y() public pure returns (string memory) {
return "Y";
}
}
contract Z is X ,Y{//X合约没有继承,Y合约继承X合约因此X放在前面
function foo() public pure override(X,Y) returns (string memory) {
return "Z";
}
function bar() public pure override(X,Y) returns (string memory) {
return "Z";
}
}
多重继承
solidity
的合约可以继承多个合约。规则:
-
继承时要按辈分最高到最低的顺序排。比如我们写一个
Erzi
合约,继承Yeye
合约和Baba
合约,那么就要写成contract Erzi is Yeye, Baba
,而不能写成contract Erzi is Baba, Yeye
,不然就会报错。 -
如果某一个函数在多个继承的合约里都存在,比如例子中的
hip()
和pop()
,在子合约里必须重写,不然会报错。 -
重写在多个父合约中都重名的函数时,
override
关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
。
例子:
contract Erzi is Yeye, Baba{
// 继承两个function: hip()和pop(),输出值为Erzi。
function hip() public virtual override(Yeye, Baba){
emit Log("Erzi");
}
function pop() public virtual override(Yeye, Baba) {
emit Log("Erzi");
}
我们可以看到,Erzi
合约里面重写了hip()
和pop()
两个函数,将输出改为”Erzi”
,并且还分别从Yeye
和Baba
合约继承了yeye()
和baba()
两个函数。
修饰器的继承
Solidity
中的修饰器(Modifier
)同样可以继承,用法与函数继承类似,在相应的地方加virtual
和override
关键字即可。
contract Base1 {
modifier exactDividedBy2And3(uint _a) virtual {
require(_a % 2 == 0 && _a % 3 == 0);
_;
}
}
contract Identifier is Base1 {
//计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数
function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
return getExactDividedBy2And3WithoutModifier(_dividend);
}
//计算一个数分别被2除和被3除的值
function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
uint div2 = _dividend / 2;
uint div3 = _dividend / 3;
return (div2, div3);
}
}
Identifier
合约可以直接在代码中使用父合约中的exactDividedBy2And3
修饰器,也可以利用override
关键字重写修饰器:
modifier exactDividedBy2And3(uint _a) override {
_;
require(_a % 2 == 0 && _a % 3 == 0);
}
构造函数的继承
子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A
里面有一个状态变量a
,并由构造函数的参数来确定:
// 构造函数的继承
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}
- 在继承时声明父构造函数的参数,例如:
contract B is A(1)
- 在子合约的构造函数中声明构造函数的参数,例如:
contract C is A {
constructor(uint _c) A(_c * _c) {}
}
输入构造函数参数的方法
在继承时输入:
contract S {
string public name;
constructor(string memory _name) {
name = _name;
}
}
contract T {
string public text;
constructor(string memory _text) {
text = _text;
}
}
contract U is S("s"), T("t") {
}
在部署时传入:
contract S {
string public name;
constructor(string memory _name) {
name = _name;
}
}
contract T {
string public text;
constructor(string memory _text) {
text = _text;
}
}
contract V is S, T{
constructor(string memory _name, string memory _text) S(_name) T(_text) {
}
}
混合用法:
contract S {
string public name;
constructor(string memory _name) {
name = _name;
}
}
contract T {
string public text;
constructor(string memory _text) {
text = _text;
}
}
contract VV is S("S"),T{
constructor(string memory _name, string memory _text) T(_text) {
}
}
构造函数初始化顺序
contract V0 is S, T{
constructor(string memory _name, string memory _text) S(_name) T(_text) {//先运行S初始化,再运行T初始化,按照继承的顺序
}
}
contract V1 is S, T{
constructor(string memory _name, string memory _text) T(_text) S(_name) {//先运行S初始化,再运行T初始化,按照继承的顺序
}
}
contract V2 is T, S{
constructor(string memory _name, string memory _text) S(_name) T(_text) {//先运行T初始化,再运行S初始化,按照继承的顺序
}
}
调用父合约的函数
子合约有两种方式调用父合约的函数,直接调用和利用super
关键字。
- 直接调用:子合约可以直接用
父合约名.函数名()
的方式来调用父合约函数,例如Yeye.pop()
。
function callParent() public{
Yeye.pop();
}
super
关键字:子合约可以利用super.函数名()
来调用最近的父合约函数。solidity
继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba
,那么Baba
是最近的父合约,super.pop()
将调用Baba.pop()
而不是Yeye.pop()
:
function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}
发送代币
transfer
会带有2300个gas,如果gas被消耗尽了,被拒收或者其他问题导致发送失败会reverts
function sendViaTransfer(address payable _to) external payable {
_to.transfer(123);//向目标地址_to发送123个wei
}
send
会带有2300个gas,如果gas被消耗尽了,被拒收或者其他问题导致发送失败会返回一个是否成功的bool值
function sendViaSend(address payable _to) external payable {
bool sent = _to.send(123);
require(sent,"no");
}
call
会发送剩余所有的gas,会返回一个是否成功的bool值和data数据,如果你发送的对象是一个智能合约,智能合约有一个返回值就会在data中提现。
function sendViaCall(address payable _to) external payable{
(bool success, byte memory data)_to.call{value:123}("");
require(success.,"no");
}
创建一个接受主币的合约验证一下
//接受主币发送的目标地址
contract EthReceiver {
event Log(uint amount, uint gas);
receive() external payable {
emit Log(msg.value, gasleft());
}
}
制作以太坊钱包
制作这个钱包可以存入与取出主币qita
contract EtherWallet {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
receive() external payable {}
function withdraw(uint _amount) external {
require(msg.sender == owner, "caller is not owner");
payable(msg.sender).transfer(_amount);//payable()向内存中的地址发送主币
}
function getBalance() external view returns (uint){
return address(this).balance;
}
}
其他合约的调用方法
方法一:需要把另一个合约当作类型然后传入他的地址,之后就能调用这个合约的函数了
contract CallTestContract{
function setX(address _test,uint _x) external {
TestContract(_test).setX(_x);
}
}
contract TestContract {
uint public x;
uint public value = 123;
function setx(uint _x) external {
x = _x;
}
function getX() external view returns (uint) {
return x;
}
function setXandReceiveEther(uint _x) external payable {
x = _x;
value = msg.value;
}
function getXandValue() external view returns (uint, uint) {
return (x, value);
}
}
方法二:把类型直接当作输入的变量的类型去传入,在传入输入变量时还是地址变量,然后调用合约。
contract fang{
function setX(TestContract _test,uint _x) external {
_test.setX(_x);
}
}
contract TestContract {
uint public x;
uint public value = 123;
function setx(uint _x) external {
x = _x;
}
function getX() external view returns (uint) {
return x;
}
function setXandReceiveEther(uint _x) external payable {
x = _x;
value = msg.value;
}
function getXandValue() external view returns (uint, uint) {
return (x, value);
}
}
只读函数的调用
function getX(address _test) external view returns (uint){{//只读函数
TestContract(_test).getX();
}
获取主币的调用
contract CallTestContract{
function setXandSendEther(address _test, uint _x) external payable//发送x一个新值和主币
TestContract(_test) . setXamdReceiveEther{value:msg.value}(_x);//把所有主币传递到另一个合约
}
contract TestContract {
function setXandReceiveEther(uint _x) external payable {
x=_x;
value = msg.value;
}
查询主币调用
contract CallTestContract{
function getXandValue(address _test) external view returns (uint x, uint value) {
(x, value) = TestContract(_test).getXandValue();
}
}
contract TestContract {
function getXandValue() external view returns (uint, uint) {
}
return (x, value);
}
}
抽象合约
如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}
中的内容,则必须将该合约标为abstract
,不然编译会报错;另外,未实现的函数需要加virtual
,以便子合约重写。拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract
,之后让别人补写上。
abstract contract InsertionSort{
function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}
就是没有函数体的函数,必须要加上abstract,不然报错。加上virtual是方便子合约重写
接口 (interface)
如果我们合约需要和区块链上其他合约会话,则需定义interface。外部合约使用函数可直接读取其中数据。
接口规则:
1.不能包含状态变量
2.不能包含构造函数
3.不能继承除接口外的其他合约
4.所有函数必须是external且不能有函数体
5.继承接口的合约必须实现接口定义的所有功能
虽然接口不实现任何功能,但它非常重要。接口是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口(比如ERC20或ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:
合约里每个函数的bytes4选择器,以及基于它们的函数签名函数名(每个参数类型)
接口id(更多信息见ERC-165: Standard Interface Detection)
另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具也可以将ABI json文件转换为接口sol文件。
在合约中正常写一个函数,在合约外定义一个接口,接口内写出函数,但不写函数体,必须是external,在另一个合约中,定义一个函数变量必须是地址,声明接口,接口变量是地址,后面加点(.)接口内的函数名。