solidity从入门到精通(持续更新)

我一度觉得自己不知何时变成了一个浮躁的人,一度不想受外界干扰的我被干扰了,再无法平静的去看一本书,但我仍旧希望我能够克服这些,压抑着自己直到所有的冲动和奇怪的思想都无法再左右我行动。
自律会让你更加自律,放纵会让你更加放纵。
做人做事,都是如此。
简单的道理却要克服很多东西。做正确的事情而不是我想做的事情。发乎情,止乎理。


速成班


1全世界人们入门的统一标准hello world

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract HelloWeb3{
    
    
    string public _string = "Hello Web3!";
}

前两行类似于说明文件,contract说明你开始编写一个合约,这个合约的名字叫做helloweb3,合约内声明了一个字符串public _string叫做 “Hello Web3!”
这个hello world我编译失败了
莫名其妙又成功了,把编译的篮子放在了右边就成功了?

合约编译成功之后可以进行部署。
https://remix.ethereum.org/ remix合约的编译环境

大佬说是一个大佬的人:崔棉大师


2 值类型

bool类型
// 声明一个对外可访问的bool
bool public _bool = true;

// 布尔运算
bool public _bool1 = !_bool; // 取非
bool public _bool2 = _bool && _bool1; // 与
bool public _bool3 = _bool || _bool1; // 或
bool public _bool4 = _bool == _bool1; // 相等
bool public _bool5 = _bool != _bool1; // 不相等
整型
// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 正整数
uint256 public _number = 20220330; // 256位正整数


// 整数运算
uint256 public _number1 = _number + 1; // +,-,*,/
uint256 public _number2 = 2**2; // 指数
uint256 public _number3 = 7 % 2; // 取余数
bool public _numberbool = _number2 > _number3; // 比大小
地址类型

地址类型(address)有两类:
普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。
payable address: 比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。

// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
// 地址类型的成员
uint256 public balance = _address1.balance; // balance of address

我有点不是太理解这个payable address

定长字节数组
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity"; 
bytes1 public _byte = _byte32[0]; 

32个字节组成的数组,也就是可以容纳32个元素,很明显后面的并没有使用,u8类型就是32个数字,hex
64类型就是32的元素组成的数组,0-f

枚举enum
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;

枚举的使用方法和各种语言的枚举方式几乎是相同的。
但是这段代码我有点没看明白。

// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
    return uint(action);
}

在 Solidity 中,external 是一个函数修饰符,用于定义函数的可见性。函数的可见性决定了函数可以从哪里被调用。也就是external和public其实是同级的。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract HelloWeb3{
    
    
    string public _string = "Hello Web3!";
    bool public _bool = true;
    int public _int = -1;
    uint public _unit = 1;
    uint256 public _unitlong = 2222;
}

contract my_accout{
    
    
    // 地址是160bits,最后压缩为40个16进制
    address public my_address = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    address payable public money_address = payable(my_address);
    uint256 public balance = money_address.balance;
    bytes32 public _byte32 =  "here is a hero";
    bytes1 public _byte1 = _byte32[0];
    enum ActionSet{
    
    
        Buy,
        Hold,
        Sell
    }
    ActionSet action = ActionSet.Buy;
    // 这个里面也存在类型转换
    // 这个操作是需要汽油费的
    function enumtounit() external view returns(uint){
    
    
        return uint(action);
    }
}

3函数function

function <function name>(<parameter types>) {
    
    internal|external|public|private} [pure|view|payable] [returns (<return types>)]

看着有一些复杂,让我们从前往后逐个解释(方括号中的是可写可不写的关键字):

function:声明函数时的固定用法。要编写函数,就需要以 function 关键字开头。

:函数名。

():圆括号内写入函数的参数,即输入到函数的变量类型和名称。

{internal|external|public|private}:函数可见性说明符,共有4种。
public:内部和外部均可见。
private:只能从本合约内部访问,继承的合约也不能使用。
external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
internal: 只能从合约内部访问,继承的合约可以用。
注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。
注意 2:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,
用于查询数值。未标明可见性类型的状态变量,默认为internal。

[pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。pure 和 view 的介绍见下一节。

[returns ()]:函数返回的变量类型和名称。

有关于pure 和view

刚开始学习 solidity 时,pure 和 view 关键字可能令人费解,因为其他编程语言中没有类似的关键字。solidity 引入这两个关键字主要是因为 以太坊交易需要支付气费(gas fee)。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 gas。
包含 pure 和 view 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的
(注意,合约中非 pure/view 函数调用 pure/view 函数时需要付gas直接调用不需要支付,但是非pure/view函数间接调用是需要支付gas fee的)。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract FunctionTypes{
    
    
    uint256 public number = 5;
    
    constructor() payable {
    
    }

    // 函数类型
    // function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
    // 默认function
    function add() external{
    
    
        number = number + 1;
    }

    // pure: 纯纯牛马
    // 没有读取权,因此什么东西都是别人传给他的
    // 瞎子
    function addPure(uint256 _number) external pure returns(uint256 new_number){
    
    
        new_number = _number+1;
    }
    
    // view: 看客
    function addView() external view returns(uint256 new_number) {
    
    
        new_number = number + 1;
    }

    // internal: 内部函数
    function minus() internal {
    
    
        number = number - 1;
    }

    // 合约内的函数可以调用内部函数
    function minusCall() external {
    
    
        minus();
    }

    // payable: 递钱,能给合约支付eth的函数
    function minusPayable() external payable returns(uint256 balance) {
    
    
        minus();    
        balance = address(this).balance;
    }
}

internal和external

internal是只可以被内部调用的函数方法,external是可以被外部调用的。
可以把内部调用函数写给外部方法里面,通过外部方法间接调用


4函数return

返回值:return 和 returns

Solidity 中与函数输出相关的有两个关键字:return和returns。它们的区别在于:
returns:跟在函数名后面,用于声明返回的变量类型及变量名。
return:用于函数主体中,返回指定的变量。

contract return_test{
    
    

    // 这种返回符合日常的使用
    function retrun_multiple() public pure returns (uint256,bool,uint256[3] memory){
    
    
        return (1,true,[uint256(2),3,5]);
    }

    // 这种属于是命名式的返回
    function return_withname() public pure returns (uint256 _int,bool _bool,uint256[3] memory _arr){
    
    
        _int = 1;
        _bool = false;
        _arr = [uint256(3),6,9];

        //return(1, true, [uint256(1),2,5]); 这种脱裤子放屁的行为也被支持
    }

	// 粘贴来的函数不能用,了解以下解构是怎么回事就好了
    // 读取返回值,解构式赋值
    function readReturn() public pure{
    
    
        // 读取全部返回值
        uint256 _number;
        bool _bool;
        bool _bool2;
        uint256[3] memory _array;
        (_number, _bool, _array) = returnNamed();
        
        // 读取部分返回值,解构式赋值
        (, _bool2, ) = returnNamed();
    }
}

这里uint256[3]声明了一个长度为3且类型为uint256的数组作为返回值。因为[1,2,3]会默认为uint8(3),因此[uint256(1),2,5]中首个元素必须强转uint256来声明该数组内的元素皆为此类型。数组类型返回值默认必须用memory修饰。
我有点没看明白这个memory
和rust有一个类似的地方,可以不接受不需要的参数

(, _bool2, ) = returnNamed();

5变量数据存储和作用域

引用类型:包括数组(array)和结构体(struct),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。

Solidity数据存储位置有三类:storage,memory和calldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memory和calldata类型的临时存在内存里,消耗gas少。大致用法:

storage:合约里的状态变量默认都是storage,存储在链上。
memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。
尤其是如果返回数据类型是变长的情况下,
必须加memory修饰,例如:string, bytes, array和自定义结构。
calldata:和memory类似,存储在内存中,不上链。
与memory的不同点在于calldata变量不能修改(immutable),
一般用于函数的参数。

我的感觉就是calldata是栈数据,虽然不知道为什么不可变,然后memory就类似于堆数据的可变数据

function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
    
    
    //参数为calldata数组,不能被修改
    // _x[0] = 0 //这样修改会报错
    return(_x);
}
变量的作用域

合约里定义的变量就是所谓的状态变量,修改这些状态变量是昂贵的

contract Variables {
    
    
    uint public x = 1;
    uint public y;
    string public z;
    // 这种pure的计算是不上链的,无需给矿工缴纳gas fee
	function bar() external pure returns(uint){
    
    
    	uint xx = 1;
    	uint yy = 3;
   		uint zz = xx + yy;
   		return(zz);
}
}

全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用:

function global() external view returns(address, uint, bytes memory){
    
    
    address sender = msg.sender;
    uint blockNum = block.number;
    bytes memory data = msg.data;
    return(sender, blockNum, data);
}

例子里,我们使用了3个常用的全局变量:msg.sender,block.number和msg.data,他们分别代表请求发起地址,当前区块高度,和请求数据。

以太单位

Solidity中不存在小数点,以0代替为小数点,来确保交易的精确度,并且防止精度的损失,利用以太单位可以避免误算的问题,方便程序员在合约中处理货币交易。
wei: 1
gwei: 1e9 = 1000000000
ether: 1e18 = 1000000000000000000

function weiUnit() external pure returns(uint) {
    
    
    assert(1 wei == 1e0);
    assert(1 wei == 1);
    return 1 wei;
}

function gweiUnit() external pure returns(uint) {
    
    
    assert(1 gwei == 1e9);
    assert(1 gwei == 1000000000);
    return 1 gwei;
}

function etherUnit() external pure returns(uint) {
    
    
    assert(1 ether == 1e18);
    assert(1 ether == 1000000000000000000);
    return 1 ether;
}

6引用类型 数组和结构体

这里不是很难,但是我不是很理解这个memory的array和正常的到底有啥子区别?

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract ArrayTypes {
    
    

    // 固定长度 Array
    uint[8] array1;
    bytes1[5] array2;
    address[100] array3;

    // 可变长度 Array
    uint[] array4;
    bytes1[] array5;
    address[] array6;
    bytes array7;

    // 初始化可变长度 Array
    uint[] array8 = new uint[](5);
    bytes array9 = new bytes(9);
    //  给可变长度数组赋值
    function initArray() external pure returns(uint[] memory){
    
    
        uint[] memory x = new uint[](3);
        x[0] = 1;
        x[1] = 3;
        x[2] = 4;
        return(x);
    }  
    function arrayPush() public returns(uint[] memory){
    
    
        uint[2] memory a = [uint(1),2];
        array4 = a;
        array4.push(3);
        return array4;
    }
}


contract StructTypes {
    
    
    // 结构体 Struct
    struct Student{
    
    
        uint256 id;
        uint256 score; 
    }
    Student student; // 初始一个student结构体
    //  给结构体赋值
    // 方法1:在函数中创建一个storage的struct引用
    function initStudent1() external{
    
    
        Student storage _student = student; // assign a copy of student
        _student.id = 11;
        _student.score = 100;
    }

    // 方法2:直接引用状态变量的struct
    function initStudent2() external{
    
    
        student.id = 1;
        student.score = 80;
    }
    
    // 方法3:构造函数式
    function initStudent3() external {
    
    
        student = Student(3, 90);
    }

    // 方法4:key value
    function initStudent4() external {
    
    
        student = Student({
    
    id: 4, score: 60});
    }
}

contract EnumTypes {
    
    
    // 将uint 0, 1, 2表示为Buy, Hold, Sell
    enum ActionSet {
    
     Buy, Hold, Sell }
    // 创建enum变量 action
    ActionSet action = ActionSet.Buy;

    // enum可以和uint显式的转换
    function enumToUint() external view returns(uint){
    
    
        return uint(action);
    }
}

7映射 hash

contract hashmap_test{
    
    
	// 创建了一个hashmap并定义了key和value的类型
    mapping (uint => address) public idToaddress;
    mapping (address => address) public swappair;
    // 规则1. _KeyType不能是自定义的 下面这个例子会报错
    // 我们定义一个结构体 Struct
    // struct Student{
    
    
    //    uint256 id;
    //    uint256 score; 
    //}
    // mapping(Struct => uint) public testVar;
    // 调用一个写这个hashmap的函数
    function writemao(uint key,address value) public {
    
    
        idToaddress[key] = value;
    }
}

8初始化变量值

bool public _bool; // false
string public _string; // ""
int public _int; // 0
uint public _uint; // 0
address public _address; // 0x0000000000000000000000000000000000000000

enum ActionSet {
    
     Buy, Hold, Sell}
ActionSet public _enum; // 第1个内容Buy的索引0

function fi() internal{
    
    } // internal空白函数
function fe() external{
    
    } // external空白函数 

// delete操作符
bool public _bool2 = true; 
function d() external {
    
    
    delete _bool2; // delete 会让_bool2变为默认值,false
}

9常数constant和immutable

只有数值变量可以声明constant和immutable;string和bytes可以声明为constant,但不能为immutable。

// constant变量必须在声明的时候初始化,之后不能改变
uint256 constant CONSTANT_NUM = 10;
string constant CONSTANT_STRING = "0xAA";
bytes constant CONSTANT_BYTES = "WTF";
address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;

// immutable变量可以在constructor里初始化,之后不能改变
uint256 public immutable IMMUTABLE_NUM = 9999999999;
// 在`Solidity v8.0.21`以后,下列变量数值暂为初始值
address public immutable IMMUTABLE_ADDRESS; 
uint256 public immutable IMMUTABLE_BLOCK;
uint256 public immutable IMMUTABLE_TEST;

构造函数和修饰器
构造函数每个合约只能定义一个,我认为这个就类似rust里的懒加载lazy_Static,用来初始化一些合约的参数。

address owner; // 定义owner变量

// 构造函数
constructor(address initialOwner) {
    
    
    owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
}

看看完全的代码

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

contract Owner {
    
    
   address public owner; // 定义owner变量

   // 构造函数
   constructor(address initialOwner) {
    
    
      owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
   }

   // 定义modifier
   modifier onlyOwner {
    
    
      require(msg.sender == owner); // 检查调用者是否为owner地址
      _; // 如果是的话,继续运行函数主体;否则报错并revert交易
   }

   // 定义一个带onlyOwner修饰符的函数
   function changeOwner(address _newOwner) external onlyOwner{
    
    
      owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
   }
}

10控制流

contract control_stream{
    
    
    function ifelsetest(uint256 _number) public pure returns (bool){
    
    
        if(_number == 1){
    
    
            return(true);
        }else {
    
    
            return(false);
        }
    }

    function looptest() public pure returns(uint){
    
    
        uint num = 1;
        for(uint i = 0;i < 10;i++){
    
    
            num +=1;
        }
        return(num);
    }

    function whileloop() public pure returns(uint){
    
    
        uint num = 0;
        while(num < 10){
    
    
            num += 1;
        }
        return num;
    }
}

练习:尝试写排序

目前是失败的 后续会重新写一下这个问题

contract sort{
    
    
    uint[5] public arr = [7,8,9,2,1];

    function popsort() public{
    
    
        for (uint i = 0;i<arr.length-1;i++){
    
    
            for(uint j = i;j<arr.length-1;j++){
    
    
                // 如果第一个大于第二个
                if (arr[j] > arr[j+1]){
    
    
                    swap(j,j+1);
                }
            }
        }
    }

    function swap(uint index1,uint index2) internal{
    
    
        uint temp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = temp;
    }

    function get_num() public returns(uint){
    
    
        return(arr[4]);
    }
}

11构造函数和修饰器

contract Owner {
    
    
   address public owner; // 定义owner变量

   // 构造函数
   constructor(address initialOwner) {
    
    
      owner = initialOwner; // 在部署合约的时候,将owner设置为传入的initialOwner地址
   }

   // 定义modifier
   modifier onlyOwner {
    
    
      require(msg.sender == owner); // 检查调用者是否为owner地址
      _; // 如果是的话,继续运行函数主体;否则报错并revert交易
   }

   // 定义一个带onlyOwner修饰符的函数
   function changeOwner(address _newOwner) external onlyOwner{
    
    
      owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
   }
}

12事件

Solidity中的事件(event)是EVM上日志的抽象,它具有两个特点:

响应:应用程序(ethers.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。
经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas。

13继承

简单的继承

contract Yeye{
    
    
    event log(string msg);
    function hip() public virtual {
    
    
        emit log("yeye");
    }
    function pop() public virtual {
    
    
        emit log("yeye");
    }
    function yeye() public virtual {
    
    
        emit log("yeye");
    }
}

contract baba is Yeye{
    
    
    function hip() public virtual override {
    
    
        emit log("baba");
    }
    function pop() public virtual override {
    
    
        emit log("baba");
    }
}

修饰器也是可以继承的

14抽象合约和接口

首先了解一下抽象合约,如果一个合约中的某个函数缺少主体,就把这个合约定义为abstract,否则会产生编译错误。

abstract constract insert_test{
    
    
	function pop_sort(uint[] memory a) public pure virtual returns(uint[] memory);
}
// 上述的合约中的函数代码是;结尾的

接口的定义:

1.不能包含状态变量
2.不能包含构造函数
3.不能继承除接口外的其他合约
4.所有的函数都应该是external的
5.非抽象合约继承接口必须实现接口定义的所有功能

我们以ERC721接口合约IERC721为例,它定义了3个event和9个function,所有ERC721标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。

interface IERC721 is IERC165 {
    
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
    function balanceOf(address owner) external view returns (uint256 balance);

    function ownerOf(uint256 tokenId) external view returns (address owner);

    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    function transferFrom(address from, address to, uint256 tokenId) external;

    function approve(address to, uint256 tokenId) external;

    function getApproved(uint256 tokenId) external view returns (address operator);

    function setApprovalForAll(address operator, bool _approved) external;

    function isApprovedForAll(address owner, address operator) external view returns (bool);

    function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}

15异常

本章的主要目的时了解三种抛出异常的方法,并比较对应的gas fee消耗:

error 
require
assert

异常的主要目的是为了debug
写到这里发现后期的一些内容总是无法督促自己把样例代码写完,现在改为按章节去更新。并新开一个专栏。

筑基期

首先看一个存储合约示例

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract SimpleStorage {
    
    
    uint storedData;

    function set(uint x) public {
    
    
        storedData = x;
    }

    function get() public view returns (uint) {
    
    
        return storedData;
    }
}

第一行告诉您,源代码是根据GPL3.0版本授权的。 在发布源代码是默认的情况下,机器可读的许可证说明是很重要的。
下一行指定源代码是为Solidity 0.4.16版本编写的,或该语言的较新版本,直到但不包括0.9.0版本。 这是为了确保合约不能被新的(有重大改变的)编译器版本编译,在那里它可能会有不同的表现。 Pragmas 是编译器关于如何处理源代码的常用指令 (例如, pragma once )。
这两行类似于配置,contract就类似于结构体或者类了。
我们在类中定义了一个unit数据,并设置了set和get函数来获取这个值。我一度很好奇这个view,同时这个public 放置的位置也很让我疑惑。搜索后发现

public 关键字用于声明智能合约的状态变量。当一个状态变量被声明为 public 时,
它会自动生成一个同名的getter函数,允许外部访问这个状态变量的值。
此外,public 状态变量的值会被存储在区块链上,并且可以通过合约的接口进行访问。
view 关键字用于修饰智能合约的函数。一个被标记为 view 的函数表示这个函数不会修改区块链上的任何状态,
它只用于读取数据。调用这样的函数不会产生交易,也就是说,
它不会消耗gas(以太坊网络的费用),因为它不会引起状态变化。这使得 view 函数非常适合用于查询操作。

我目前把这个public理解为普遍的public,因为我不知道他在智能合约里起到一个什么样的作用。

该合约能完成的事情并不多(由于以太坊构建的基础架构的原因), 它能允许任何人在合约中存储一个单独的数字,并且这个数字可以被世界上任何人访问, 且没有可行的办法阻止您发布这个数字。 当然,任何人都可以再次调用 set ,传入不同的值,覆盖您的数字, 但是这个数字仍会被存储在区块链的历史记录中。
神奇之处来了,修改的历史居然会被保存。

接下来我们继续看下一个例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract Coin {
    
    
    // 关键字 "public" 使变量可以从其他合约中访问。
    address public minter;
    mapping(address => uint) public balances;

    // 事件允许客户端对您声明的特定合约变化做出反应
    event Sent(address from, address to, uint amount);

    // 构造函数代码只有在合约创建时运行
    constructor() {
    
    
        minter = msg.sender;
    }

    // 向一个地址发送一定数量的新创建的代币
    // 但只能由合约创建者调用
    function mint(address receiver, uint amount) public {
    
    
        require(msg.sender == minter);
        balances[receiver] += amount;
    }

    // 错误类型变量允许您提供关于操作失败原因的信息。
    // 它们会返回给函数的调用者。
    error InsufficientBalance(uint requested, uint available);

    // 从任何调用者那里发送一定数量的代币到一个地址
    function send(address receiver, uint amount) public {
    
    
        if (amount > balances[msg.sender])
            revert InsufficientBalance({
    
    
                requested: amount,
                available: balances[msg.sender]
            });

        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

address public minter; 这一行声明了一个可以被公开访问的 address 类型的状态变量。 address 类型是一个160位的值,且不允许任何算数操作。 这种类型适合存储合约地址或 外部账户 的密钥对。

猜你喜欢

转载自blog.csdn.net/qq_54384621/article/details/142024989