一solidity 基础《实战NFT web3 solidity(新版本0.8.+)》

Posted 1_bit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一solidity 基础《实战NFT web3 solidity(新版本0.8.+)》相关的知识,希望对你有一定的参考价值。

《web3 solidity0.8.+版本(持续更新新版本内容) 基础到实战NFT开发》会及时更新新版本 solidity 内容,以及完成最终的 NFT 实战商业项目部分。

注:由于是付费专栏内容,若有错误请及时联系@1_bit,博客链接:https://blog.csdn.net/A757291228 ,或在文章下留言,收到后将会对错误进行改正,若是版本更新导致的问题也希望大家对错误进行提交,尽力去保证付费用户该得到的权益。

文章目录可查看:目录(文章更新中…)
更新内容将会在目录中更新…

友情提示:本系列文章读者最好学过一门编程语言,面向对象语言更佳,文章所有代码将会完整贴出。

一、solidity 智能合约

solidity 是以太坊上的“脚本语言”,当然你可以这样理解;这个语言是运行在以太坊之上的,也称作智能合约。

智能合约是指“可编写不可篡改”的合同,solidity 程序员可将合约内容使用 solidity 编写在以太坊上,一经部署便不可更改,并且合约内容公开透明,参与该合约则会“履行”该合约。

二、solidity 结构

以下是一个 solidity 代码的最基本结构:

// SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

contract TestContract


其中第一行是表示 SPDX 的许可标识,照样写上就ok了。

第二行是声明当前合约所使用的版本号,版本号声明需要在 pragma solidity 之后写明版本信息,其中 ^0.8.0 表示 0.8.0 版本以上都可以适用当前合约内容。若除去“^”符号则表示适用于 0.8.0 这个版本。

版本声明还可以使用其他方式进行声明,也可以写成如下形式:

pragma solidity >=0.7.0 <0.9.0;

其中 >=0.7.0 表示大于0.7.0 版本,小于 0.9.0 版本。

接下来的 contract TestContract 则是创建一个合约,其中 contract 为创建合约的“关键字”,TestContract 为合约名称,由此可以看出,创建一个合约的方式跟一个类创建的方式类似。

三、数据类型

solidity 中包含 bool、int、uint、string、address、bytes 数据类型,其中 int 、uint、bytes 拥有不同“大小”的同类型下的不同表达的数据类型:

  • int:int 每 8位一个步长,从而得到 int8、int16、int24、int32…int256,其 int默认为 int256
  • uint:同上
  • bytes:bytes可以声明成定长字节数组与不定长字节数组,例如 bytes1、bytes2、bytes3…bytes32 为定长数组,直接使用 bytes 为不定长数组

以上类型用 solidity 代码表示如下:

// SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

contract TestContract
    bool boolVal=true;
    int256 intVal=-6;
    uint256 uintVal=10;
    string strVal="Hello Solidity";
    address addrVal=0x4BC613E2D4A342f8301e51f2636Cb05b0D0CCE01;
    bytes32 byteVal="Byte Value";
    bytes1 length1BytesVal="1";
    bytes autoLengthBytesVal="abcdefghigklmn";

若此时你想 bytes1 类型数据中创建多个值,如:bytes1 length1BytesVal="12";,此时由于内容超过了定长 bytes 数组,此时将会报错:


此时报错将会提示不能把值 12 转成你想要的bytes1类型数据:

四、函数

在 solidity 中可以使用函数,函数如类方法一样声明,创建方式跟 javascript 之类的语言类似:

function setIntVal(int _val)public
    intVal=_val;

以上是一个函数,function 表示创建一个函数 setIntVal 为这个函数的名称,又或者此时再合约之中,按照我们的习惯便自然的称之为合约方法,其接收一个参数 _val,为 int 类型。此时需要注意的一个点,跟 C++ 之类的语言不同,其函数“可见”修饰符修饰语在参数之后,在这里使用 public 表示这个方法是公开的,方法内代码为 intVal=_val 在此为这个合约中的 IntVal 设置一个值为传入的参数值。

五、部署

部署点击后即可部署合约:

该部分为已部署的合约内容:

展开后即可查看对应的公开内容,此时只有一个公开的方法,并且不能查看对应的数值:

此时可以为其状态变量,也就是这个值添加对应的修饰,使其可公开:

重新部署后克查看到该公开的状态变量值,点击后即可查看其中的值:

设置值后再次点击,查看值已更新:

点击设置值后我们可以看到产生了一个操作(点击展开):

其中 :

  • status 表示状态,再次提示其操作成功执行

  • transaction hash 表示一个交易hash,因为更改其值,那么为一个交易 from 表示操作方 to 表示被操作方,在此显示 TestContract.setIntVal(int256) 0xf8e81D47203A594245E36C48e151709F0C19fBe8 表示操作的是 TestContract
    这个合约之下的 setIntVal 方法,合约地址是 0xf8e81D47203A594245E36C48e151709F0C19fBe8

  • gas 表示当前 gas 燃烧 transaction cost 和 execution cost 分别表示交易成本和执行成本的 gas

  • input 和 decode input 表示 输入和解码后输出结果是 256又或者说是一个数据

  • decoded output 解码输出当前为空

而我们在查看某值的时候并未花费 gas,因为只是做了查看不做其他操作,不修改任何值:

其他范围修饰

以上所述的 public为公开修饰,除了公开 public 之外还有 private、external、internal:

  • private 仅在当前合同中可见
  • external 仅在外部可见(仅用于函数,合约之外),即只能通过this.func调用消息
  • internal 仅在内部可见

函数返回值

solidity 中允许函数有返回值,需要使用 returns 对函数返回值进行说明:

function getuintVal()public view returns(uint256)
    return uintVal;

以上函数中,使用了returns 对返回值进行说明,说明返回值的类型是 uint256 类型,此时还注意到了有一个未成使用过的 view 对函数进行了修饰,这个 view 延缓一下,后面将会说明。

我们部署合约之后,可以看到 getuintVal 函数,接着对其调用可得到这个 uintVal 的值:

view

那 view 是什么意思呢?

view 表示修饰一个函数只用于读取数据,并不修改其内部状态(若不在消耗 gas 的函数中调用它则不消耗 gas)。跟view 类似的修饰还有 pure,其表示一个函数不读取状态也不修改状态,这些是 solidity 中需要我们对一个函数进行修饰的,这两个修饰的方法并不会消耗 gas。

六、结构体

solidity 中的结构体定义跟C语言的类似:

struct People
    string name;
    uint age;

先用 struct 表示需要定义一个结构体,之后跟着一个结构体名,其花括号内为对应的结构体数据内容。

若此时我们需要一个函数创建一个结构体后并且返回这个结构体对象,此时可以编写如下代码:

function peopleData()public pure returns(People memory)
     People memory MrXi = People(name:"Xi",age:18);
     return MrXi;
 

以上函数使用了 pure 修饰,表示这个函数并不修改和读取合约中的状态变量,有同学可能疑问,不是创建一个结构体了吗?但是这个结构体是在函数中创建的,而并不是外部。结构体类似于一个数据类型,就像在一个合约中创建一个整型数据一样,所以并没有盗用合约中的数据。

在此使用了returns 返回对应的数据,数据类型是 People 这个没问题,但为什么加了一个 memory?这是表示当前 People 数据仅为临时的内存数据,可能有小伙伴说可以不加吗?这个可不行,这个是 solidity 中规定,你需要对数据进行对应的修饰,不加则会出现下面报错:


接着我们继续查看这个函数中的代码,在函数中使用了 People 创建了一个 MrXi 的结构体变量,并且给了初始值,在此需要注意,我依旧使用了 memory 对其进行修饰,最后进行返回。

除了 memory 之外,在 solidity 中还包括 storage,这两者称为引用类型,我们可以简单的理解为 storage 为指针传递,而 memory 为值传递。

换句话说,memory 就像函数中的局部变量,函数结束后内存中的内容将会被释放,二存储在合约之中的就是 storage,当你即使在函数中使用,结束后也不会释放,将会永久存储在合约之中,你使用的时候也不需要使用memory 之类的进行修饰,而是这个变量本身就存储在合约之中,进行赋值之类的操作更像是对合约其本身所在的存储位置进行值的修改,所以storage 可表示为指针传递。

什么时候使用 memory 修饰参数

在变长数据时就需要使用 memory进行修饰,什么是变长呢?你可以理解为string 类型参数在输入时长度不确定,那就是变长,需要使用memory 修饰,当然还有不定长数组 bytes 之类的数据,以及结构体都需要使用 memory 对其进行修饰,而 int 就不需要使用 memory。

storage、calldata 使用示例

storage

之前有简单了解过这两个修饰,在此咱们使用示例对其进行了解。

storage 类似于指针,例如如下示例:

//storage
uint256[] public storageTest=[1,2,3,4,5];
function storageFunc()public
    uint256[] storage storageUintVal=storageTest;
    storageUintVal[0]=100;

此时 storageFunc 中创建了一个 uint256 类似的数组 storageUintVal 赋值为一个数组 storageTest,并且使用了 storage 对其进行修饰,那么在此就表示 storageUintVal 指向了 storageTest,若修改storageUintVal 内元素的值,那么同样 storageTest 的值也会发生改变。

部署合约后,我们成功查看对应的色storageTest 数组的第 0 个元素值为 1:

当我调用 storageFunc 函数后:

在此查看其数组值为100:

这是因为在创建 storageUintVal 时使用了 storage 修饰。

在此处需要注意,storage 只能为数组、结构或映射类型指定数据位置,但提供了“存储”。

calldata

calldata 修饰数据将会使数据不能进行修改:

以上实例中,使用 calldata 对参数 _c 进行了修饰,那么 _c 就不能在接下来的代码中进行修改,否则将会报错:

当然,在你传参的时候你传入的参数将会存储到这个 calldata 变量中,之后则不能进行修改,若你想查看是否存入成功,那么此时整个函数可以写成:

//calldata
function calldataFunc(uint[] calldata _c)public pure returns(uint[] calldata)
    return _c;

在此我们用作了 pure 对函数进行修饰,并且在返回值的时候要说明我们需要返回的是一个 calldata 的数据。

七、map 键值对

在 solidity 中还提供了map(字典),使用字典可以完成一些数组不便(不高效浪费 gas)的操作,例如你在数组中存储了一个结构为名称与身高,你需要获取某个姓名的数据时需要遍历数组取值,在 solidity 中这是十分消耗 gas 的操作,在此我们可以使用 map 对其进行存取,以下是一个 map 的使用示例:

//map 
mapping(string=>uint256) public mapVal;
function setMapVal(string memory _name,uint256 _age) public
    mapVal[_name]=_age;

以上代码中 mapping 表示创建一个键值对,键值对key与value 为 string=>uint256,在此我们设置为 public,该键值对变量名为 mapVal。

接下来在 setMapVal 的函数中对其进行调用,接收参数一个为 name 一个为 age,之后将会存储到 map 之中。在函数体内,直接设置其值 mapVal[_name]=_age;

那么取得 map 中的值的函数如下:

function getMapVal(string memory _name) public view returns(uint256)
    return mapVal[_name];

直接对map进行取值即可返回。

部署后直接调用 setMap 方法传入值:

随后在 getMap 的时候输入 key 即可:

以上知识点完整 sol 代码

// SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

contract TestContract
    bool boolVal=true;
    int256 public intVal=-6;
    uint256 public uintVal=10;
    string strVal="Hello Solidity";
    address addrVal=0x4BC613E2D4A342f8301e51f2636Cb05b0D0CCE01;
    bytes32 byteVal="Byte Value";
    bytes1 length1BytesVal="1";
    bytes autoLengthBytesVal="abcdefghigklmn";

    struct People
        string name;
        uint age;
    

    People[] public people;
    function setPersonArray(string memory _name,uint _age)public 
        //people.push(People(name:_name,age:_age));
        people.push(People(_name,_age));
    

    //storage
    uint256[] public storageTest=[1,2,3,4,5];
    function storageFunc()public
        uint256[] storage storageUintVal=storageTest;
        storageUintVal[0]=100;
    

    //calldata
    function calldataFunc(uint[] calldata _c)public pure returns(uint[] calldata)
        return _c;
    
    //function 
    function setIntVal(int _val)public
        intVal=_val;
    

    function getuintVal()public view returns(uint256)
        return uintVal;
    

    function peopleData()public pure returns(People memory)
        People memory MrXi = People(name:"Xi",age:18);
        return MrXi;
    
    
	//map 
    mapping(string=>uint256) public mapVal;
    function setMapVal(string memory _name,uint256 _age) public
        mapVal[_name]=_age;
    
    function getMapVal(string memory _name) public view returns(uint256)
        return mapVal[_name];
    

八、合约之间的相互调用

在 solidity 中还可以对合约进行相互之间的调用,可传入合约地址或直接对某一合约进行创建,创建后生成的对象即可对该合约进行调用。

此时再 remix 中创建两个合约,一个称之为A另一个称之为B:

在 A 合约中设置一个 public 的状态变量 7,以及设置两个方法存取这个状态变量的值,那么该合约编写如下:

// SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
contract A 
    uint256 public v=7;
    function setV(uint256 _data) public 
        v=_data;
    
    function getV() public view returns (uint256) 
        return v;
    

接着在 B 合约中,可以使用 import 引入当前路径下的合约A:

import "./A.sol";

若你不加 ./ 则当前路径为根目录下。接着创建一个合约 B,在合约B中创建一个 public 的合约 A 类型的状态变量小a:

A public a;

此时小a 并没有复制,只是合约A类型的一个状态变量,若想给A赋值,可以直接new 一个合约 A 类型到小 a 之中,可以写为:

a=new A();

那么此时我们可以创建一个方法:

function createA()public
    a=new A();

接着,可以创建一个方法设置合约A中的状态变量,直接使用点运算符“.”即可操作这个合约对象 a 中的方法,那么此时这个方法可以写为:

function setVal(uint256 _v)public
    a.setV(_v);

同样,获取 a 合约中的内容可以写为:

function getAVal() public view returns(uint256)
    return a.getV();

整体合约代码如下:

// SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
import "./A.sol";

contract B 
    A public a;
    function createA()public
        a=new A();
    

    function setVal(uint256 _v)public
        a.setV(_v);
    

    function getAVal() public view returns(uint256)
        return a.getV();
    

部署合约后可以点击 createA方法创建对应的合约对象A:

随后可以点击 getAVal 查看合约A 中的状态变量值:

接着使用 setVal 设置合约变量中的值:

在此 get 后,值发生了改变:

若你未创建合约 A 时,合约变量 a 中存储的是0地址:

此时还需要注意,你创建的合约变量在 new createA后产生的合约是新的合约,如下第一词点击 createA 生成的合约为:

第二次点击createA 后合约地址为:

由此可知,即使你部署了合约A,你在合约B中创建的合约跟你之前部署的合约A没有任何关系,除非你对合约地址进行设定。

例如先在合约B中创建一个构造方法(当然你也可以修改createA方法):

constructor(address adr)
    a=A(adr);

此构造方法传入一个地址,这个地址是已部署的合约A的地址,在此使用 A(adr) 创建一个对合约地址 A 的指向。

接着首先部署合约A,部署完毕后复制合约A 的地址到合约B部署时,因为有了构造函数,那么构造函数需要传递一个地址作为对合约A的指向,所以此时需要填入参数才能部署合约B:

随后点击部署:

部署完毕后点击合约B中的setVal 方法为合约A中的值进行更改,由于合约A已在合约B中进行指向,那么合约A中的 v 值发生改变:

solidity 中还有其他调用合约的方法 DELEGATECALL、CALL 等,之后再进行讲解。

九、继承

在 solidity 中,合约允许继承,使用 is 可以指定当前合约继承与哪一个合约。

新建一个sol 文件后,在其中创建一个 Base 合约:

contract HumanBase
    string name;
    uint256 age;
    uint256 height;
    function sayHello()public pure returns(string memory)
        return "hi~";
    

接着创建一个合约继承自该合约,若合约名称为 InheritDemo,就可以写成 InheritDemo is HumanBase,加上 contract 关键字后即:

contract InheritDemo is HumanBase


接着我们创建一个方法用于调用当前合约中的方法,因为继承当前合约了,那么肯定能够调用得到,那么方法即:

function Say()public pure returns(string memory)
    return sayHello();

此时部署合约后可以看到当前合约两个方法都是存在的:

点击后也可以调用:

重写

若集成某一个方法后,在开发过程中想重新编写该方法可以对其重写。重写方法需要在“基类”也就是父合约中对方法添加 virtual 修饰:

接着在子合约要重写的方法之中添加 override 修饰:

最后部署后调用:

构造函数

构造函数使用 constructor 创建一个最简单的构造函数如下:

constructor() 


构造函数是在合约创建时对数据进行初始化的函数,可以在其中传入参数:

uint256 age;
constructor(uint _age) 
	age=_age;

若一个合约继承了一个合约,那么这个合约还可以调用父类构造函数对其本身进行初始化,若一个合约继承了 A 合约,那么直接使用这个“合约名()” 即可调用父合约的构造函数:

uint256 age;
constructor(uint _age) A()
	age=_age;

还允许给父合约构造函数传入对应的参数:

uint256 age;
string name;
constructor(uint _age) A(name)
	age=_age;

以上是关于一solidity 基础《实战NFT web3 solidity(新版本0.8.+)》的主要内容,如果未能解决你的问题,请参考以下文章

一solidity 基础《实战NFT web3 solidity(新版本0.8.+)》

(2.3)其他补充—— 二solidity 基础进阶《实战NFT web3 solidity(新版本0.8.+)》

(2.3)其他补充—— 二solidity 基础进阶《实战NFT web3 solidity(新版本0.8.+)》

(2.3)其他补充—— 二solidity 基础进阶《实战NFT web3 solidity(新版本0.8.+)》

二solidity 基础进阶(2.1)—— library 库合约《实战NFT web3 solidity(新版本0.8.+)》

二solidity 基础进阶(2.1)—— library 库合约《实战NFT web3 solidity(新版本0.8.+)》