首页 > 其他分享 >Solidity学习笔记-1

Solidity学习笔记-1

时间:2024-11-16 20:07:14浏览次数:1  
标签:function 函数 Solidity 笔记 学习 uint contract address public

01.Hello World

开发工具

Remix

// SPDX-License-Identifier: MIT
// 软件许可,不写编译会出现警告
// 版本,“0.8.21”-->不能小于这个版本,"^"-->不能大于0.9.0
pragma solidity ^0.8.21;
// 创建合约
contract HelloWorld {
    string public helloworld = "Hello World!";
}

编译

image-20241104192640489

部署运行

Remix会提供虚拟机来模拟测试链,并提供了多个测试账户(有100ETH)来运行智能合约,部署后就能运行看到效果:

image-20241104193515370

02.值类型

重要的就是address,bytes32这些,其余的和C差不多;

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

contract ValueTypes {
    // 布尔值
    bool public _bool = true;
    bool public _false = false;
    // 运算符:与C差不多
    // ! && || == !=

    // -----------------------------------------------------

    // 无符号整数,无负数
    // uint = uint256
    uint public _uint = 123;
    // 有符号整数
    // int = int256
    int public _int = -123;
    // 运算符:
    // + - * / **(幂) %(取余)

    // -----------------------------------------------------

    // *******特有的类型*******
    // 普通地址
    // 存储20字节的值(以太坊地址就是20字节)
    address public addr_1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
    // payable address
    // 多了transfer和send两个成员方法,用于接收转账
    address payable public addr_2 = payable(addr_1);

    // -----------------------------------------------------

    // 定长字节数组(数组长度在声明后不能改变)
    // bytes1,bytes8,bytes32
    bytes32 public b32 = "ABCDEFG"; // 0x4142434445464700000000000000000000000000000000000000000000000000
    bytes1 public b1 = b32[0];  // 0x41

    // -----------------------------------------------------

    // 枚举 enum,比较冷门
    enum Actions { Eat, Drink, Smell}
    // Eat-->0, Drink-->1, Smell-->2
    // 创建枚举对象(类似于创建对象)
    Actions act = Actions.Drink;
}

03.函数

solidity中函数的形式:

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

03_01.function

声明的固定用法

03_02.function name

函数名

03_03.parameter types

函数参数,输入的变量类型和名称

03_04.internal|external|public|private

函数可见性说明符(必须明确指定)

public:内外均可访问

private:只能内部访问,继承的合约也不能访问

external:只能从外部访问(内部可以通过this.xxx()访问)

internal:只能从内部访问,继承的合约可以用

public | private | internal可以修饰状态变量,没有明确标明的会默认internalpublic修饰的会自动生成getter()函数,用于查询数值;

03_05.pure|view|payable

决定函数权限/功能的关键字

payable:可转入的,带着它,运行时可给合约转账

引入pure和view的原因是以太坊交易需要支付gas,而状态变量存在链上的gas fee很高,若运行不改变状态变量,就不需要付gas

包含pure或者view的函数执行时不需要支付gas;

pure:纯粹的,不能读不能写

view:可以看的,能读但不能写

03_06.returns (return types)

函数返回的值,类型

03_07.示例

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

contract Functions {
    uint public num = 100;
    // 在这添加pure或view就会报错,因为该函数对状态变量修改了
    function Add() external {
        num += 1;
    }

    function Add_pure(uint temp_pure) external pure returns(uint temp_pure_add){
        // 在其中读取num(状态变量)也会报错
        temp_pure_add = temp_pure + 1;
    }

    function Add_view() external view returns(uint temp_view_add){
        // 可以读取,但不能改写
        temp_view_add = num + 1;
    }

    // -----------------------------------------------------------------

    function Minus_internal() internal {
        num -= 10;
    }
    // 调用内部函数
    function Minus_internal_Call() external {
        Minus_internal();
    }

    // -----------------------------------------------------------------

    // 给合约转账,并返回余额
    function Pay() external payable returns (uint balance){
        balance = address(this).balance;
    }
}

最后一个Pay函数,调用时,可以在value处填写金额,运行后便看到账户上多了23ETH;

(不是部署的时候填写,是调用该函数前填写,部署合约是向0x0000...000发送的交易)

image-20241104230947368

04.函数返回

Solidity中与函数返回相关的有returnreturns两个关键字;

return:在函数中

returns:跟在函数定义后面

示例:

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

contract Functions {
    // 命名式返回
    function Cacl_1(uint num1, uint num2) public pure returns(uint add_num, uint minus_num){
        add_num = num1 + num2;
        minus_num = num1 - num2;

        // 可以不加
        return(add_num, minus_num);
    }

    // 非命名式返回
    function Cacl_2() public pure returns(uint, bool, uint[3] memory){
        // 数组类型后面的memory
        // 数组类型返回值默认要加memory

        // [uint(1), 2, 3]这么写的原因:
        // [1, 2, 3]默认是uint8,返回时必须强转为uint256
        return(100, true, [uint(1), 2, 3]);
    }

    // 解构式赋值
    function getVal() public pure{
        uint a;
        bool b;
        uint[3] memory c;
        // 读取全部返回值
        (a, b, c) = Cacl_2();
        
        // 读取部分返回值
        // 不读取的留空
        bool b2;
        (, b2, ) = Cacl_2();
    }
}

05.变量数据存储和数据域

引用类型:数组、结构体;

三类数据存储位置(不同存储位置的变量消耗的gas不同):

storage:存储在链上,消耗的gas多,合约中的状态变量

memory:内存中,消耗的gas少,变长的数据类型必须用memory修饰,如string、bytes、array、struct

calldata:内存中,消耗的gas少,与memory不同的是其不能修改,常用做函数的参数

当声明一个storage类型当变量时(也就是状态变量),对它的赋值是引用操作(类似C中的指针),修改其中一个,另一个也修改:

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

contract Functions {
    uint[] arry = [1, 1, 1];

    function Change_storage() public {
        uint[] storage arry_2 = arry;
        // 此时arry中的arry[1]也为0
        arry_2[1] = 0;
    }
}

05_01.状态变量

存储在链上,消耗的gas高,公开的,所有合约均可访问(像C中的全局变量);

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

contract Functions {
    // 状态变量声明处
    uint[] arry = [1, 1, 1];
    // 更多状态变量

    function fun1() public {
    }
    function func2() public {
    }
    // ...
}

05_02.局部变量

同C语言中的局部变量,gas消耗低;

05_03.全局变量

与C语言中的全局变量不同!

此处的全局变量都是solidity已经预留的关键字,不需要声明直接使用:

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

contract Functions {
    function global() public view returns(address, uint, bytes memory) {
        // 消息发送者(当前是谁调用的)
        address sender = msg.sender;
        // 当前区块的number
        uint blockNum = block.number;
        // 消息里的data
        bytes memory data = msg.data;
        return (sender, blockNum, data);
    }
}

完整的全局变量列表:https://learnblockchain.cn/docs/solidity/units-and-global-variables.html#special-variables-and-functions

05_04.全局变量-以太单位和时间单位

以太单位

Solidity中不存在小数点,以0代替小数点,方便货币交易额度的计算:

wei:1

gwei:1e9 = 1000000000wei

ether:1e18 = 1000000000000000000wei

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;
}

时间单位

可以规定一个操作必须在一周内、一个月内完成,这样能够使得合约的执行更加精准,不会因为技术的误差而影响合约的结果;

时间单位在solidity中是一个很重要的概念,有助于提高合约的可读性和维护性;

seconds:1

minutes:60 * 1

hours:60 * 60 * 1

days:24 * 60 * 60 * 1

weeks:7 * 24 * 60 * 60 * 1

function secondsUnit() external pure returns(uint) {
    assert(1 seconds == 1);
    return 1 seconds;
}

function minutesUnit() external pure returns(uint) {
    assert(1 minutes == 60);
    assert(1 minutes == 60 seconds);
    return 1 minutes;
}

function hoursUnit() external pure returns(uint) {
    assert(1 hours == 3600);
    assert(1 hours == 60 minutes);
    return 1 hours;
}

function daysUnit() external pure returns(uint) {
    assert(1 days == 86400);
    assert(1 days == 24 hours);
    return 1 days;
}

function weeksUnit() external pure returns(uint) {
    assert(1 weeks == 604800);
    assert(1 weeks == 7 days);
    return 1 weeks;
}

06.引用类型

引用类型有两个:arraystruct

06_01.array数组

array有固定长度数组和可变长度数组;

// 固定长度
uint[10] array_1;
// 可变长度
uint[] array_2;

// 特别的,bytes声明时不用加[]
bytes array_3;// 可变长
bytes1[5] array_4;// 固定长

不能用byte[]来声明单字节数组,需要用bytes或者bytes1[]来声明,bytes比bytes[]更省gas

// 对于memory修饰的动态数组,可以用new来创建
// 必须声明长度,且长度不可改变
uint[] memory a1 = new uint[](10);
bytes memory a2 = new bytes(20);

数组里面的类型是以第一个元素为准的,默认是uint8

[1, 2, 3]	// 这里面全是uint8
[uint(1), 2, 3]	// 这里面全是uint类型

数组成员:

length:元素数量,memory创建的数组长度是固定的;

push():只有动态数组有,在数组最后添加元素,默认是0,push(10)就会添加10;

pop():只有动态数组有,移除最后一个元素;

06_02.struct结构体

结构体的一些操作,基本与其他语言类似:

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

contract Functions {
    // 创建结构体
    // 里面可以是原始类型,也可以是引用类型(数组,结构体)
    struct Student{
        uint256 id;
        uint256 score; 
    }
    // 初始一个student结构体,状态变量
    Student student;

    // 赋值方法1
    function initStudent() public {
        student.id = 1;
        student.score = 200;
    }

    // 赋值方法2
    function initStudent_1() public pure {
        Student memory student_1;
        student_1.id = 2;
        student_1.score = 100;
    }

    // 构造函数式
    function createStudent() public pure {
        // 直接
        Student memory student_2 = Student(3, 300);
        // key value
        student_2 = Student({id:4, score:400});
    }
}

07.映射类型mapping

可以理解为哈希表,一个key 对应一个value

  1. 存储位置必须是storage

  2. 不能用于public修饰函数的参数或返回值中(因为mapping记录的是一种关系key-value);

  3. 若声明为public,系统会自动创建一个getter函数,可以通过key来查询value;

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

contract Functions {
    // 声明映射的格式
    // id 到 地址
    mapping (uint => address) public idToAddress;

    struct Wallet{
        uint id;
        address wallet_addr;
    }
    // 下面这个会报错
    // key只能用内置的类型,value可以随意
    // mapping (Wallet => address) public walletToAddress;
    mapping (address => Wallet) public addressToWallet;

    // 添加key-value对
    // wallet必须指明memory,因为是引用类型
    function addMappingValue(address addr, Wallet memory wallet) public {
        addressToWallet[addr] = wallet;
    }
}

08.变量的初始值

08_01.值类型

bool:false

string:""

int:0

uint:0

enum:枚举中的第一个元素

address:0x000....000(全0)或者address(0)

function

internal:空白函数

external:空白函数

可以用public声明的变量的getter函数来查看:

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

contract Functions {
    // false
    bool public bool_var;
    // 0
    uint public uint_var;
    // ""
    string public string_var;
    // 全0
    address public address_var;

    enum Actions{
        Eat, Drink, Smell
    }
    // 默认第一个的索引
    Actions public act;

    // 为空
    function internal_fun() internal {}
    function external_fun() external {}
}
image-20241110214637840

08_02.引用类型

array

固定长度:所有成员均为其默认值的静态数组(比如uint就为0)

动态数组:[]

struct:所有成员设为其默认值的结构体

mapping

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

contract Functions {
    // [0, 0, 0, 0, 0]
    uint[5] public static_array;
    // '[]'
    uint[] public dynamic_array;

    struct Test{
        uint id;
        string str;
        address addr;
    }
    // id = 0, str = "", addr = 全0
    Test public test;

    // 0 => 0x00...00
    mapping (uint => string) public idToString;
}
image-20241110215847829

08_03.delete操作符

delete操作符会将变量的值变为初始值

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

contract Functions {
    // 10
    uint public uint_var = 10;

    // 10 --> 0
    function delete_var() public {
        delete uint_var;
    }
}
image-20241110220152988

09.常量

constantimmutable两个关键字来修饰常量;

  1. 状态变量用这两个关键字声明后,不能更改数值(提升安全性,节省gas);

  2. 只有数值变量可以声明constantimmutablestringbytes可以声明为constant,但不能为immutable

constant:声明的时候必须初始化,之后也不能改变;

immutable:可以在函数声明时或者构造函数中初始化;

constant严格一点,immutable更加灵活一点

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

contract Functions {
    // constant
    // 不赋值就会报错
    uint constant CONSTANT_VAR = 10;

    // -------------------------------------------
    // immutable
    // 不初始化不会报错
    uint immutable IMMUTABLE_UINT;
    address immutable IMMUTABLE_AAARESS;
    uint immutable IMMUTABLE_TEST;

    // 在构造函数里面初始化
    // 可以用address(this),block.number等全局变量来赋值
    // 也可以用自定义的函数来对常量进行初始化
    constructor(){
        IMMUTABLE_AAARESS = address(this);
        IMMUTABLE_UINT = block.number;
        IMMUTABLE_TEST = test();
    }
    // 自定义函数
    function test() public pure returns(uint){
        uint res = 999;
        return res;
    }
}

10.控制流

solidity中的一些控制流与其他语言基本类似:

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

contract Functions {
    // if else
    function Test1() public pure returns(bool) {
        uint num = 0;
        if(num == 1){
            return true;
        }else {
            return false;
        }
    }

    // for循环
    function Test2() public pure returns(uint){
        uint sum = 0;
        for(uint i = 0; i < 100; i++){
            sum += i;
        }
        return sum;
    }

    // while循环
    function Test3() public pure returns(uint){
        uint sum = 0;
        uint i = 100;
        while(i >= 0){
            sum += i;
            i -= 1;
        }
        return sum;
    }

    // do-while循环
    function Test4() public pure returns(uint){
        uint sum = 0;
        uint i = 0;
        do {
            i += 1;
            sum += 1;
        }while (i <= 100);
        return sum;
    }

    // 三元运算符
    function Test5(uint a, uint b) public pure returns(bool){
        return a >= b ? true : false;
    }
}

下面用solidity写一个插入排序;

这是Python实现的一个插入排序:

# Python实现的代码
def insertion_sort(arr):
    # 遍历从1到len(arr)的每个元素
    for i in range(1, len(arr)):
        key = arr[i]
        # 将选中的元素与已排序序列中的元素进行比较,并向后移动
        j = i - 1
        # 将比key大的元素向后移动一个位置
        while j >= 0 and key < arr[j]:
            arr[j + 1] = arr[j]
            j -= 1
        # 将key插入到正确的位置
        arr[j + 1] = key

下面将用solidity将其改写:

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

contract Functions {
    function insertion_sort(uint256[] memory arr) public pure returns (uint256[] memory)
    {
        for (uint256 i = 1; i < arr.length; i++) {
            uint256 temp = arr[i];
            uint256 j = i - 1;
            while ((j >= 0) && (temp < arr[j])) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = temp;
        }
        return arr;
    }
}

将其编译并部署:

image-20241112235849185

发现有报错;

原因就是uint类型是正整数,取到负值的话,就会报错;

正确代码:

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

contract Functions {
    function insertion_sort(uint256[] memory arr) public pure returns (uint256[] memory)
    {
        for (uint256 i = 1; i < arr.length; i++) {
            uint256 temp = arr[i];
            // 原
            // uint256 j = i - 1;
            // 将j + 1,使其取不到负数
            uint256 j = i;
            // 原
            // while ((j >= 0) && (temp < arr[j])) {
            while ((j >= 1) && (temp < arr[j - 1])) {
                // 原
                // arr[j + 1] = arr[j];
                arr[j] = arr[j - 1];
                j--;
            }
            // 原
            // arr[j + 1] = temp;
            arr[j] = temp;
        }
        return arr;
    }
}

image-20241113000510152

11.构造函数和修饰器

11_01.构造函数

constructor是一个特殊的函数;可以用它来初始化一些参数(之前immutable声明的变量可以在这边初始化)

  1. 每个合约可以定义一个
  2. 部署合约时,自动执行
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Functions {
    address owner;
    // 初始化address
    constructor(address initAddress){
        owner = initAddress;
    }
}

构造函数在不同的语言版本中写法不一致,旧的版本(0.4.22之前)不用constructor关键字,而是使用与合约同名的函数作为构造函数;

现在已不使用;

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

contract Functions {
    address owner;
    // 初始化address
    function Functions(address initAddress) public {
        owner = initAddress;
    }
}

11_02.修饰器

modifiersolidity中特有的语法;声明函数拥有的特性,减少代码冗余;

主要的一些场景:运行函数前的检查(地址、变量、余额等)、权限控制等等;

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

contract Functions {
    address public owner;
    // 初始化address
    constructor(address initAddress){
        owner = initAddress;
    }

    modifier only_owner_can_do{
        // 检查调用者是否是owner地址
        require(msg.sender == owner);
        // 如果是的话,继续运行,不是则报错并revert交易
        _;
    }

    // 真正的owner来才能调用
    function realowner_changeOwner(address newAddress) external only_owner_can_do{
        owner = newAddress;
    }
}

通过构造器传入initAddress

image-20241113003505128

当前账户(owner)调用real owner_changeOwner函数修改为新的owner:0x0A0...,并且能执行成功:

image-20241113004417446

切换钱包(让当前的账户不为0x0A0...这个),调用发现失败:

image-20241113004711348

上面这个实例,实现了一个简单的权限控制。

12.事件

Event是EVM上日志的抽象;它有两个特点:

  1. 响应:应用程序可以通过RPC接口订阅和监听这些事件,并在前端做出响应;
  2. 经济:事件是EVM上比较经济的存储方式;(每个大概2000gas,而变量需要20000gas)

12_01.事件的声明

// 以event开头,接着是事件名称,括号里写好事件需要记录的变量类型及名字
// 以ERC20代币合约的Transfer事件
event Transfer(address indexed from, address indexed to, uint256 value);

可以看到上面这个Event记录了3个变量:fromto value

其中indexed关键字的意思是,将这些变量保存在EVM日志的topics中,方便后续检索;

12_02.事件的释放

在事件的前面加emit关键字即可,可以在函数中释放事件;

emit Transfer(from, to, value);

12_03.EVM日志

EVM用日志Log来存储事件,每条日志记录都包含主题topics和数据data

image-20241114230454640

topics

这个部分是个数组,用于描述事件,但长度不能超过4;

第一个元素是事件的签名:

比如上面这个事件:
keccak256("Transfer(address,address,uint256)")
// 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

除了事件的hash,此处还可以包含最多3个indexed参数,都可以记录在这;

indexed标记的参数可以理解为索引事件的index,方便后面索引;

每个indexed参数的大小固定为256比特;如果参数太大了,比如一些字符串什么的,会自动计算哈希存储在主题中;

data

可以看到带有indexed标记的被存储在topics中,那不带indexed的参数就会被存储在data中;

这部分的变量不能被直接检索,但可以存储任意大小的数据(比如复杂的数据结构,很长的数组、字符串等等,这些即使存储在topics中,也是以hash的形式);

此外,data部分的变量在存储上消耗的gas比topics上的更少;

gas比较:状态变量 > 事件(topics > data)

12_04.代币转账演示

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

contract Functions {
    // 一个地址-余额的映射
    mapping (address => uint256) public address_money;

    // 以event开头,接着是事件名称,括号里写好事件需要记录的变量类型及名字
    // 以ERC20代币合约的Transfer事件
    event Transfer(address indexed from, address indexed to, uint256 value);

    // from向to转账
    function transfer_fun(address from, address to, uint256 amount) external {
        // 给转账的地址赠予一些代币
        address_money[from] = 100000000;
        // 先扣钱(不然会有漏洞,可以搜索DAO)
        address_money[from] -= amount;
        // 再赋值
        address_money[to] += amount;
        // 释放事件
        emit Transfer(from, to, amount);
    }
}
image-20241114230357599

13.继承

inheritance继承是面向对象语言的重要组成部分,可以显著减少重复代码;可以把合约看作是对象;

virtual:父合约中的函数若希望子合约重写,需要加上这个关键字

override:子合约重写了父合约中的函数,需要加上这个关键字

若用override修饰了public变量,会重写与变量同名的getter函数:mapping(address=>uint256) public override balanceOf;

13_01.简单继承

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

// 均输出"YeYe"
contract YeYe {
    event Log(string str);

    function Ye1() public virtual {
        emit Log("YeYe");
    }

    function Ye2() public virtual {
        emit Log("YeYe");
    }

    function Ye3() public virtual {
        emit Log("YeYe");
    }
}

// 定义BBB继承AAA
// 改写了两个函数,又自己写了个新函数
contract BaBa is YeYe{
    // 改写函数
    function Ye1() public virtual override {
        emit Log("BaBa");
    }
    // 改写函数
    function Ye2() public override {
        emit Log("BaBa");
    }
    // 新写函数
    function Ba1() public virtual {
        emit Log("BaBa");
    }
}

部署YeYe合约:

image-20241115005452716

部署BaBa合约(可以看到有四个函数,因为自己实现了一个父合约没有的函数):

image-20241115005544540

执行结果:

image-20241115005755904 image-20241115010056513

13_02.多重继承

一个合约可以继承多个合约(一对多);

  1. 继承时辈分要按从高到低的顺序排,比如contract 儿子 is 爷爷, 父亲
  2. 若某一个函数在它继承的多个合约中都存在,必须进行重写,不然会报错(比如上面这个 爷爷父亲合约中都有个不一样的say函数, 儿子 调用say函数时若不重写,根本不知道到底调哪一个)
  3. 重写在多个父合约中都重名的函数时,需要在override关键词后面加上所有父合约的名字,比如上面这个:function say() public virtual override(爷爷, 父亲){}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 均输出"YeYe"
contract YeYe {
    event Log(string str);

    function Ye1() public virtual {
        emit Log("Ye1");
    }

    function Ye2() public virtual {
        emit Log("Ye2");
    }

    function Ye3() public virtual {
        emit Log("Ye3");
    }
}

// 定义BBB继承AAA
// 改写了两个函数,又自己写了个新函数
contract BaBa is YeYe{
    // 改写函数
    function Ye1() public virtual override {
        emit Log("override Ye1");
    }
    // 改写函数
    function Ye2() public virtual override {
        emit Log("override Ye2");
    }
    // 新写函数
    function Ba1() public virtual {
        emit Log("new func Ba1");
    }
}

contract Erzi is YeYe, BaBa{
    // 必须重写(因为YeYe和BaBa中的这个函数不一样)
    function Ye1() public virtual override (YeYe, BaBa){
        emit Log("Erzi override Ye1");
    }
    // 必须重写(因为YeYe和BaBa中的这个函数不一样)
    function Ye2() public virtual override (YeYe, BaBa){
        emit Log("Erzi override Ye2");
    }
    // 重写BaBa的Ba1函数
    function Ba1() public virtual override (BaBa){
        emit Log("Erzi override Ba1");
    }
}

可以看到Erzi合约中有4个函数(2个必须重写,1个重写的BaBa的,还有一个是YeYe自带的),此处展示了重写了其父合约们共有的一个Ye1函数:

image-20241116154152550

13_03.修饰器的继承

修饰器Modifier同样可以继承,与函数继承类似,在相依地方加上virtualoverride即可;

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

contract Modifier_Origin{
    // 起初的修饰器,判断数字是否是7的倍数
    // 加了virtual,可以对其进行改写
    modifier judgeNum(uint a) virtual {
        require(a % 7 == 0);
        _;
    }
}

// 继承,但不重写修饰器
contract Unoverride is Modifier_Origin{
    // 首先修饰器判断输入的数字是否是7的倍数
    // 是的话直接返回a / 7
    // 不是则直接退出
    function judgeInputNumber_Unoverride(uint a) public pure judgeNum(a) returns(uint){
        return a / 7;
    }
}

// 继承,同时重写修饰器
contract Override is Modifier_Origin{
    // 重写修饰器
    // 判断数字是否是10的倍数
    modifier judgeNum(uint a) virtual override {
        require(a % 10 == 0);
        _;
    }
    // 修饰器判断是否为10的倍数
    // 之后返回a / 10
    function judgeInputNumber_Override(uint a) public pure judgeNum(a) returns(uint){
        return a / 10;
    }
}

未改写修饰器运行的函数(输入10,应该是不通过检查):

image-20241116162314320

未改写修饰器运行的函数(输入14,应该是通过检查,并返回2):

image-20241116162448333

改写修饰器后运行的函数(输入14,应该是不通过检查):

image-20241116162611710

改写修饰器后运行的函数(输入10,应该是通过检查,并返回1):

image-20241116162710747

13_04.构造函数的继承

子合约有两种方式继承父合约构造函数;

  1. 继承时直接声明构造函数的参数,例如contract B is A(10)
  2. 在子合约的构造函数中声明构造函数的参数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract YeYe{
    uint public num;
    // 构造函数给状态变量赋值
    constructor(uint a){
        num = a;
    }
}

// 继承时直接声明构造函数的参数
contract BaBa is YeYe(10 * 10){}

// 在子合约的构造函数中
contract Erzi is YeYe{
    constructor(uint a) YeYe(a * a){
        // 子合约的构造函数内容
    }
}
image-20241116164559877

13_05.调用父合约的函数

有两种方法调用父合约里面的函数:

  1. 直接调用,父合约名.函数名
  2. super关键字,子合约可以利用这个关键字调用最近的父合约函数(继承时最右边的合约),例如contract Erzi is YeYe, BaBa,调用的就是BaBa中的函数;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 均输出"YeYe"
contract YeYe {
    event Log(string str);

    function Ye1() public virtual {
        emit Log("Ye1");
    }

    function Ye2() public virtual {
        emit Log("Ye2");
    }
}

contract BaBa is YeYe{
    // 改写函数
    function Ye1() public virtual override {
        emit Log("override Ye1");
    }
    // 新写函数
    function Ba1() public virtual {
        emit Log("new func Ba1");
    }
}

contract Erzi is YeYe, BaBa{
    // 必须重写的函数
    function Ye1() public virtual override (YeYe, BaBa){
        emit Log("Erzi override Ye1");
    }

    // 直接调用
    function callParent() public {
        // 期望输出"Ye1"
        YeYe.Ye1();
    }

    // 利用super调用最近的父合约函数
    function callParent_Super() public {
        // 期望输出"override Ye1"
        super.Ye1();
    }
}

直接调用:

image-20241116170709891

super调用:

image-20241116170813146

13_06.钻石继承

也称菱形继承,指一个派生类同时有两个或两个以上的基类;

在这种情况下,使用super关键字时,需要注意的是其会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约;

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

/* 
继承树:
   God
  /   \
Adam  Eve
  \   /
  people

13_05的继承树(对比一下为什么不是钻石继承)
YeYe
 |   \
 |  BaBa
 |   /
Erzi(实质上只有一个基类)
*/

contract God {
    event Log(string message);

    function foo() public virtual {
        emit Log("God.foo called");
    }

    function bar() public virtual {
        emit Log("God.bar called");
    }
}

contract Adam is God {
    // 预期输出
    // "Adam.foo called"
    // "God.foo called"
    function foo() public virtual override {
        emit Log("Adam.foo called");
        super.foo();
    }
    // 预期输出
    // "Adam.bar called"
    // "God.bar called"
    function bar() public virtual override {
        emit Log("Adam.bar called");
        super.bar();
    }
}

contract Eve is God {
    // 预期输出
    // "Eve.foo called"
    // "God.foo called"
    function foo() public virtual override {
        emit Log("Eve.foo called");
        super.foo();
    }
    // 预期输出
    // "Eve.bar called"
    // "God.bar called"
    function bar() public virtual override {
        emit Log("Eve.bar called");
        super.bar();
    }
}

contract people is Adam, Eve {
    // 预期输出
    // "Eve.foo called"
    // "Adam.foo called"
    // "God.foo called"
    function foo() public override(Adam, Eve) {
        super.foo();
    }
    // 预期输出
    // "Eve.bar called"
    // "Adam.bar called"
    // "God.bar called"
    function bar() public override(Adam, Eve) {
        super.bar();
    }
}

// 换继承顺序后,输出内容也会变(Adam, Eve)-->(Eve, Adam)
// contract people is Eve, Adam {
//     // 预期输出
//     // "Adam.foo called"
//     // "Eve.foo called"
//     // "God.foo called"
//     function foo() public override(Adam, Eve) {
//         super.foo();
//     }
//     // 预期输出
//     // "Adam.bar called"
//     // "Eve.bar called"
//     // "God.bar called"
//     function bar() public override(Adam, Eve) {
//         super.bar();
//     }
// }

有点像递归,contract people is Adam, Eve从右向左,先Eve,再Adam,最后God

虽然EveAdam都是God的子合约,但整个过程God只会被调用了一次;原因是Solidity借鉴了Python的方式,强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序;

输出结果:

image-20241116174715612

14.抽象合约和接口

14_01.抽象合约

若一个合约中至少有一个未实现的函数,即function XXX() public {},则必须将合约标记为abstract,不然编译会报错;

同时,未实现的函数还得加上virtual,以便子合约重写;

14_02.接口

接口类似抽象合约,但它里面不实现任何具体功能;

接口的规则:

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

如果合约实现了某种接口(比如ERC20ERC721),其他Dapps和合约就知道如何与它交互;

因为接口提供了两个重要的消息:

  1. 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)(比如前面的这个:keccak256("Transfer(address,address,uint256)")
  2. 接口id

另外,接口与合约ABI等价,可以相互转换;编译接口可以得到合约的ABI,利用abi-to-sol工具,也可以将ABI json文件变为sol文件;

ERC721接口合约:

// 有3个事件和9个函数
// 所有ERC721标准的NFT都实现了这些函数
// 函数体都用';'替代了
interface IERC721 is IERC165 {
    // 在转账时被释放,记录代币的发出地址、接收地址、tokenid
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    // 在授权时被释放,记录发出地址、被授权地址、tokenid
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    // 在批量授权时被释放,记录批量授权的发出地址、被授权地址、授权与否
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
    // 返回某地址的NFT持有量
    function balanceOf(address owner) external view returns (uint256 balance);
    // 返回某tokenid的主人
    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;
    // 授权另一个地址使用你的NFT
    function approve(address to, uint256 tokenId) external;
    // 查询tokenid被批准给了哪个地址
    function getApproved(uint256 tokenId) external view returns (address operator);
    // 将自己持有的某系列NFT批量授权给某个地址
    function setApprovalForAll(address operator, bool _approved) external;
    // 查询某地址的NFT是否批量授权给了另一个地址
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    // 安全转账的重载函数
    function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}

14_03.什么时候使用接口

如果我们知道一个合约实现了ERC721接口,我们不需要知道它具体的代码实现,就可以与其相互交互;

若某个代币实现了ERC721接口的功能,我们不需要知道它的源码,只需要知道它的合约地址,用IERC721接口就可以与其交互:

contract interactCoin{
    // 通过代币的地址创建接口合约变量
    IERC721 COIN = IERC721(0x.....);

    // 查询代币的持有量
    function balanceOfCOIN(address owner) external view returns (uint256 balance){
        // 直接调用接口中的查询
        return COIN.balanceOf(owner);
    }

    // 安全转账
    function safeTransferFromCOIN(address from, address to, uint256 tokenId) external {
        // 直接调用接口中的安全转账
        COIN.safeTransferFrom(from, to, tokenId);
    }
}

15.异常

写代码时的异常处理很重要;Solidity中有3种异常命令;

15_01.error

error可以方便且高效(省gas)地向用户解释操作失败的原因;

还可以在抛出异常的同时携带参数,帮助开发者更好的调试;

可以在contract之外定义异常;

error必须搭配revert来使用;

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

// 定义异常
// error TransferError();
error TransferError(address sender);

contract A{
    mapping (uint256 => address) private owners;
    
    // 给newOwner转账,但只能发送方来转
    function transfer_func_1(uint256 tokenId, address newOwner) public {
        if(owners[tokenId] != msg.sender){
            revert TransferError(msg.sender);
        }
        owners[tokenId] = newOwner;
    }
}
image-20241116191530014

15_02.require

很好用的抛出异常的方法,缺点就是gas会随着描述异常的字符串长度增加而增加;

使用方法:require(检查条件,异常描述),条件不成立则抛出异常;

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

contract A{
    mapping (uint256 => address) private owners;

    // 给newOwner转账,但只能发送方来转
    function transfer_func_2(uint256 tokenId, address newOwner) public {
        require(owners[tokenId] == msg.sender, "Transfer not owner!");
        owners[tokenId] = newOwner;
    }
}
image-20241116192007641

15_03.assert

assert命令一般用于开发人员debug,因为它不能解释抛出异常的原因(比require少个字符串);

使用方法:assert(检查条件),条件不成立,则抛出异常;

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

contract A{
    mapping (uint256 => address) private owners;

    // 给newOwner转账,但只能发送方来转
    function transfer_func_3(uint256 tokenId, address newOwner) public {
        assert(owners[tokenId] == msg.sender);
        owners[tokenId] = newOwner;
    }
}
image-20241116192454470

15_04.三种方法的gas比较(0.8.26版本)

执行函数后,可以在Debug中看到transaction costexecution cost

  1. error不带参数:24434 gas + 2874 gas
  2. error带参数:24677 gas + 3117 gas
  3. require:24749 gas + 3189 gas
  4. assert:24448 gas + 2888 gas

可以看到最小的消耗是error,其次是assert,最多的是require

参考:https://github.com/AmazingAng/WTF-Solidity

标签:function,函数,Solidity,笔记,学习,uint,contract,address,public
From: https://www.cnblogs.com/WZM1230/p/18549744

相关文章

  • UEFI 笔记 002 —— PrintLib.h
    https://github.com/tianocore/edk2/blob/master/MdePkg/Include/Library/PrintLib.h//MdePkg\Include\Library\PrintLib.h//--2024-11-16////Thisfunctionissimilarassnprintf_sdefinedinC11.UnicodeSPrint(OUTCHAR16*,INUINTN,INconstCHAR16*......
  • JDBC学习笔记(四)--JdbcUtil工具类的底层实现、解决 java.sql.SQLException: Operation
    目录(一)为什么要使用JdbcUtil工具类(二)创建一个prorperties文件1.在文件目录或src目录下,选择新建FIle2.创建properties文件 3.编写配置文件Java基础:反射4.获取资源的方式第一种第二种 ​编辑 第三种(一)为什么要使用JdbcUtil工具类问题:在编写jdbc的时候,在每一......
  • 学期:2024-2025-1 学号:20241303 《 计算机基础与程序设计》第八周学习总结
    作业信息这个作业属于哪个课程<班级的链接>(如2024-2025-1-计算机基础与程序设计)这个作业要求在哪里<作业要求的链接>(如2024-2025-1计算机基础与程序设计第八周作业)这个作业的目标<写上具体方面>计算机科学概论(第七版)第9章并完成云班课测试,《C语言程序设计》第7......
  • 【学习笔记】贪心,从会贪一点到入狱。
    为什么会入狱?因为贪太多会被抓。\(\colorbox{white}{\color{white}{会被拉清单}}\)\(\colorbox{white}{\color{white}{只要再会反悔就行了。}}\)主要写点方式方法然后记点题目。参考资料贪心还能这么玩?——浅谈进阶贪心邻项交换法比较基础的方法,主要就是证明一个状态是最优......
  • 基于Spring Boot成人自考本科教育网站设计与实现(源码+定制+讲解)在线自考教育管理系统
    博主介绍:  ✌我是阿龙,一名专注于Java技术领域的程序员,全网拥有10W+粉丝。作为CSDN特邀作者、博客专家、新星计划导师,我在计算机毕业设计开发方面积累了丰富的经验。同时,我也是掘金、华为云、阿里云、InfoQ等平台的优质作者。通过长期分享和实战指导,我致力于帮助更多学生......
  • 使用Python实现智能食品安全监测的深度学习模型
    食品安全是关乎公共健康的重要议题。随着科技的发展,深度学习技术在食品安全监测中的应用越来越广泛,通过自动化和智能化手段,可以有效提高食品质量检测的效率和准确性。本文将介绍如何使用Python实现一个智能食品安全监测的深度学习模型,并通过代码示例展示实现过程。项目概述......
  • GC优化:栈内存、span、NativeMemory、指针、池化内存 笔记
    stackalloc使用栈内存,减少GC压力varwordMatchCounts=stackallocfloat[wordCount];SpanSpan支持reinterpret_cast的理念,即可以将Span强制转换为SpanSpan支持reinterpret_cast的理念,即可以将Span强制转换为Span(其中,Span中的索引0映射到Span的前四个字节......
  • 【全栈开发(TypeOrm-Javascript)学习笔记三】
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、EntityManager二、Repository三、Find选项四、自定义Repository五、EntityManagerAPI六、RepositoryAPI总结前言本章节主要了解typeOrm框架EntityManager和Repository,学习常见的s......
  • 《Django 5 By Example》阅读笔记:p165-p210
    《Django5ByExample》学习第6天,p165-p210总结,总计46页。一、技术总结1.bookmarks项目(1)登录认证作者这里使用的是Django自带的auth。(2)上传头像图片处理,使用Pillow。(3)扩展user扩展user模型与自带的user使用外键进行关联,命名为profile。二、英语总结(生词:4)1.def......
  • [做题笔记 #3] 图论
    目录[做题笔记#3]图论1.P6175无向图的最小环问题2.P4568[JLOI2011]飞行路线3.P5304[GXOI/GZOI2019]旅行者二进制划分+最短路做法正反建图+最短路做法4.P6961[NEERC2017]JourneyfromPetersburgtoMoscow5.P4899[IOI2018]werewolf狼人6.P4606[SDOI2018]......