这里写目录标题
第一个智能合约
首先是对使用solidty版本的声明
pragma solidity 0.8.26 // 表示使用0.8.26版本的编译环境;
pragma solidity ^0.8.26 // 表示使用0.8.26版本及以上的编译环境;
pragma solidity >=0.8.24<0.8.26 // 表示使用0.8.24到0.8.26版本的编译环境;
- contract 关键字
表示接下来得内容是智能合约内容(类似于class关键字)
基本数据类型
- string ——表示字符串类型
- uint—— 表示无符号整数
2的n次方减一为最大数值,可以使用type(x).max来查看该数据类型的最大值和最小值。比如:
function max256 ()public pure returns(uint256) {
uint256 result = type(uint256).max;
return result;
}
部署合约之后可以看到uint256类型的最大值:
我们也可以写一个函数来使这个变量的值逐步增加,我们就可以看到当这个变量的存储的值超过该类型的最大值之后就会给出错误的信息。
- bool——表示布尔类型 默认值为false
- bytes——表示字节类型
定长字节数组:bytes [] BytesArray;
// 可以在括号当中指定数组的长度,如果不指定表示该数组是一个动态数组。
- address——表示合约得地址
变量的可见度标识符
1.public: 表示该变量可以被所有人看到和使用(相当于给变量添加了一个getter函数)
2.private:这个变量只能被本合约使用
3.internal:这个变量只能被本合约及其子合约使用
函数
首先来说一下函数的构成
function retrieve() public view returns(uint256){
return favoriteNumber;
}
- function是函数的关键字
- retrieve是函数的名称,名称后面的括号用于传入参数
- public代表函数的可见度
- view关键字用来表示函数的属性
- returns后面的括号表示函数的返回值(数据类型)
接下来是函数的属性
函数的默认的属性是写操作,还包括view和pure,这两个属性都不会改变合约的状态。
view和pure的区别在于pure不可以使用合约内的数据
// 默认属性
function set(uint256 _number) public {
number = _number;
}
// view 关键字
function get() public view returns (uint256){
return number;
}
// pure 关键字
function add(uint256 a,uint256 b)public pure returns(uint256){
return a + b;
}
注意事项
- view和pure关键字表示标识的函数在调用的时候不需要消耗gas,view和pure只会读取当前合约的存在状态,并且不允许修改合约当中的任何状态。
- pure和view的区别在于:pure不允许读取区块链当中的数据,用于不需要读取数据的算法。
- view和pure只有在合约内部被调用的时候才会消耗gas。
结构体(struct)
struct People{
uint256 favoriteNumber;
string name;
}
结构体的使用
比如我要创建一个新的People
我们可以写下People public,把它命名为person。这样我们就创建了一个新的People,并将其分配给person这个变量,后面再加上(),表示我们在创建一个新的People类型的变量,里面添加{},表示参数是一个结构体,然后赋值。
// 依据之前定义的结构体当中的变量使用
People public person = People({favoriteNumber:1,name:"MAKE"});
People当中的花括号才能让solidity知道我们使用的是这些结构体内的变量
因为它是一个public变量,所以我们部署合约之后会有一个People的蓝色按钮,同时会根据不同的变量产生索引。
- favoriteNumber的索引是0
- name的索引是1
其实在solidity的列表变量中放入变量,都会自动获取一个索引。
即 : 每在合约当中新加一个变量,就会自动的获取索引
如果我们要存储很多数据到列表当中的方法不是一个个的创建People,而是使用数组这样的数据结构。
数组
- 首先来说数组的定义方式,比如我们现在想要得到一个People类型的数组
和变量的定义类似
数据类型 [] 可见度标识符 数组/列表的名称
// 这是一个People类型的数组
People [] public people;
// 这是一个uint256类型的数组
uint256 [] public favoriteNumber;
此时的数组相当于是动态数组,因为我们在初始化的时候并没有指定它的大小;如果我们在方括号里放一个数字,比如3,那就相当于指定了这个数组的最大长度为3,只能放进去三个People对象。
- 给数组添加内容(相当于把People这一个结构体放到了数组当中)
第一种方法:
我们需要定义一个函数addPerson来完成这个操作
// 创建一个People类型的数组
People [] public people;
// 定义一个函数向数组当中添加内容
function addPerson(uint256 _favoriteNumber,string memory _name) public {
// 对people对象调用push函数
people.push(People(_favoriteNumber,_name));
}
push就是添加的意思,给people这个对象添加一个People类型的数据
第二种方法:
function addPerson(uint256 _favoriteNumber,string memory _name) public {
People memory newPerson = People({favoriteNumber:_favoriteNumber,name:_name});
people.push(newPerson);
}
注意
:如果像上述代码一样使用花括号,就一定要按关键字传参,不能按参数位置传参。
其实把第二种方法参数的传递改为按位置传参,那么就可以省略这一行,直接改为第一种写法,省去给newPerson赋值的这一步。如下图:
function addPerson(uint256 _favoriteNumber,string memory _name) public{
People memory newperson = People(_favoriteNumber,_name)
people.push(newperson);
}
注意
:这里的两种给数组添加内容的方法都是使用的push方法,区别在于一个直接进行push,另一个先赋值给了一个变量,然后再把这个变量push到数组当中。
- 关于定长数组和动态数组
定长数组使用下标来存入数据,但是动态数组使用push方法存入数据,不需要使用下标索引。
contract ArrayTest{
uint256[4] public fixedData;
uint256[] public dynamicData;
// 定长数组 使用下标来存放数据
function setFixedData(uint8 index,uint256 value)public{
fixedData[index] = value;
}
// 动态数组 使用push方法来存入变量,不需要使用下标索引
function addDataToDynamic(uint256 value) public{
dynamicData.push(value);
}
}
- 数组的使用
根据我们定义的数组的结构来输入存放的数据(_num,_name),点击set_info就可以把数据存放进去。
因为我们定义了一个名为person的数组,所以会有一个与数组名相同的名为person的按钮,我们在右侧输入框可以输入索引就可以查看索引对应的数据。
那么该如何辨别我输入的索引是否有效呢?
当我们输入一个合法的索引之后,会显示出如下的信息,并且记录了一些内容
如果我们输入的索引不合法(在最大索引的索引之外),那么会是这样的:
并且他还告诉了我们错误提示信息。
- Memory,Storage & Calldata(最常用的三种数据存储的位置)
- memory: 临时存储,类似于一个临时变量,用完之后在其他位置不需要再使用
- calldata:临时存储,和memory类似,但是calldata的变量定义并初始化之后不可以再次修改。
- storage:永久存储,也是大部分变量默认的存储位置。
- 关于memory
数组,结构和映射在solidity当中被认为是特殊的数据类型,要让solidity知道数组,结构和映射的数据位置,num是一个uint256类型的数据,solidity可以自动知道uint256的位置所以不需要memory,但是string
类型的数据需要在前面加上memory。比如刚刚函数当中定义的参数 string memory _name
因为string的背后原理是一个字节类型的数组 bytes[] ,所以需要加上memory让solidity知道string的数据位置
总结:当数组,结构体和映射在作为参数被添加到不同的函数当中时,需要给定义一个memory或calldata关键字
mapping映射
为了更方便的在数组当中找到一个人以及他的详细信息,使用了另外一种数据结构——mapping(映射),类似于python的字典,有键值对的结构每一个键反映出对应的值。
mapping映射的定义:
// 从string到uint256的mapping()类型
mapping(string => uint256) public nameToFavoriteNumber;
嵌套的mapping映射定义方法:
mapping (string => string) nameToDesc;
mapping (string => uint8) nameToAge;
mapping (string => mapping(string=>uint8)) public name2age;
mapping的使用
在创建数组之后,我们写了一个addperson函数来把内容添加到数组当中,这时候我们需要把mapping映射也写到addperson函数当中,把添加到数组当中的内容也添加到mapping映射当中。
contract Storage{
struct People{
uint256 num;
string name;
}
mapping (uint256 => string) public numToname;
People [] public people;
function set_info(uint256 _num,string memory _name) public {
People memory newperson = People(_num,_name);
// People memory newperson = People({num:_num,name:_name}); 第二种添加元素的方法
people.push(newperson);
numToname[_num] = _name;
}
}
我们先往数组当中存放几条数据方便展示mapping的作用,在我们往数组里存放数据的时候这个addPerson函数也将这些数据存放到了mapping映射当中。
我们使用numToname这个mapping映射,输入一个数字他会给我们返回这个数字对应的名字,相当于存在了一个学号和学生名字的python字典关系
注意
:
当你创建一个映射时,会把所有东西都初始化为空值,现在这里每一个可能的uint256数据,都有一个对应的初始值name为空字符串。所以我们需要手动的往里添加值。
如果想让mapping映射作为参数传入一个函数当中,那么这个函数的可见性就必须是internal或者是private,不能是public。
function setnameToAge (mapping(string=> uint8) storage data) internal{
}
练习案例:使用mapping映射关系来实现账户与账户之间的转账操作。
代码实现思路:
- 定义一个由address类型指向uint256类型的mapping映射关系来存放账户以及账户余额的对应关系
- 使用函数构造器,在部署合约在之前将合约的调用者地址拿到(使用上下文变量msg.sender),并且给这个账户100块钱存入到balances这个映射当中。
- 写一个getter函数来查看账户当中的余额
- 主函数(交易函数)需要的参数:相互转账的账户地址,需要进行转账的金额
- 使用require语句判断需要转账的账户的余额是否够用。
contract Storage{
mapping (address => uint256) balances;
constructor() {
address deployer = msg.sender;
balances[deployer] = 100;
}
function get_info (address owner) public view returns(uint256){
return balances[owner];
}
function transfer (address from ,address to ,uint256 money) public {
require( balances[from] > money,"The count do not have the enough money to transfer");
balances[from] -= money;
balances[to] += money;
}
}
优化,完善代码:
- 设置将要进行转账的账户必须是自己的账户,不能随意动用其他账户的资金进行转账给别人
- 设置黑名单账户,实现无法对黑名单账户进行转账。
- 只有合约的调用者才有权限对黑名单修改,不能让任意账户随意添加或者去除黑名单
最终代码:
contract Storage{
address authority;
mapping (address => uint256) balances;
mapping (address => bool) blacklist;
constructor(address auth) {
authority = auth;
address deployer = msg.sender;
balances[deployer] = 100;
}
function add_blacklist(address adder) public{
require(msg.sender == authority,"You are not the contract deployer, so you do not have the authority to perform this operation.");
blacklist[adder] = true;
}
function get_info (address owner) public view returns(uint256){
return balances[owner];
}
function transfer (address to ,uint256 money) public {
require(msg.sender == authority,"You can only operate on your own account, not on any other accounts.");
require(blacklist[to],"The account you are about to transfer to is on the blacklist.");
require( balances[authority] > money,"The count do not have the enough money to transfer");
balances[authority] -= money;
balances[to] += money;
}
}
引用数据类型
引用数据类型包括数组和结构体
因为在solidity当中含有storage这个数据块,所以变量的数据块会固化在存储当中,并不会指向所引用的数据类型的变量。所以不会出现其他变量当中的引用拷贝,只会出现值拷贝
。
contract Storage{
// 创建两个整形数组
uint256[3] public stateData1;
uint256[3] public stateData2;
function storage2memory() public{
stateData1 = stateData2; // 并不会产生一个由stateData2指向stateData1的指针 ,只是把stateData2的值赋值给了stateData1
stateData2[0] = 2;
}
}
调用storage2memory函数之后 ,发现stateData2当中的索引为0的值为2,但是在stateData1当中索引为0的值还是默认值0,所以这个说法是正确的。
判定算法
下面举例说明:
function storage2memory () public {
uint256[3] memory data;
data = stateData1; // 因为两个数据的location并不相同,所以不会出现引用拷贝,只会是值拷贝
stateData1 [0] = 9;
}