Skip to content

Latest commit

 

History

History
6614 lines (5168 loc) · 232 KB

02.type-of-data.md

File metadata and controls

6614 lines (5168 loc) · 232 KB

02.数据

数据是任何编程语言的生产生活资料,离开了数据,编程语言将会变得毫无意义。但是我们并不能把所有的数据都称为一个数据,就好像我们现实生活中会分为人类,动物类,植物类等。Solidity 智能合约的含义就是一些功能和数据的集合,它们是位于以太坊区块链的特定地址上。

Solidity 提供了几种基本类型,并且基本类型可以用来组合出复杂类型。

本章目录

  • 1️⃣ 数据与变量
  • 2️⃣ 两种类型的数据
  • 3️⃣ 值类型
  • 4️⃣ 值类型:地址类型
  • 5️⃣ 值类型:合约类型
  • 6️⃣ 引用类型的额外注解:数据位置
  • 7️⃣ 引用类型
  • 8️⃣ 类型转换
  • 9️⃣ 字面常量与基本类型的转换
  • 🆗 实战 1: 同志们好增加提示
  • 🆗 实战 2: ETH 钱包
  • 🆗 实战 3: 多签钱包
  • #️⃣ 问答题

1️⃣ 数据与变量

提到数据,就不可避免的需要牵扯到变量。变量名是数据的在计算中的引用。

uint256 u = 123;

如上是将123这个 uint8类型数据,赋值给 u 这个只能赋值uint256类型数据的变量名(uint8 可以隐式转为uint256)。后续我们需要使用123这个数据时,写 u 就可以代表。

uint256 u 中的 uint256关键字,限制了 u 这个变量名只能赋值uint256类型数据;在其他弱类型语言中可能存在变量类型取决于数据的情况。但是 Solidity 中不存在这种情况,在变量声明时必须指定变量类型。Solidity 是一种静态强类型的语言,对于常见错误,开发者可以通过编译迅速捕捉到,任何的 Solidity 合约都需要编译部署的阶段。

本节配套视频

1.隐式转换

如果上面 uint256 u = 123; 改为 uint256 u = "Hello";,将会收到错误 Type literal_string "Hello" is not implicitly convertible to expected type uint256.,因为这两种类型不能隐式转换的;

如果上面 uint256 u = 123; 改为 uint256 u = uint8(123);,就不会有问题,因为uint8类型可以隐式转换为uint256类型。后面介绍类型转换的时候会详细的介绍。

  • 总结:
    1. Solidity 是一种静态强类型的语言。
    2. 变量类型和需要赋值的数据类型必须必配,或者所赋值的数据可以隐式转换为变量类型。

2️⃣ 两种类型的数据

本节配套视频

内容

Solidity 按照数据类型可以分为值类型引用类型

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

contract Demo {
    uint256 public u = 123; // 无变化
    string public welcome1 = "Hello";
    string public welcome2 = "Hello";

    function test() external returns(uint256,string memory,string memory){
        // 修改值类型
        uint256 x = u; // 赋值
        x = 1; // 修改

        // 修改引用类型
        string storage hi1 =  welcome1; // 赋值
        bytes(hi1)[0] = bytes1("2");

        string memory hi2 =  welcome2; // 赋值
        bytes(hi2)[0] = bytes1("2");

        // 返回值
        return(x,hi1,hi2);
    }
}
  • 值类型: 值类型传值时会将值拷贝一份,传递的是值本身,对其修改时并不会对原来值有影响。
    • 始终按值来传递,当被用作函数参数或者用在赋值语句中时,总会进行值拷贝。
    • 值类型里有两个比较特殊的类型是函数和地址(包括合约),会分为单独的部分介绍。
  • 引用类型: 引用类型进行传递时,传递的是其指针,而引用类型进行传递时可以为值传递也可以为引用传递

3️⃣ 值类型

  1. Boolean
  2. Integer
    1. uint
    2. int
  3. 定长字节数组(固定大小字节数组)
    1. bytes1 - bytes32
  4. Enum:枚举
  5. 地址(Address)
  6. 合约类型
  7. 函数(Function Types)
    1. 比较特殊,单独开了一章说明

1.Boolean 布尔类型

本节配套视频

布尔型使用 bool表示,该类型只有两个值,分别是 true/false

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

contract Demo1 {
    bool public t = true;
    bool public f = false;
}

布尔值除了赋值得到外,还可以通过运算符的计算结果得到。

⓵ 支持的运算符

  • 包括:!逻辑非,
  • ==等于,!= 不等于;
  • &&逻辑与,||逻辑或,
    • &&|| 为短路运算符。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo2 {
    bool public a = false;
    bool public b = !a; // 仅用于布尔值
    bool public c = a == b;
    bool public d = a != b;
    bool public e = a && b;
    bool public f = a || b;
}

运算符 ||&& 都遵循同样的短路( short-circuiting )规则。就是说在表达式 f(x) || g(y) 中, 如果 f(x) 的值为 true ,那么 g(y) 就不会被执行,

⓶ 使用短路规则节省 gas

借助短路规则,可以让合约少执行一些逻辑。

  • || 如果第一个表达式是true,则第二个表达式不再执行。(因为两个表达式有一个为 true,结果就为 true,不需要计算第二个表达式就知道结果了)
  • && 如果第一个表达式是false,则第二个表达式不再执行。(两个表达式必须都为 true,结果才能 true,如果第一个为 false,不需要计算第二个表达式就知道结果了)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract Demo3 {
    // 21488 gas
    function testA1() external pure returns (bool) {
        uint256 a = 100 - 99;
        uint256 b = 100 - 1;
        if (a > 50 || b < 50) {
            return true;
        }
        return false;
    }

    // 21440 gas
    function testA2() external pure returns (bool) {
        if ((100 - 99) > 50 || (100 - 10) < 50) {
            return true;
        }
        return false;
    }
}

本章主要介绍数据类型,后续介绍数据类型的时不再介绍操作符,会专门有一章来总结操作符。

2.Integer 整数类型

本节配套视频

整数类型分为有符号整型,用 int 标示;和无符号整型,用 uint 标示;

int 和 uint:

类型 符号名 取值
整型 int8 to int256 8 位到 256 位的带符号整型数。
uint8 to uint256 8 位到 256 位的无符号整型。
int 有符号整数,int 与 int256 相同。
uint 无符号整数,uint 和 uint256 是一样的。
定长浮点型 fixed 有符号的定长浮点型
unfixed 无符号的定长浮点型
  • int 是有符号整型,支持 int8 到 int256。
  • uint 是无符号整型,支持从 uint8 到 uint256。
  • uintint 分别是 uint256int256 的别名。

⓵ 属性

对于整型 T 有下面的全局属性可访问:

  • type(T).min
    • 获取整型 T 的最小值。
  • type(T).max
    • 获取整型 T 的最大值。

⓶ uint 类型

uint 无符号整数,只能表示非负数;包括数字0;其中 uint256 与 uint 相同,推荐使用 uint256;支持 int8 到 int256,后面的数字是 8 的倍数。

  • uint8: 最小值是 0,最大值是 2**8-1
  • uint256:最小值是 0,最大值是 2**256-1
  • 可以使用 type(uint8).max 获取该类型的最大值
  • 可以使用 type(uint8).min 获取该类型的最小值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Data {
    // uint
    uint256 public u1 = 123;
    uint8 public u8Max = type(uint8).max; // 255 => (2**8-1)
    uint8 public u8Min = type(uint8).min; // 0

    // 115792089237316195423570985008687907853269984665640564039457584007913129639935
    uint256 public u256Max = type(uint256).max;
    uint256 public u256Min = type(uint256).min; // 0
}

通过代码获取所有 uint 类型和取值范围

通过合约获取具体的最大值范围,最小值类似。

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

contract Data {
    // uint
    uint8 public u008Max = type(uint8).max;
    uint16 public u016Max = type(uint16).max;
    uint24 public u024Max = type(uint24).max;
    uint32 public u032Max = type(uint32).max;
    uint40 public u040Max = type(uint40).max;
    uint48 public u048Max = type(uint48).max;
    uint56 public u056Max = type(uint56).max;
    uint64 public u064Max = type(uint64).max;
    uint72 public u072Max = type(uint72).max;
    uint80 public u080Max = type(uint80).max;
    uint88 public u088Max = type(uint88).max;
    uint96 public u096Max = type(uint96).max;
    uint104 public u104Max = type(uint104).max;
    uint112 public u112Max = type(uint112).max;
    uint120 public u120Max = type(uint120).max;
    uint128 public u128Max = type(uint128).max;
    uint136 public u136Max = type(uint136).max;
    uint144 public u144Max = type(uint144).max;
    uint152 public u152Max = type(uint152).max;
    uint160 public u160Max = type(uint160).max;
    uint168 public u168Max = type(uint168).max;
    uint176 public u176Max = type(uint176).max;
    uint184 public u184Max = type(uint184).max;
    uint192 public u192Max = type(uint192).max;
    uint200 public u200Max = type(uint200).max;
    uint208 public u208Max = type(uint208).max;
    uint216 public u216Max = type(uint216).max;
    uint224 public u224Max = type(uint224).max;
    uint232 public u232Max = type(uint232).max;
    uint240 public u240Max = type(uint240).max;
    uint248 public u248Max = type(uint248).max;
    uint256 public u256Max = type(uint256).max;
}

所有 uint 结果如下:

uintN 最大值
uint8 0 255
uint16 0 65535
uint24 0 16777215
uint32 0 4294967295
uint40 0 1099511627775
uint48 0 281474976710655
uint56 0 72057594037927935
uint64 0 18446744073709551615
uint72 0 4722366482869645213695
uint80 0 1208925819614629174706175
uint88 0 309485009821345068724781055
uint96 0 79228162514264337593543950335
uint104 0 20282409603651670423947251286015
uint112 0 5192296858534827628530496329220095
uint120 0 1329227995784915872903807060280344575
uint128 0 340282366920938463463374607431768211455
uint136 0 87112285931760246646623899502532662132735
uint144 0 22300745198530623141535718272648361505980415
uint152 0 5708990770823839524233143877797980545530986495
uint160 0 1461501637330902918203684832716283019655932542975
uint168 0 374144419156711147060143317175368453031918731001855
uint176 0 95780971304118053647396689196894323976171195136475135
uint184 0 24519928653854221733733552434404946937899825954937634815
uint192 0 6277101735386680763835789423207666416102355444464034512895
uint200 0 1606938044258990275541962092341162602522202993782792835301375
uint208 0 411376139330301510538742295639337626245683966408394965837152255
uint216 0 105312291668557186697918027683670432318895095400549111254310977535
uint224 0 26959946667150639794667015087019630673637144422540572481103610249215
uint232 0 6901746346790563787434755862277025452451108972170386555162524223799295
uint240 0 1766847064778384329583297500742918515827483896875618958121606201292619775
uint248 0 452312848583266388373324160190187140051835877600158453279131187530910662655
uint256 0 115792089237316195423570985008687907853269984665640564039457584007913129639935

⓷ int 类型

int 是有符号整数,其中 int256 与 int 相同,推荐使用 int256; 8 位到 256 位的带符号整型数。8 的倍数。

  • int8: 最小值是 -(2**8/2),最大值是 (2**8/2)-1
    • int8: 最小值是 -128,最大值是 127
  • int256: 最小值是 -(2**256/2),最大值是 (2**256/2)-1
  • 可以使用 type(int8).max 获取该类型的最大值
  • 可以使用 type(int8).min 获取该类型的最小值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Data {
    // int
    int256 public i1 = 123;
    int256 public i2 = -123;
    int8 public i8Max = type(int8).max; // 127 => (2**8/2)-1
    int8 public i8Min = type(int8).min; // -128  => - 2**8/2
    //  57896044618658097711785492504343953926634992332820282019728792003956564819967
    int256 public i256Max = type(int256).max;
    // -57896044618658097711785492504343953926634992332820282019728792003956564819968
    int256 public i256Min = type(int256).min;
}

计算 int 的最大值和最小值

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

contract Data {
    int8 public i008Max = type(int8).max;
    int16 public i016Max = type(int16).max;
    int24 public i024Max = type(int24).max;
    int32 public i032Max = type(int32).max;
    int40 public i040Max = type(int40).max;
    int48 public i048Max = type(int48).max;
    int56 public i056Max = type(int56).max;
    int64 public i064Max = type(int64).max;
    int72 public i072Max = type(int72).max;
    int80 public i080Max = type(int80).max;
    int88 public i088Max = type(int88).max;
    int96 public i096Max = type(int96).max;
    int104 public i104Max = type(int104).max;
    int112 public i112Max = type(int112).max;
    int120 public i120Max = type(int120).max;
    int128 public i128Max = type(int128).max;
    int136 public i136Max = type(int136).max;
    int144 public i144Max = type(int144).max;
    int152 public i152Max = type(int152).max;
    int160 public i160Max = type(int160).max;
    int168 public i168Max = type(int168).max;
    int176 public i176Max = type(int176).max;
    int184 public i184Max = type(int184).max;
    int192 public i192Max = type(int192).max;
    int200 public i200Max = type(int200).max;
    int208 public i208Max = type(int208).max;
    int216 public i216Max = type(int216).max;
    int224 public i224Max = type(int224).max;
    int232 public i232Max = type(int232).max;
    int240 public i240Max = type(int240).max;
    int248 public i248Max = type(int248).max;
    int256 public i256Max = type(int256).max;
}

所有 int 结果如下:

intN 最小值 最大值
int8 -128 127
int16 -32768 32767
int24 -8388608 8388607
int32 -2147483648 2147483647
int40 -549755813888 549755813887
int48 -140737488355328 140737488355327
int56 -36028797018963968 36028797018963967
int64 -9223372036854775808 9223372036854775807
int72 -2361183241434822606848 2361183241434822606847
int80 -604462909807314587353088 604462909807314587353087
int88 -154742504910672534362390528 154742504910672534362390527
int96 -39614081257132168796771975168 39614081257132168796771975167
int104 -10141204801825835211973625643008 10141204801825835211973625643007
int112 -2596148429267413814265248164610048 2596148429267413814265248164610047
int120 -664613997892457936451903530140172288 664613997892457936451903530140172287
int128 -170141183460469231731687303715884105728 170141183460469231731687303715884105727
int136 -43556142965880123323311949751266331066368 43556142965880123323311949751266331066367
int144 -11150372599265311570767859136324180752990208 11150372599265311570767859136324180752990207
int152 -2854495385411919762116571938898990272765493248 2854495385411919762116571938898990272765493247
int160 -730750818665451459101842416358141509827966271488 730750818665451459101842416358141509827966271487
int168 -187072209578355573530071658587684226515959365500928 187072209578355573530071658587684226515959365500927
int176 -47890485652059026823698344598447161988085597568237568 47890485652059026823698344598447161988085597568237567
int184 -12259964326927110866866776217202473468949912977468817408 12259964326927110866866776217202473468949912977468817407
int192 -3138550867693340381917894711603833208051177722232017256448 3138550867693340381917894711603833208051177722232017256447
int200 -803469022129495137770981046170581301261101496891396417650688 803469022129495137770981046170581301261101496891396417650687
int208 -205688069665150755269371147819668813122841983204197482918576128 205688069665150755269371147819668813122841983204197482918576127
int216 -52656145834278593348959013841835216159447547700274555627155488768 52656145834278593348959013841835216159447547700274555627155488767
int224 -13479973333575319897333507543509815336818572211270286240551805124608 13479973333575319897333507543509815336818572211270286240551805124607
int232 -3450873173395281893717377931138512726225554486085193277581262111899648 3450873173395281893717377931138512726225554486085193277581262111899647
int240 -883423532389192164791648750371459257913741948437809479060803100646309888 883423532389192164791648750371459257913741948437809479060803100646309887
int248 -226156424291633194186662080095093570025917938800079226639565593765455331328 226156424291633194186662080095093570025917938800079226639565593765455331327
int256 -57896044618658097711785492504343953926634992332820282019728792003956564819968 57896044618658097711785492504343953926634992332820282019728792003956564819967

问题: 为什么 uint8/int8uint256/uint256 都是以 8 的倍数递增,且最大值是 256。 1 字节是 8 位,所以后面 8,16,都需要是 8 的整数倍,int8 是 8 位。EVM 为地址设置的最大长度是 256 位,所以最大值是uint256/uint256

计算中最小一级的信息单位是 byte 和 bit: 其中字节(Byte)为最小存储容量单位位(bit)是最小储存信息的单位,也被称为最小的数据传输单位;一个位就代表一个 0 或 1(即二进制);每 8 个 bit(简写为 b)组成一个字节 Byte(简写为 B);所以 uint256bytes32 可以转换

  • bytes1 对应 uint8
  • bytes2 对应 uint16
  • ...
  • bytes32 对应 uint256

问题: 为什么 int8 的取值范围是-128~127 呢?为什么 uint256 的最大值是 2**256 -1,而不是 2**256 呢?

1 字节是 8 位,int8 是 8 位,二进制表示为0000 00001000 0000,第一位是符号位;第一位为 0 是正值,第一位为 1 是负值;因为 int8 总共能够表示 2 的 8 次方,所以带符号的正值为 128 个数,负值为 128 个数;

计算机里是将 0 算在正值内,负值的范围还是-128;但是 0 不是正数也不是负数,所以正值范围少了一个位置,就剩 127 个位置了。

问题: 字节 & bit & 十六进制数字关系

  • bytes1 是指 1 个字节,1 个字节可以表示成 2 个连续的 16 进制数字。最大值是 0xff
  • bytes1 是指 1 个字节,1 个字节可以表示成 8 个连续的 bit 数字。最大值是 11111111
    • bytes1 等于两位连续的十六进制数字 0xXX
  • 8 个 bit 最大值是 11111111,8 个 bit 对应 2 个连续的十六进制数字,最大是 0xff;
    • uint8 等于两位连续的十六进制数字 0xXX

⓸ checked 模式

⚠️: 在 Solidity 之前的版本中,当对无限制整数执行算术运算,其结果超出结果类型的范围,这是就发生了上溢出或下溢出。在 Solidity 0.8.0 之前,算术运算总是会在发生溢出的情况下进行“截断”,而不是抛出异常。这就会导致一些麻烦的事情,可能导致未知的错误,所以我们不得不靠引入额外检查库来解决这个问题(最常见的如 OpenZepplin 的 SafeMath)

而从 Solidity 0.8.0 开始,所有的算术运算默认就会进行溢出检查,额外引入库将不再必要。0.8.0 开始,算术运算有两种计算模式:一种是checked(检查)模式,另一种是 unchecked(不检查)模式。

默认情况下,算术运算在 checked 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。

你也可以通过 unchecked{ ... } 切换到 “unchecked”模式,更多可参考 unchecked .

⓹ unchecked 非检查模式

如果依然想要之前“截断”的效果,而不是抛出异常错误,那么可以使用 unchecked{} 代码块:

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

contract C {
    uint256 public a = type(uint256).max;
    uint8 public b = 1;

    function f1() public view returns (uint256) {
        // 减法溢出会返回“截断”的结果
        unchecked {
            return a + b;
        }
    }

    function f2() public view returns (uint256) {
        unchecked {
            return a + 2;
        }
    }

    function g() public view returns (uint256) {
        // 溢出会抛出异常
        return a + b;
    }
}

调用 g() 会触发失败异常, 调用 f1()/f2() 分别是截断效果,

⚠️: unchecked 代码块可以在代码块中的任何位置使用,但不可以替代整个函数代码块,同样不可以嵌套。切此设置仅影响语法上位于 unchecked 块内的语句。 在块中调用的函数不会此影响。

⚠️: 为避免歧义,不能在 unchecked 块中使用 _;, 该表示方法仅用于函数修改器。

触发溢出检查的运算符

下面的这个运算操作符会进行溢出检查,如果上溢出或下溢会触发失败异常。 如果在非检查模式代码块中使用,将不会出现错误:

++, --, +, 减 -, 负 -, *, /, %, **

+=,-=, *=, /=, %=

0(或除 0取模

⚠️ 警告: 除 0(或除 0取模)的异常是不能被 unchecked 忽略的。会发生 Panic 错误。

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

contract C {
    uint256 public a = type(uint256).max;

    function f1() public view returns (uint256) {
        unchecked {
            return a / 0;
        }
    }

    function f2() public view returns (uint256) {
        unchecked {
            return a % 0;
        }
    }
}
位运算不会执行上溢或下溢检查

⚠️ 注解: 位运算不会执行上溢或下溢检查。 这在使用位移位(<<, >>, <<=, >>=)来代替整数除法和 2 指数时尤其明显。 例如 type(uint256).max << 3 不会回退,而 type(uint256).max + 1 会失败回退。

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

contract C {
    uint256 public a = type(uint256).max;
    uint8 public b = 1;

    function g1() public view returns (uint256) {
        return a + b;
    }

    function g2() public view returns (uint256) {
        return a << 3;
    }
}
-int 值需要注意

注解 int x = type(int).min; -x; 中的第 2 句会溢出,因为负数的范围比正整数的范围大 1。

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

contract C {
    int256 x = type(int256).min;
    int256 y = -1;

    function fn1() public view returns (int256) {
        unchecked {
            return -x;
        }
    }

    // 溢出
    function fn2() public view returns (int256) {
        return -x;
    }

    function fn3() public view returns (int256) {
        return -y;
    }
}

显式类型转换将始终截断并且不会导致失败的断言,但是从整数到枚举类型的转换例外。

显式类型转换
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract C {
    uint256 public a = type(uint256).max;
    uint8 public b = uint8(a);
}

3.Integer 整数字面常量

本节配套视频

⓵ 整数字面常量中用_增加可读性

为了提高可读性可以在数字之间加上下划线。 例如,十进制 123_000,十六进制 0x2eff_abde,科学十进制表示 1_2e12 都是有效的。

需要注意以下几点:

  • 下划线仅允许在两位数之间,并且不允许下划线连续出现。
  • 添加到数字文字中下划线没有额外的语义,仅仅只是为了可读性.
  • 下划线会被编译器忽略。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo{
    uint public count1 = 123_456_789; // 23503 gas
    uint public count2 = 123_456_789; // 23493 gas
    uint public count3 = 123456789; // 23537 gas
    int public count4 = -123456789; // 23559 gas
    int public count5 = -123_456_789; // 23471 gas
}

带有_数字的变量在可读性上,更加直观. 至于gas消耗实质上是一样的,之所以会出现gas消耗少的情况,是因为选择器优先命中的结果.

⓶ 字面常量支持任意精度

数值字面常量表达式本身支持任意精度,直到被转换成了非常量类型(例如,在常量变量表达式之外有运算,或发生了显示转换)。 这意味着在数值常量表达式中, 计算不会溢出而除法也不会截断。

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

contract Demo {
    uint8 public a = (2**800 + 1) - 2**800;
    uint8 public b = 0.5 * 8;
}

(2**800 + 1) - 2**800 的结果是字面常量 1 (属于 uint8 类型),尽管计算的中间结果已经超过了 以太坊虚拟机的机器字长度。 此外, .5 * 8 的结果是整型 4 (尽管有非整型参与了计算)。

⚠️: 数 值字面常量表达式只要在非字面常量表达式中使用就会转换成非字面常量类型。 在下面的例子中,尽管我们知道 b 的值是一个整数,但 2.5 + a 这部分表达式并不进行类型检查,因此编译不能通过。

uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

⓷ 除法截断

注意除法截断: 在智能合约中,在 字面常量 会保留精度(保留小数位)。

整数的除法会被截断(例如:1/4 结果为 0),但是使用字面量的方式不会被截断. 之所以会出现 字面量 未截断的根本原因在于 合约编译 与 EVM 执行上的差异. 字面量 未截断 是在合约编译阶段既已经确定了数值,编译后的字节码得到的数据是确定的数值. 而截断的数据是 在 EVM 中运行的, 无法保留精度.

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

contract IntegerTest {
    function test1() public pure returns (uint256, uint256) {
        uint256 a = 1;
        uint256 b = 4;
        uint256 c1 = (1 / 4) * 4; // 1 => 未截断
        uint256 c2 = (a / b) * b; // 0 => 截断
        return (c1, c2);
    }

    function test2() public pure returns (int256, int256) {
        int256 a = -1;
        int256 b = -4;
        int256 c1 = (-1 / -4) * (-4); // -1 => 未截断
        int256 c2 = (a / b) * b; // 0 => 截断
        return (c1, c2);
    }
}

注释: 表达式 type(int).min / (-1) 是仅有的整除会发生向上溢出的情况。 在算术检查模式下,这会触发一个失败异常,在截断模式下,表达式的值将是 type(int).min

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

contract Test {
    function test1() public pure returns (int256 a) {
        a = type(int256).min / (-2);
    }

    // VM error: revert.
    function test2() public pure returns (int256 a) {
        a = type(int256).min / (-1);
    }

    function test3() public pure returns (int256 a) {
        unchecked {
            a = type(int256).min / (-1);
        }
    }
}

⓸ 优先使用较小类型计算

虽然大多数运算符在字面常量运算时都会产生一个字面常量表达式,但有一些运算符并不遵循这种模式:

  • 三元运算符 (... ? ... : ...),
  • 数组下标访问 (<array>[<index>]).

你可能认为像255 + (true ? 1 : 0)255 + [1, 2, 3][0] 这样的表达式等同于直接使用 256 字面常量。 但事实上,它们是在 uint8 类型中计算的,会溢出。

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

contract Test {
    // VM error: revert.
    function testA1() public pure returns (uint256 a) {
        a = 255 + (true ? 1 : 0);
    }

    function testA2() public pure returns (uint256 a) {
        a = (true ? 1 : 0) + 255;
    }

    // VM error: revert.
    function testB1() public pure returns (uint256 a) {
        a = 255 + [1, 2, 3][0];
    }

    function testB2() public pure returns (uint256 a) {
        a = [1, 2, 3][0] + 255;
    }

    function testA3() public pure returns (uint256 a) {
        a = 255 + uint256(true ? 1 : 0);
    }

    function testB3() public pure returns (uint256 a) {
        a = 255 + uint256([1, 2, 3][0]);
    }
}

4.Fixed 定长浮点型

本节配套视频

Solidity 还没有完全支持定长浮点型。

可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。。

可以通过用户定义的值类型的 wrap / unwrap 来模拟出来,后面介绍用户自定义类型时候会介绍。

fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxNfixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。

5.BytesN 定长字节数组

本节配套视频

定义方式 bytesN,其中 N 可取 1~32 中的任意整数;

bytes1 代表只能存储一个字节。

  • ⚠️ 注意:一旦声明,其内部的字节长度不可修改,内部字节不可修改。
  • ⚠️ 注意:bytes32bytes 是不同的。
    • bytesN: 是定长的字节数组,是值类型
    • bytes: 是变长字节数组,是引用类型。

⓵ 普通赋值

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

contract Test {
    // 固定长度的字节数组
    bytes1 public a1 = 0x61;
    bytes2 public a2 = 0x6100;
    bytes4 public a3 = 0x61000000;
    bytes6 public a4 = 0x416e62616e67;
    bytes7 public a5 = 0x416e62616e6700;
    bytes8 public a6 = 0x416e62616e670000;
    bytes16 public a7 = 0x416e62616e6700000000000000000000;
    bytes32 public a8 =
        0x416e62616e670000000000000000000000000000000000000000000000000000;
}

注意这里 bytes32bytes 是不同的。bytes 是变长字节数组,是引用类型。

⓶ 使用字符串赋值

警告:字符串字面常量在赋值给 bytesN 时被解释为原始的字节形式。

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

contract Test {
    bytes1 public b1 = "a";
    bytes2 public b2 = "a";
    bytes4 public b3 = "a";
    bytes6 public b4 = "Anbang";
    bytes7 public b5 = "Anbang";
    bytes8 public b6 = "Anbang";
    bytes16 public b7 = "Anbang";
    bytes32 public b8 = "Anbang";
}

⓷ 属性

  • length (只读)
    • 返回字节个数,可以通过索引读取对应索引的字节。
  • 索引访问: bytesN[index]
    • index 取值范围[0, N],其中 N 表示长度。
    • 如果 xbytesI 类型,那么 x[k] (其中 0 <= k < I)返回第 k 个字节(只读)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Test {
    // 固定长度的字节数组
    bytes1 public a1 = 0x61;
    bytes2 public a2 = 0x6100;

    // length
    uint256 public n1 = a1.length;
    uint256 public n2 = a2.length;

    // 索引
    function getIndex(uint8 index_) public view returns(bytes1){
        return a2[index_];
    }

    // 不修可以修改
    // function setIndex(uint8 index_,bytes1 value_) public view{
    //     a2[index_] = value_;
    // }
}

6.字符串字面常量及类型

字符串字面常量只能包含可打印的 ASCII 字符,这意味着他是介于 0x20 和 0x7E 之间的字符。

字符串字面常量是指由双引号或单引号引起来的字符串( "foo" 或者 'bar');

本节配套视频

⓵ 字符串字面量是值类型

转换: 和整数字面常量一样,字符串字面常量的类型也可以发生改变,它们可以隐式地转换成bytes1,……, bytes32,如果合适的话,还可以转换成 bytes 以及 string

比如 bytes1 public a8 = "a";bytes2 public b2 = "a";。字符串字面常量在赋值给 bytesN 时被解释为原始的字节形式

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

contract Test {
    bytes1 public b1 = "a";
    string public b2 = "a";
}

⓶ 转义字符

但是我们写特殊字符串时候遇到一个问题,比如我想输出一个 fo"ofo'o 的字符串就很难弄,因为莫认为"' 是字符串的结尾。如果想要输出这种特殊的字符串,就需要转义字符了。

此外,字符串字面常量支持下面的转义字符:

  • \' (单引号)
  • \" (双引号)
  • \\ (反斜杠)
  • \<newline> (转义实际换行)
  • \b (退格)
  • \f (换页)
  • \n (换行符)
  • \r (回车)
  • \t (标签 tab)
  • \v (垂直标签)
  • \xNN (十六进制转义,见下文)
  • \uNNNN (unicode 转义,见下文)

\xNN 表示一个 16 进制值,最终转换成合适的字节,而 \uNNNN 表示 Unicode 编码值,最终会转换为 UTF-8 的序列。

问答题:下面字符串长度为多少字节?

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

contract Test {
    string public a1 = "\n\"'\\abc\
def";
    bytes32 public a2 = "\n\"'\\abc\
def";
}

字符串长度为十个字节,它以换行符开头,后跟双引号,单引号,反斜杠字符,以及(没有分隔符)字符序列 "'\abcdef

⓷ 用空格分开的字符串

用空格分开的 "foo" "bar" 等效于 "foobar",

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

contract Test {
    string public a = "a" "b";
}

7.Unicode 字面常量

本节配套视频

常规字符串文字只能包含 ASCII,而 Unicode 文字(以关键字 unicode 为前缀)可以包含任何有效的 UTF-8 序列。 它们还支持与转义序列完全相同的字符作为常规字符串文字。

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

contract Test {
    string  public a = unicode"同志们好";
}

8.十六进制字面常量

十六进制字面常量以关键字 hex 打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex"001122FF" )。 字符串的内容必须是一个十六进制的字符串,它们的值将使用二进制表示。

本节配套视频

⓵ 基本用法

它们的内容必须是十六进制数字,可以选择使用单个下划线作为字节边界分隔符。 字面常量的值将是十六进制序列的二进制表示形式

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

contract Test {
    string public a1 = "a";
    bytes1 public a2 = "a";
    bytes1 public a3 = 0x61;
    bytes1 public a4 = hex"61";
}

⓶ 用空格分开的十六进制字面常量

用空格分隔的多个十六进制字面常量被合并为一个字面常量: hex"61" hex"61" 等同于 hex"6161"

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

contract Test {
    bytes2 public a = hex"61" hex"61";
}

十六进制字面常量跟 字符串字面常量 很类似,具有相同的转换规则

9.Enum:枚举

本节配套视频

enum 是一种用户自定义类型,用于表示多种状态,枚举可用来创建由一定数量的“常量值”构成的自定义类型。主要作用是用于限制某个事务的有限选择。比如将咖啡的容量大小限制为:大、中、小,这将确保任何人不能购买其他容量的咖啡,只能在这里选择。

枚举默认值是第一个成员,所以枚举类型至少需要一个成员,枚举不能多于 256 个成员。枚举默认的类型为 uint8,当枚举数足够多时,它会自动变成 uint16..等变大。可以通过 remix 部署后,函数的输入值内查看类型 uint8 / uint16

  • 枚举类型,返回值是索引,默认值是 0;
  • 枚举类型的默认值是第一个值。
    • 枚举类型 enum 至少应该有一名成员。
  • 设置的时候,可以设置为索引,也可以对应的枚举名称;
  • 枚举类型 enum 可以与整数进行显式转换,但不能进行隐式转换。
    • 显示转换会在运行时检查数值范围,如果不匹配,将会引起异常。

例子:考虑一个限制,将交易的状态限制为:None/Pending/Shiped/Completed/Rejected/Canceled 这几种。这将确保交易状态仅在列出的状态内。

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

contract Enum {
    // 枚举类型的默认值是第一个值。
    // 结构
    enum Status {
        None, // 0
        Pending, // 1
        Shiped,// 2
        Completed,
        Rejected,
        Canceled
    }
    // 变量
    Status public status;

    // 设置索引值
    function set(Status _status) external {
        status = _status;
    }

    // 由于枚举类型不属于 |ABI| 的一部分,因此对于所有来自 Solidity 外部的调用,
    // "getStatus" 的签名会自动被改成 "getStatus() returns (uint8)"。
    function getStatus() public view returns (Status) {
        return status;
    }

    function getDefaultStatus() public view returns (uint256) {
        return uint256(status);
    }

    // 设置
    function ship() external {
        status = Status.Shiped;
    }

    // 恢复为0
    function reset() external {
        delete status;
    }
}

很多人感觉 enum 很少用,一是因为应用场景确实比较窄,二是因为可以被其他数据类型所代替;但按照编码规范,限制选择范围场景,除了 bool 以外的,推荐使用 enum 类型来定义。

枚举是显示所有整型相互转换,但不允许隐式转换。从整型显式转换枚举,会在运行时检查整数时候在枚举范围内,否则会导致异常( Panic 异常 )。

枚举还可以在合约或库定义之外的文件级别上声明。

⓵ 属性

数据表示与:选项从0开始的无符号整数值表示。

⓶ 方法

  • delete
  • type(NameOfEnum).min
  • type(NameOfEnum).max

使用 type(NameOfEnum).mintype(NameOfEnum).max 你可以得到给定枚举的最小值和最大值。

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

contract Enum {
    // 枚举类型的默认值是第一个值。
    enum Status {
        None,//0
        Pending,//1
        Shiped,//2
        Completed,//3
        Rejected,//4
        Canceled// 5
    }

    function getLargestValue() public pure returns (Status) {
        return type(Status).max;
    }

    function getSmallestValue() public pure returns (Status) {
        return type(Status).min;
    }
}

10.用户定义的值类型

Solidity 允许在一个基本的值类型上创建一个零成本的抽象。这类似于一个别名,但有更严格的类型要求。

用户定义值类型使用 type UserType is DefaultType 来定义。

其中 UserType 是新引入的类型的名称, DefaultType 必须是内置的值类型(”底层类型”)。自定义类型的值的数据表示则继承自底层类型,并且 ABI 中也使用底层类型。

⚠️: 用户定义的类型 UserType 没有任何运算符或绑定成员函数。即使是操作符 == 也没有定义。也不允许与其他类型进行显式和隐式转换。

本节配套视频

⓵ 方法

  • UserType.wrap(): 用来从底层类型转换到自定义类型
  • UserType.unwrap(): 从自定义类型转换到底层类型。

⓶ 例子

案例:一个 18 位小数、256 bit 的浮点类型

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

// 使用用户定义的值类型表示 18 位小数、256 bit的浮点类型。
type UFixed256x18 is uint256;

/// 在 UFixed256x18 上进行浮点操作的精简库。
library FixedMath {
    uint constant multiplier = 10**18;

    /// 两个 UFixed256x18 数相加,
    /// 溢出时恢复,依赖于 uint256 上的检查算术
     function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
    }
    /// 将 UFixed256x18 和 uint256 相乘.
    /// 溢出时恢复,依赖于 uint256 上的检查算术
     function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
    }
    ///  UFixed256x18 向下取整.
    /// @return 不超过 `a` 的最大整数。
    function floor(UFixed256x18 a) internal pure returns (uint256) {
        return UFixed256x18.unwrap(a) / multiplier;
    }
    /// 将 uint256 转换为相同值的 UFixed256x18。
    /// 如果整数太大,则还原。
    function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
        return UFixed256x18.wrap(a * multiplier);
    }
}
contract Test {
    uint256 a = 1;
    uint256 b = 2;

    function testAdd() external view returns (UFixed256x18) {
        return FixedMath.add(FixedMath.toUFixed256x18(a), FixedMath.toUFixed256x18(b));
    }
    function testMul() external view returns (UFixed256x18) {
        return FixedMath.mul(FixedMath.toUFixed256x18(a),b);
    }
}

注意 UFixed256x18.wrapFixedMath.toUFixed256x18 的签名相同,但执行的是两个完全不同的操作:

  • UFixed256x18.wrap 函数返回一个与输入的数据表示相同的自定义值类型(UFixed256x18)。
  • FixedMath.toUFixed256x18则返回一个具有相同数值的 UFixed256x18

4️⃣ 值类型:地址类型

地址分为外部地址和合约地址,每个地址都有一块持久化内存区称为存储。

地址类型也是值类型,因为比较特殊,所以单独拿出来讲。地址类型是 Solidity 语言独有的数据类型,表示以太坊的地址类型。用 address 表示地址,长度是 20 个字节;我们日常使用的是十六进制的地址格式,比如: 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;这种类型适合存储合约地址或外部地址。

本节配套视频

1.地址字面常量

通常的地址类型是 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac 这样的 checksum address,。 而没有通过校验测试, 长度在 39 到 41 个数字之间的十六进制字面常量,会产生一个错误, 比如 0XFFD0D80C48F6C3C5387B7CFA7AA03970BDB926AC 就是一个错误 address 类型。会提示正确的地址,也可以将地址输入到 etherscan 获取。

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

contract Test {
    // This looks like an address but has an invalid checksum.
    // Correct checksummed address: "0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac".
    // If this is not used as an address, please prepend '00'.
    // For more information please see
    // https://docs.soliditylang.org/en/develop/types.html#address-literals
    // address public a1 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926AC;

    // 直接在提示种获取到正确的 checksummed address,
    // 也可以在 etherscan 种得到 checksum 地址。
    address public a2 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
}

如果不怕麻烦,还可以通过 SDK 来自己转换 web3.utils.toChecksumAddress(address)

2.address/uint/bytes32 之间的转换

  • 1 字节 8 位,一个 address 是 20 个字节,是 160 位,所以 address 可以用 uint160 表示
  • 1 字节可以表示为两个连续的十六进制数字,所以 address 可以用连续的 40 个十六进制数字表示
  • address 不允许任何算数操作
  • address 允许和 uint160整型字面常量bytes20合约类型相互转换。
    • 如果将使用较大字节数组类型转换为 address ,例如 bytes32 ,那么 address 将被截断。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Test {
    bytes32 public a =
        0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC;

    // 0x111122223333444455556666777788889999aAaa
    address public b = address(uint160(bytes20(a)));

    // 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
    address public c = address(uint160(uint256(a)));
}

⚠️:为了减少转换歧义,我们在转换中显式截断处理。 以 32bytes 值 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC 为例, 如果使用 address(uint160(bytes20(b))) 结果是 0x111122223333444455556666777788889999aAaa, 而使用 address(uint160(uint256(b))) 结果是 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc

注意,网上很多在线转换工具得到的结果并不正确,比如: https://tool.oschina.net/hexconvert/

如下例子进行真实转换:_owner 在一些在线的软件内转换的不正确,上面的网址有个小 BUG,输入十六进制数据的时候,不能带 0x 前缀。

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

contract test {
    address _owner; // 十六进制
    uint160 _ownerUint; // 十进制

    constructor() {
        _owner = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
        _ownerUint = 1460450021995508802976443037013257463744970696364;
    }

    function toUint160() public view returns (uint160) {
        //转换10进制
        return uint160(_owner);
    }

    function toAddress() public view returns (address) {
        return address(_ownerUint);
    }
}

注意: 这里说的地址是 0x123... 这种十六进制的地址公钥,而不是应用层的 anbang.eth 这种 ENS 地址。虽然在很多钱包可以通过anbang.eth来向0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac转账,但仅仅是应用层的中转服务。具体可以在 以太坊浏览器,或者钱包内输入 ENS 域名查看中转逻辑.(ENS 是一种别名,除了在以太坊网络可以使用,在 imToken 等钱包内,在 BTC 网络也可以使用,它并不是区块链的底层,而是应用层)

3.两种形式的地址

在第一章接收 ETH 那一节的三个关键字里,也介绍了 payable,这里再次讲一下加深印象。

  • address:保存一个 20 字节的值(以太坊地址的大小)。
  • address payable :可支付地址,与 address 相同,不过有成员函数 transfersend

如果你需要 address 类型的变量,并计划发送以太币给这个地址,那么声明类型为 address payable 可以明确表达出你的需求。 同样,尽量更早对他们进行区分或转换。

这种区别背后的思想是 address payable 可以向其发送以太币,而不能向一个普通的 address 发送以太币。比如,它可能是一个智能合约地址,并且不支持接收以太币。

⓵ 两种形式的地址转换

允许从 address payableaddress 的隐式转换,而从 addressaddress payable 必须显示的 通过 payable(<address>) 进行转换。也只能通过 payable(...) 表达式把 address 类型和合约类型转换为 address payable

在介绍地址 payable 方法时候会具体介绍,转换的时候注意下面两个点:

  1. 只有能接收以太币的合约类型,才能够进行此转换。
    1. 例如合约要么有 receive 或可支付的回退函数。
  2. payable(0) 是有效的,这是此规则的例外。

4.地址属性

address 拥有如下属性;

  1. .balance : 以 Wei 为单位的余额。

    <address>.balance    returns(uint256)
    
  2. .code : 地址上的代码(可以为空)

    <address>.code        returns(bytes memory)
    
  3. .codehash : 地址的 codehash

    <address>.codehash    returns(bytes32)
    

⓵ balance 属性

获取地址的余额,wei 单位。如下例子是获取指定地址的 ETH 余额,和当前调用者的余额。(基于当前使用的网络)

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

contract Demo {
    function getBalance1(address addr) public view returns (uint256) {
        return addr.balance;
    }
    function getBalance2() external view returns (uint256) {
        return address(msg.sender).balance;
    }
}

函数内一般不像上面那么用,更多的是获取合约本身的 ETH 余额;

如何获取合约地址?:合约部署后,会有一个合约地址; 所有合约都可以转换为 address 类型

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

contract Demo {
    function returnContractAddress() external view returns (address) {
        return address(this);
    }
}

因此可以使用 address(this).balance 查询当前合约的余额,获取合约本身的 ETH 余额如下

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

contract Demo {
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
    receive() external payable {}
}

⚠️: 在版本 0.5.0 之前,Solidity 允许通过合约实例来访问地址的成员,例如 this.balance ,不过现在禁止这样做,必须显式转换为地址后访问,如: address(this).balance

⓶ code 属性

可以查询任何智能合约的部署代码。使用 .code 来获取 EVM 的字节码,其返回 bytes memory ,值可能是空。

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

contract Test {
    function getCode() public view returns (bytes memory) {
        return address(this).code;
    }

    // 外部地址 code 内容是空,
    // 可以通过这个来判断地址是否为合约
    function getAdsCode(address a_) public view returns (bytes memory) {
        return address(a_).code;
    }
}

注意:合约没有完全创建,也就是 constructor 没有完全执行完的时候,code 也是空。

下面是发糖果的合约,只允许用户的地址领取,禁止合约地址。结果被合约地址薅羊毛了。

// 发糖果的合约
contract A {
    uint256 giftValue = 666;
    mapping(address=>uint256) public gifts;

    function gift() public returns(uint256){
        bytes memory senderCode = getCode(msg.sender);
        require(senderCode.length==0,unicode"只能用户领取,薅羊毛的合约滚!!!");
        gifts[msg.sender] = giftValue;
        return giftValue;
    }

    function getCode(address ads_) public view returns(bytes memory){
        return address(ads_).code;
    }
}

// 薅羊毛的合约
contract Test {
    A a;
    uint256 public target; // 保存薅羊毛得到的糖果
    constructor(address ads_){
        target = A(ads_).gift();
    }
}

⓷ codehash 属性

使用 .codehash 获得合约代码的 Keccak-256 哈希值 (为 bytes32 )。

注意, addr.codehash 比使用 keccak256(addr.code) 更便宜。

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

contract Test {
    function getCode() public view returns (bytes memory) {
        return address(this).code;
    }

    function getCodeByKeccak256() public view returns (bytes32) {
        return keccak256(address(this).code);
    }

    function getCodehash() public view returns (bytes32) {
        return address(this).codehash;
    }
}

5.地址方法

address 拥有如下方法;

  1. address(): 可以将地址转换到地址类型。
  2. payable(): 将普通地址转为可支付地址。
  3. .transfer(uint256 amount): 将余额转到当前地址(合约地址转账)
  4. .send(uint256 amount): 将余额转到当前地址,并返回交易成功状态(合约地址转账)
  5. .call(bytes memory): 用给定的有效载荷(payload)发出低级 CALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账)
  6. .delegatecall(bytes memory): 用给定的有效载荷(payload)发出低级 DELEGATECALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账)
  7. staticcall(bytes memory): 用给定的有效载荷(payload)发出低级 STATICCALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账)

⓵ address()

1.获取当前合约地址:

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

contract Address {
    mapping(address => uint256) public balances; // 用在 mapping 结构内

    // 存款
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function getAddress() external view returns (address) {
        return address(this);
    }

    function getBalance1() external view returns (uint256) {
        return address(this).balance;
    }

    function getBalance2() external view returns (uint256) {
        return address(msg.sender).balance;
    }
}

2.uint 值转换成地址:

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

contract Test {
    function getAddress()
        external
        pure
        returns (
            address,
            address,
            address,
            address,
            address
        )
    {
        return (address(0), address(1), address(3), address(6), address(9));
    }
}

返回结果如下:

0: address: 0x0000000000000000000000000000000000000000
1: address: 0x0000000000000000000000000000000000000001
2: address: 0x0000000000000000000000000000000000000003
3: address: 0x0000000000000000000000000000000000000006
4: address: 0x0000000000000000000000000000000000000009

3.获取即将部署的地址

这是 uint 值转换成地址 的一种应用。

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

contract Demo {
    // 获取即将部署的地址
    function getAddress(bytes memory bytecode, uint256 _salt)
        external
        view
        returns (address)
    {
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), // 固定字符串
                address(this), // 当前工厂合约地址
                _salt, // salt
                keccak256(bytecode) //部署合约的 bytecode
            )
        );
        return address(uint160(uint256(hash)));
    }
}

⓶ payable()

注意:支付的时候,地址必须 payable 类型!从 addressaddress payable 的转换。可以通过 payable(x) 进行 ,其中 x 必须是 address 类型。

让普通地址为 payable 有两种方式

  • 方式一: 参数中 地址标注 address payable ,并且函数状态可变性标为 payable;
    • 这种更省 gas (推荐)
  • 方式二: 仅在内部进行 payable(address) 显示转换
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Payable {
    address payable public owner;

    constructor() {
        // 直接赋值会报错,因为 msg.sender 不是 payable 类型的地址。
        // Type address is not implicitly convertible to expected type address payable.
        // owner = msg.sender;

        // 使用 payable 函数,显示转换一下就可以了。
        owner = payable(msg.sender);
    }

    // deposit1 没有 payable 标示;如果传入ETH币,会报错
    // transact to Payable.deposit1 errored: VM error: revert.
    function deposit1() external {}

    // deposit2 有 payable, 所以可以发送ETH到合约
    function deposit2() external payable {}

    function getBalance() external view returns (uint256) {
        // 使用 address(this) 就可以包装当前合约,然后就可以使用 .balance 获取余额了。
        return address(this).balance;
    }
}

注意点:

  • 如果状态变量是 payable 类型,赋值的时候需要使用 payable() 进行显示转换
  • 如果函数没有 payable 标示,那么调用时候不能发送网络主币。
    • 如果尝试这么做会收到错误: transact to Payable.functionName errored: VM error: revert.
转换 0 地址
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Test {
    function getAddress()
        external
        pure
        returns (
            address,
            address,
            address,
            address,
            address
        )
    {
        return (address(0), address(1), address(3), address(6), address(999));
    }

    function getPayaableAddress() external pure returns (address) {
        // Explicit type conversion not allowed from "int_const 1" to "address payable".
        // return payable(1);
        return payable(0);
    }
}

⓷ transfer()

将余额转到当前地址(合约地址转账),语法如下:

<address payable>.transfer(uint256 amount)
  1. 需要 payable address
  2. 使用固定(不可调节)的 2300 gas 的矿工费,错误会 reverts (回滚所有状态)
    1. 2300 gas 足够转账,但是如果接收合约内的 fallbackreceive 函数有恶意代码,复杂代码。容易导致 gas 耗尽的错误。
  3. 失败时抛出异常,
    1. 如果当前合约的余额不够多,则 transfer 函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer 函数同样会失败而进行回退。
  4. 如果目标地址是一个合约,那么目标合约内部的 receive/fallback 函数会随着调用 transfer函数一起执行,这是 EVM 的特性,没办法阻止。

例子演示:

核心: _to.transfer(200);

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

contract SendEth {
    event Log(string funName, address from, uint256 value, bytes data);

    fallback() external payable {
        emit Log("fallback", msg.sender, msg.value, msg.data);
    }

    receive() external payable {
        emit Log("receive", msg.sender, msg.value, "");
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    // transfer :  地址必须为   payable 类型
    // 方式一: 参数中 地址标注 address payable ,并且函数标注 payable; 这种更省 gas
    // 28767 gas
    function transfer1(address payable _to) external payable {
        _to.transfer(100);
    }

    // 也可以 在内部进行显示转换
    // 方式二: 仅在内部进行 payable(address) 显示转换
    // 28813 gas
    function transfer2(address _to) external {
        payable(_to).transfer(200);
    }
}

⓸ send()

将余额转到当前地址,并返回交易成功状态(合约地址转账)

<address payable>.send(uint256 amount) returns (bool)

sendtransfer 的低级版本。如果执行失败,当前的合约不会因为异常而终止。transfer 等价于require(send())

  1. 需要 payable address
  2. 使用固定(不可调节)的 2300 gas 的矿工费。
    1. gas 同transfer一样的是 2300 gas ;足够转账,但是如果接收合约内的 fallbackreceive 函数有恶意代码,复杂代码。容易导致 gas 耗尽的错误。
  3. 失败时仅会返回 false,不会终止执行(合约地址转账);
    1. send() 执行有一些风险:为了保证安全,必须检查 send 的返回值,如果交易失败,会回退以太币。
  4. 补充:send 与 transfer 对应,但 send 更底层。如果执行失败,transfer 不会因异常停止,而 send 会返回 false。transfer 相对 send 较安全

例子演示:

核心: bool success = _to.send(100);

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

contract SendEth {
    event Log(string funName, address from, uint256 value, bytes data);

    fallback() external payable {
        emit Log("fallback", msg.sender, msg.value, msg.data);
    }

    receive() external payable {
        emit Log("receive", msg.sender, msg.value, "");
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    // 28791 gas
    function send1(address payable _to) external payable {
        bool success = _to.send(100);
        require(success, "Send Faied");
    }

    // 28793 gas
    function send2(address _to) external {
        bool success = payable(_to).send(100);
        require(success, "Send Faied");
    }
}

.transfer(uint256 amount) 失败时抛出异常, 等价于require(send()) 使用固定(不可调节)的 2300 gas 的矿工费,错误会 reverts.

⓹ call/delegatecall/staticcall

为了与不知道 ABI 的合约进行交互,Solidity 提供了函数 call/delegatecall/staticcall 直接控制编码。它们都带有一个 bytes memory 参数和返回执行成功状态(bool)和数据(bytes memory)。

函数 abi.encodeabi.encodePackedabi.encodeWithSelectorabi.encodeWithSignature 可用于编码结构化数据。

它们可以接受任意类型,任意数量的参数。这些参数会被打包到以 32 字节为单位的连续区域中存放。其中一个例外是当第一个参数被编码成正好 4 个字节的情况。 在这种情况下,这个参数后边不会填充后续参数编码,以允许使用函数签名。

下面具体的介绍三种 call。

⓺ call()

用给定的有效载荷(payload)发出低级 CALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账), 格式如下:

<address>.call(bytes memory) returns (bool, bytes memory)
  1. 低级 CALL 调用:不需要 payable address, 普通地址即可
    1. 注意: 调用 call 的时候,地址可以不具备 payable 属性
  2. 返回两个参数,一个 bool 值代表成功或者失败,另外一个是可能存在的 data
  3. 发送所有可用 gas,也可以自己调节 gas。
    1. 如果 fallbackreceive 内的代码相对复杂也可以,但是如果是恶意代码,需要考虑消耗的 gas 是否值得执行。
    2. _ads.call{value: msg.value,gas:2300}(data)
  4. 当合约调用合约时,不知道对方源码和 ABI 时候,可以使用 call 调用对方合约
  5. 推荐使用 call 转账 ETH,但是不推荐使用 call 来调用其他合约。
    1. 原因是: call 调用的时候,将合约控制权交给对方,如果碰到恶意代码,或者不安全的代码就很容易凉凉。
  6. 当调用不存在的合约方法时候,会触发对方合约内的 fallback 或者 receive
    1. 我们的合约也可以在 fallback / receive 这两个方法内抛出事件,查看是否有人对其做了什么操作。
  7. 三种方法都提供 gas 选项,而 value 选项仅 call 支持 。三种 call 里只有 call 可以进行 ETH 转账,其他两种不可以进行转账。

例子 1:发送 ETH

核心: (bool success, ) = _to.call{value: 100}("");

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

contract SendEth {
    event Log(string funName, address from, uint256 value, bytes data);

    fallback() external payable {
        emit Log("fallback", msg.sender, msg.value, msg.data);
    }

    receive() external payable {
        emit Log("receive", msg.sender, msg.value, "");
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    // 29005
    function call1(address payable _to) external payable {
        (bool success, bytes memory data) = _to.call{value: 100}("");
        require(success, "call Faied");
    }

    // 29007
    function call2(address _to) external {
        (bool success, bytes memory data) = payable(_to).call{value: 100}("");
        require(success, "call Faied");
    }
}

例子 2(重要):调用其他合约方法

完整代码如下:

分别使用 Test1Test2 的地址进行测试。

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

contract Test1 {
    string public name;
    uint256 public age;
    address public owner;
    event Log(string message);

    fallback() external payable {
        emit Log("fallback was called");
    }

    receive() external payable {
        emit Log("receive was called");
    }

    function setNameAndAge(string memory name_, uint256 age_)
        external
        payable
        returns (string memory __name, uint256 __age)
    {
        name = name_;
        age = age_;
        owner = msg.sender;
        return (name_, age_);
    }

    // 获取合约的余额
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

contract Test2 {}

contract CallTest {
    // 需要一个网页,动态的解析 _bys
    bytes public bys;

    function call_Test1_setNameAndAge(
        address ads_,
        string memory name_,
        uint256 age_
    ) external payable {
        bytes memory data = abi.encodeWithSignature(
            "setNameAndAge(string,uint256)",
            name_,
            age_
        );
        (bool success, bytes memory _bys) = ads_.call{value: msg.value}(data);
        require(success, "Call Failed");
        bys = _bys;
    }
}

简单说下这个例子的原理

/**
普通调用
用户A 调用 callB 合约, 发送 100 wei ; callB 调用 Test1, 发送 50 wei
此时在 Test1 合约内部
        msg.sender = B
        msg.value = 50
        Test1 内部如果有状态变量修改,则会被修改
        发送到 Test1 内的ETH主币也会被留在Test1内
 */

call 核心代码如下

bytes memory data = abi.encodeWithSignature(
    "setNameAndAge(string,uint256)",
    _name,
    _age
);
(bool success, bytes memory _bys) = _ads.call{value: msg.value}(data);
require(success, "Call Failed");
bys = _bys;

⓻ delegatecall() 委托调用

发出低级函数 DELEGATECALL,失败时返回 false,发送所有可用 gas,也可以自己调节 gas。

<address>.delegatecall(bytes memory) returns (bool, bytes memory)

delegatecall 使用方法和 call 完全一样。区别在于,delegatecall 只调用给定地址的代码(函数),其他状态属性如(存储,余额 …)都来自当前合约。delegatecall 的目的是使用另一个合约中的库代码。

委托调用是:委托对方调用自己数据的。类似授权转账,比如我部署一个 Bank 合约, 授权 ContractA 使用 Bank 地址内的资金,ContractA 只拥有控制权,但是没有拥有权。

  • 委托调用后,所有变量修改都是发生在委托合约内部,并不会保存在被委托合约中。
    • 利用这个特性,可以通过更换被委托合约,来升级委托合约。
  • 委托调用合约内部,需要和被委托合约的内部参数完全一样,否则容易导致数据混乱
    • 可以通过顺序来避免这个问题,但是推荐完全一样

例子 1(重要)

代码如下:

  • DelegateCall 是委托合约
  • TestVersion1 是第 1 次被委托合约
  • TestVersion2 是第 2 次被委托合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// 合约版本 V.1
contract TestVersion1 {
    address public sender;
    uint256 public value;
    uint256 public num;

    function set(uint256 num_) external payable {
        sender = msg.sender;
        value = msg.value;
        num = num_;
    }
}

// 合约版本 V.2
contract TestVersion2 {
    address public sender;
    uint256 public value;
    uint256 public num;

    function set(uint256 num_) external payable {
        sender = msg.sender;
        value = msg.value;
        num = num_ * 2;
    }
}

// 委托调用测试
contract DelegateCall {
    address public sender;
    uint256 public value;
    uint256 public num;

    function set(address _ads, uint256 num_) external payable {
        sender = msg.sender;
        value = msg.value;
        num = num_;
        // 第1种 encode
        // 不需知道合约名字,函数完全自定义
        bytes memory data1 = abi.encodeWithSignature("set(uint256)", num_);
        // 第2种 encode
        // 需要合约名字,可以避免函数和参数写错
        bytes memory data2 = abi.encodeWithSelector(TestVersion1.set.selector, num_);

        (bool success, bytes memory _data) = _ads.delegatecall(data2);

        require(success, "DelegateCall set failed");
    }
}

简单说下这个例子的原理

/**
委托调用
用户A 调用 DelegateCallB 合约, 发送 100 wei ; DelegateCallB 委托调用 Test1
此时在 Test1 合约内部
        msg.sender = A
        msg.value = 100
        Test1 内部如果有状态变量修改,也不会被修改,会在DelegateCallB 内改变
        发送到 Test1 内的ETH主币,会被留在 DelegateCallB 内,不会在Test1 内
 */

⓼ staticcall() 静态调用

用给定的有效载荷(payload)发出低级 STATICCALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账)

<address>.staticcall(bytes memory) returns (bool, bytes memory)

它与 call 基本相同,发送所有可用 gas,也可以自己调节 gas,但如果被调用的函数以任何方式修改状态变量,都将回退

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

// 被调用的合约
contract Hello1 {
    function echo() external pure returns (string memory) {
        return "Hello World!";
    }
}

contract Hello2 {
    uint8 public a;
    function echo() external returns (string memory) {
        a = 1;
        return "Hello World!";
    }
}

// 调用者合约
contract SoldityTest {
    function callHello(address ads_) external view returns (string memory) {
        // 编码被调用者的方法签名
        bytes4 methodId = bytes4(keccak256("echo()"));

        // 调用合约
        (bool success, bytes memory data) = ads_.staticcall(
            abi.encodeWithSelector(methodId)
        );
        if (success) {
            return abi.decode(data, (string));
        } else {
            return "error";
        }
    }
}

⓽ 三种 call 的总结

  1. calldelegatecallstaticcall 都是非常低级的函数,应该只把它们当作最后一招来使用,它们破坏了 Solidity 的类型安全性。
  2. 三种方法都提供 gas 选项,而 value 选项仅 call 支持 。所以三种 call 里只有 call 可以进行 ETH 转账,其他两种不可以进行转账。
  3. 不管是读取状态还是写入状态,最好避免在合约代码中硬编码使用的 gas 值。这可能会引入错误,而且 gas 的消耗也是动态改变的。
  4. 如果在通过低级函数 delegatecall 发起调用时需要访问存储中的变量,那么这两个合约的存储布局需要一致,以便被调用的合约代码可以正确地通过变量名访问合约的存储变量。 这不是指在库函数调用(高级的调用方式)时所传递的存储变量指针需要满足那样情况。

⚠️ 注意: 在 0.5.0 版本以前, .call, .delegatecall and .staticcall 仅仅返回成功状态,没有返回值。

⚠️ 在 0.5.0 版本以前, 还有一个 callcode 函数,现在已经去除。

⓾ transfer / send / call 三种转账的总结

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

contract Demo {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function byTransfer() public {
        payable(msg.sender).transfer(100);
    }

    function bySend() public {
        bool success = payable(msg.sender).send(100);
        require(success, "Send Fail");
    }

    // 如果使用 transfer 或 send 函数必须添加fallback回退函数
    fallback() external {}

    receive() external payable {}
}

相同点:

  • 三种方法都可以进行转账
  • _to.transfer(100)_to.send(100)_to.call{value: 100}("") 的接收方都是_to
    • 如果_to是合约,则合约中必须增加 fallback 或者 receive 函数!
    • 否则报错In order to receive Ether transfer the contract should have either 'receive' or payable 'fallback' function

不同点:

  • 低级 CALL 调用:不需要 payable address
    • transfer 和 send 只能是 payable address
  • call 的 gas 可以动态调整
    • transfer 和 send 只能是固定制 2300
  • call 除了可以转账外,可以还可以调用不知道 ABI 的方法,还可以调用的时候转账
    • 当调用不存在的合约方法时候,会触发对方合约内的 fallback 或者 receive
    • 如果使用 _to.call{value: 100}(data),那么data中被调用的方法必须添加 payable 修饰符,否则转账失败!
    • 因为可以调用方法,所以 call 有两个参数,除了一个 bool 值代表成功或者失败,另外一个是可能存在的 data,比如创建合约时候得到部署的地址,调用函数时候得到的函数放回值。

⓪ 注意事项·

send

使用 send 有很多危险:如果调用栈深度已经达到 1024(这总是可以由调用者所强制指定),转账会失败;并且如果接收者用光了 gas,转账同样会失败。为了保证以太币转账安全,总是检查 send 的返回值,利用 transfer 或者下面更好的方式: 用这种接收者取回钱的模式。

call

在执行另一个合约函数时,应该尽可能避免使用 .call() ,因为它绕过了类型检查,函数存在检查和参数打包。

由于 EVM 会把对一个不存在的合约的调用作为是成功的。 Solidity 会在执行外部调用时使用 extcodesize 操作码进行额外检查。 这确保了即将被调用的合约要么实际存在(它包含代码)或者触发一个异常。低级调用不 包括这个检查,这使得它们在 GAS 方面更便宜,但也更不安全

上面的这三个 call 方法都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了 Solidity 的类型安全。

5️⃣ 值类型:合约类型

每一个合约定义都有他自己的类型。

  • 可以隐式地将合约转换为从他们继承的合约。
  • 合约可以显式转换为 address 类型。
  • 可以转换为 address payable 类型

⚠️ 注意:合约不支持任何运算符。

本节配套视频

1.创建的例子

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

contract A {
    uint256 public a = 123;

    fallback() external {}

    receive() external payable {}
}

contract C {
    A public a1;
    // A public a1 = A(payable(0xa0808B3e1713ff8C66b89aa4d0033c9ACfe37016));
    A public a2 = new A();

    // 先部署后,然后传入地址
    function getA1(A _a) external pure returns (address, address) {
        return (address(_a), payable(address(_a)));
    }

    // 内部直接new创建
    function getA2() external view returns (address, address) {
        return (address(a2), payable(address(a2)));
    }


    function test1(A _a) external view returns (uint256) {
        return _a.a();
    }
    function test2() external view returns (uint256) {
        return a2.a();
    }
}

如果声明一个合约类型的局部变量( MyContract c ),则可以调用该合约的函数。 注意需要赋相同合约类型的值给它。

还可以实例化合约(即新创建一个合约对象),使用 new 创建合约。

合约和 address 的数据表示是相同的.

2.转钱的例子

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

contract A {
    uint256 public a = 123;

    fallback() external {}

    receive() external payable {}
}

contract B {
    // 没有 fallback / receive
    uint256 public b = 123;
}

contract C {
    A public a = new A();
    B public b = new B();

    function transferA() external payable returns (address, address) {
        payable(address(a)).transfer(msg.value);
        return (address(a), payable(address(a)));
    }


    function transferB() external payable returns (address, address) {
        payable(address(b)).transfer(msg.value);
        return (address(b), payable(address(b)));
    }

        // 获取合约的余额
    function getBalance(address ads_) external view returns (uint256) {
        return ads_.balance;
    }
}

3.合约的属性

合约类型的成员是合约的外部函数及 public 的 状态变量。

对于合约 C 可以使用 type(C) 获取合约的类型信息,

  • type(C).name
    • 获得合约名
  • type(C).creationCode
    • 获得包含创建合约字节码的内存字节数组。
    • 该值和合约内使用 address(this).code; 结果一样。
    • 它可以在内联汇编中构建自定义创建例程,尤其是使用 create2 操作码。
    • 不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
  • type(C).runtimeCode
    • 获得合约的运行时字节码的内存字节数组。这是通常由 C 的构造函数部署的代码。
    • 如果 C 有一个使用内联汇编的构造函数,那么可能与实际部署的字节码不同。
    • 还要注意库在部署时修改其运行时字节码以防范定期调用(guard against regular calls)。 与 .creationCode 有相同的限制,不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。

⓵ 无 constructor

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

contract Test {
    address public owner;

    function getCode() public view returns (bytes memory) {
        return address(this).code;
    }
}

contract C {
    string public name = type(Test).name;

    bytes public creationCode = type(Test).creationCode;

    bytes public runtimeCode = type(Test).runtimeCode;
}

// Test.getCode
//

// creationCode
//

// runtimeCode
// 和 Test.getCode 相同

⓶ 有 constructor

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

contract Test {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function getCode() public view returns (bytes memory) {
        return address(this).code;
    }
}

contract C {
    string public name = type(Test).name;

    bytes public creationCode = type(Test).creationCode;

    // runtimeCode 不能获取 constructor 修改 immutable 变量的数据
    // 比如 Test 里的owner 不能是 immutable 类型
    // "runtimeCode" is not available for contracts containing immutable variables.
    // 等于合约地址上的属性 address(this).code
    bytes public runtimeCode = type(Test).runtimeCode;
}

// Test.getCode
//

// creationCode
//

// runtimeCode
// 和 Test.getCode 相同

6️⃣ 引用类型的额外注解:数据位置

data location ,中文名为数据位置。

在讲引用类型之前,先介绍数据位置。这是因为在 Solidity 中,引用类型是由简单数据类型组合而成,相比于简单的值类型,这些类型通常通过名称引用。这些类型涉及到的数据量较大,复制它们可能要消耗大量 Gas,所以我们在使用引用数据类型时,必须考虑存储位置。我们需要仔细考虑数据是保存在内存中,还是在 EVM 存储区中。这就是线介绍数据位置的原因。

注意:所有的引用类型,都有数据位置这个额外的注解来指定存储在哪里,所以一定要掌握好。

总结:如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里

1.数据位置的基础介绍

在合约中声明和使用的变量都有一个数据位置,合约变量的数据位置将会影响 Gas 消耗量。

Solidity 提供的有三种如下数据位置。

  • 存储 storage : 状态变量保存的位置,只要合约存在就一直存储.
  • 内存 memory : 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
  • 调用数据 calldata : 用来保存函数参数的特殊数据位置,是一个只读位置。
    • 调用数据 calldata 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存 memory 。
    • 主要用于外部函数的参数,但也可用于其他变量,无论外部内部函数都可以使用。

核心:更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储 storage 来说)的复制仅在某些情况下进行拷贝。

本节配套视频

⓵ storage

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

contract DataLocation {
    // storage
    uint256 stateVariable = 1;
    uint256[] stateArray = [1, 2, 3];

    // uint storage stateVariable; // Expected identifier but got 'storage'
    // uint[] memory stateArray; // Expected identifier but got 'memory'
}

该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。保存在存储区(storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。

storage 是永久存储在以太坊区块链中,更具体地说存储在存储 Merkle Patricia 树中,形成帐户状态信息的一部分。一旦使用这个类型,数据将永远存在。 扩展: 默克尔树(merkle tree)

重点:状态变量总是存储在存储区(storage)中,并且不能显式地标记状态变量的位置。。状态变量是强制为 storage。

⓶ memory

内存位置是临时数据,比存储位置便宜。它只能在函数中访问。通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。

memory:存储在内存中,即分配、即使用,越过作用域则不可访问,等待被回收

重点 1:函数参数(包括返回参数)都存储在内存中。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract DataLocation {
    function add(uint256 num1, uint256 num2)
        public
        pure
        returns (uint256 result)
    {
        return num1 + num2;
    }
}

上面例子中: 函数参数 uint num1uint num2,返回值 uint result 都存储在内存中。

重点 2:引用类型的局部变量,需要显式指定数据位置(storage/memory)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Locations {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }
    Book public java; // 一本 java 书
    mapping(address => uint256) public balances;

    function test() public {
        /* 此处都是局部变量  */
        // 值类型:所以它们被存储在内存中
        bool flag = true;
        uint256 number = 1;
        address account = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
        bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff)));

        // Data location can only be specified for array, struct or mapping types,
        // but "memory" was given.
        // bool memory flag2; // 错误:值类型的数据不能标示 memory

        // 引用类型:需要显示指定数据位置,此处指定为内存
        uint256[] memory localArray; // array
        // uint8[] memory nums = [1, 2, 3]; // 内存中不能创建动态数组
        uint8[3] memory numsFixed = [1, 2, 3];
        uint256[] memory a = new uint256[](5); // 推荐
        a[1] = 1;
        a[2] = 2;
        a[3] = 3;
        a[4] = 4;

        string memory myStr = "hello"; // string

        // 映射不能在函数中动态创建,您必须从状态变量中分配它们。
        // mapping(address => bool) memory myMapping;
        mapping(address => uint256) storage ref = balances; // mapping
        java = Book({title: "Solidity", author: "Anbang", book_id: 1}); // struct
        bytes memory bc = bytes("!"); //
    }
}
  • mapping 和 struct 类型,不能在函数中动态创建,必须从状态变量中分配它们。
  • 内存中不能创建动态数组
重点 3:函数的输入和输出参数如果是数组,使用 memory
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract DataLocations {
    // name_ 是 string ,所以使用 memory
    // mm_ 是 uint256[] ,所以使用 memory
    // 输出相同,也是使用 memory
    function examples2(string memory name_, uint256[] memory mm_)
        external
        pure
        returns (uint256[] memory memArr, string memory myName)
    {
        memArr = new uint256[](mm_.length);
        myName = name_;
        for (uint256 index = 0; index < mm_.length; index++) {
            memArr[index] = mm_[index];
        }
    }
}
重点 4:引用类型的局部变量:指定 storage 和 memory 的区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract DataLocations {
    struct MyStruct {
        string name;
        uint256 age;
    }
    mapping(address => MyStruct) public myStructs;

    function test1() external returns (MyStruct memory) {
        myStructs[msg.sender] = MyStruct({name: "Anbang1", age: 18});

        // storage 会修改状态变量
        MyStruct storage myStruct1 = myStructs[msg.sender];
        myStruct1.age++;
        return myStruct1;
    }

    function test2() external returns (MyStruct memory) {
        myStructs[msg.sender] = MyStruct({name: "Anbang2", age: 18});

        // memory 函数运行完后即消失,修改的值也不会储存在状态变量中
        MyStruct memory myStruct2 = myStructs[msg.sender];
        myStruct2.age++;
        return myStruct2;
    }
}
  • storage修改引用数据: 会修改状态变量
  • memory修改引用数据: 函数运行完后即消失,修改的值也不会储存在状态变量中

⓷ calldata

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

contract DataLocations {
    function test(uint256[] calldata mm_)
        external
        pure
        returns (uint256[] calldata)
    {
        // mm_[0] = 1; // Calldata arrays are read-only.
        return mm_;
    }
}

calldata 是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,calldata 是外部函数(external function)的参数的默认位置。外部函数(external function)的参数存储在 calldata 中。函数的返回值中也可以使用 calldata 数据位置的数组和结构,但是无法给其分配空间。

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

contract DataLocations {
    // 参数 [1,2,3]      消耗 25586 gas
    // 参数 [1,2,3,4,5,6,7,8,9,0] 消耗 32114 gas
    function iMemory(uint256[] memory _mm)
        external
        pure
        returns (uint256[] memory memArr)
    {
        memArr = new uint256[](_mm.length);
        for (uint256 index = 0; index < _mm.length; index++) {
            memArr[index] = _mm[index];
        }
    }

    // 参数 [1,2,3]      消耗 24551 gas
    // 参数 [1,2,3,4,5,6,7,8,9,0] 消耗 29510 gas
    function iCalldata(uint256[] calldata _mm)
        external
        pure
        returns (uint256[] memory memArr)
    {
        memArr = new uint256[](_mm.length);
        for (uint256 index = 0; index < _mm.length; index++) {
            memArr[index] = _mm[index];
        }
    }
}
  • 要点: calldata 只能用在函数的输入和输出参数中
  • 要点: calldata 用在输入参数中,比 memorg 更省 gas
  • 要点: calldata 的参数不允许修改,但是 memorg 参数允许修改

存储函数参数,它是只读的,不会永久存储的一个数据位置。外部函数(external function)的参数被强制指定为 calldata,效果与 memory 类似。

注解: 如果可以的话,请尽量使用 calldata 作为数据位置,因为它将避免复制,并确保不能修改数据。

注解: 在 0.6.9 版本之前,引用类型参数的数据位置有限制,主要表现在函数的可见性上;外部函数中使用 calldata ,公共函数中使用 memory ,以及内部和私有函数中的 memory 或 storage 。 现在 memory 和 calldata 在所有函数中都被允许使用,无论其可见性如何。

⓸ stack

堆栈是由 EVM (Ethereum 虚拟机)维护的非持久性数据。EVM 使用堆栈数据位置在执行期间加载变量。堆栈位置最多有 1024 个级别的限制。

⓹ 小结

按照关键字:

  • storage: 存储区: 状态变量总是储存在存储区
  • memory: 内存区: 局部变量使用,只在内存中生效。
    • 值类型的局部变量,存储在内存中。
    • 引用类型局部变量,需要显式地指定数据位置
    • 函数的输入参数如果是数组或者 string,必须是 memorycalldata
    • 内存中的数组必须是定长数组(不能使用 push 赋值),动态数组只能储存在状态变量中。
  • calldata
    • 和 memory 类似,但是 calldata 只能用在函数的输入参数中。
    • 相比使用 memory ,合约输入参数如果使用 calldata, 可以节约 gas

按照函数参数:

  • 内部函数参数: (包括返回参数)都存储在**memory(内存)**中。
  • 外部函数参数: (不包括返回参数)存储在 calldata 中。

2.不同数据位置之间的赋值规则

本小节总结如下:

  1. 存储变量 赋值给 存储变量 (同类型)
    • 值 类 型: 创建一个新副本。
    • 引用类型: 创建一个新副本。
  2. 内存变量 赋值给 存储变量
    • 值 类 型: 创建一个新副本。
    • 引用类型: 创建一个新副本。
  3. 存储变量 赋值给 内存变量
    • 值 类 型: 创建一个新副本。
    • 引用类型: 创建一个新副本。
  4. 内存变量 赋值给 内存变量 (同类型)
    • 值 类 型: 创建一个新副本。
    • 引用类型: 不会创建副本。(重要)

本节配套视频

⓵ 将存储变量赋值给存储变量

将一个状态(存储)变量赋值给另一个状态(存储)变量,将创建一个新的副本。

  • 值 类 型: 创建一个新副本。
  • 引用类型: 创建一个新副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Locations {
    // 值类型
    uint256 public stateA1 = 10;
    uint256 public stateA2 = 20;
    // 引用类型
    string public stateB1 = "ABCD";
    string public stateB2 = "1234";

    function testA() public returns (uint256) {
        stateA1 = stateA2;
        stateA2 = 30;
        return stateA1; // returns 20
    }

    function testB() public returns (string memory) {
        stateB1 = stateB2;
        bytes(stateB2)[0] = bytes1("9");
        return stateB1; // returns 1234
    }
}

问答题: 上面函数 testAtestB 的返回值是什么?

  • testA: 第一次执行返回值是 20,之后执行返回值是 30
  • testB: 第一次执行返回值是字符串 "1234",之后执行返回值是字符串 "9234"

值类型: 先将 stateA2 赋值给 stateA1,再把 stateA2 修改;结果得到的 stateA1 是 stateA2 修改前的值,说明对于值类型的局部变量来说 => 创建一个新副本。

引用类型: 先将 stateB2 赋值给 stateB1,再把 stateB2 修改;结果得到的 stateB1 是 stateB2 修改前的值,说明对于引用类型的局部变量来说 => 创建一个新副本。

⓶ 将内存变量赋值给存储变量

将内存变量赋值给存储变量,总会创建一个新副本。

  • 值 类 型: 创建一个新副本。
  • 引用类型: 创建一个新副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Locations {
    uint256 public stateA1 = 10; //storage
    string public stateB1 = "ABCD";

    function testA() public returns (uint256) {
        uint256 memoryA2 = 20; // memory
        stateA1 = memoryA2;
        memoryA2 = 40;
        return stateA1; // returns 20
    }

    function testB() public returns (string memory) {
        string memory memoryB2 = "1234"; // memory
        stateB1 = memoryB2;
        bytes(memoryB2)[0] = bytes1("9");
        return stateB1; // returns 1234
    }
}

问答题: 上面函数 testAtestB 的返回值是什么?

  • testA: 永远返回 20
  • testB: 永远返回字符串 "1234"

值类型: 先将 memoryA2 赋值给 stateA1,再把 memoryA2 修改;结果得到的 stateA1 是 memoryA2 修改前的值,说明对于值类型的局部变量来说 => 创建一个新副本。

引用类型: 先将 memoryB2 赋值给 stateB1,再把 memoryB2 修改;结果得到的 stateB1 是 memoryB2 修改前的值,说明对于引用类型的局部变量来说 => 创建一个新副本。

⓷ 将存储变量赋值给内存变量

从存储变量复制到内存变量,将创建一个副本。

  • 值 类 型: 创建一个新副本。
  • 引用类型: 创建一个新副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Locations {
    uint256 public stateA1 = 10; //storage
    string public stateB1 = "ABCD";

    function testA() public returns (uint256) {
        uint256 memoryA2 = 20; // memory
        memoryA2 = stateA1;
        stateA1 = 40;
        return memoryA2; // returns 第一次是 10, 以后都是40
    }

    function testB() public returns (string memory) {
        string memory memoryB2 = "1234"; // memory
        memoryB2 = stateB1;
        bytes(stateB1)[0] = bytes1("9");
        return memoryB2; // returns 第一次是 "ABCD", 以后都是 "9BCD"
    }
}

问答题: 上面函数 testA 和 testB 的返回值是什么?

  • testA: 第一次执行返回值是 10,之后执行返回值是 40
  • testB: 第一次执行返回值是字符串 "ABCD",之后执行返回值是字符串 "9BCD"

值类型: 先将 stateA1 赋值给 memoryA2,再把 stateA1 修改;结果得到的 memoryA2 是 stateA1 修改前的值,说明对于值类型的局部变量来说 => 创建一个新副本。

引用类型: 先将 stateB1 赋值给 memoryB2,再把 stateB1 修改;结果得到的 memoryB2 是 stateB1 修改前的值,说明对于引用类型的局部变量来说 => 创建一个新副本。

⓸ 将内存变量赋值给内存变量

  • 对于值类型的局部变量: 创建一个新副本。
  • 对于引用类型局部变量: 不会创建副本。(重要)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Locations {
    function testA() public pure returns (uint256) {
        uint256 memoryA1 = 10; // memory
        uint256 memoryA2 = 20; // memory
        memoryA1 = memoryA2;
        memoryA2 = 40;
        return memoryA1; // returns 永远是 20
    }

    function testB() public pure returns (string memory) {
        string memory memoryB1 = "ABCD"; // memory
        string memory memoryB2 = "1234"; // memory
        memoryB1 = memoryB2;
        bytes(memoryB2)[0] = bytes1("9");
        return memoryB1; // returns 永远是 "9234"
    }
}

问答题: 上面函数 testA 和 testB 的返回值是什么?

  • testA: 永远是 20
  • testB: 永远是字符串 "9234"

值类型: 先将 memoryA2 赋值给 memoryA1,再把 memoryA2 修改;结果得到的 memoryA1 是 memoryA2 修改前的值,说明对于值类型的局部变量来说,此时仍然创建一个新副本。

引用类型: 先将 memoryB2 赋值给 memoryB1,再把 memoryB2 修改;结果得到的 memoryB1 是 memoryB2 修改后的值。说明它们都指向相同的存储位置,并不会创建新副本。

⚠️ 重点:对于引用类型的局部变量,从一个内存变量复制到另一个内存变量不会创建副本,共享内存

⓹ 小结

数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:

  • 在 storage 和 memory 之间两两赋值(或者从 calldata 赋值 ),都会创建一份独立的拷贝。
  • 从 memory 到 memory 的赋值只创建引用,这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
  • 从 storage 到本地存储变量的赋值也只分配一个引用。
  • 其他的向 storage 的赋值,总是进行拷贝。 这种情况的示例,如对状态变量或 storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝。

3.深刻理解引用类型赋值和修改

本节配套视频

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

contract Locations {
    string public stateB1 = "ABCD";
    struct MyStruct {
        string name;
        uint256 age;
    }
    mapping(address => MyStruct) public stateC1;

    constructor() {
        stateC1[msg.sender] = MyStruct({name: "Anbang", age: 1});
    }

    function testB1() public returns (string memory, string memory) {
        string memory memoryB2 = "1234"; // memory
        stateB1 = memoryB2;

        // storage 修改:会改变状态变量
        string storage stateB3 = stateB1;
        bytes(stateB3)[0] = bytes1("9");
        return (stateB1, stateB3);
        // returns (9234,9234)
        // 储存空间中 stateB1 = 9234
    }

    function testB2() public returns (string memory, string memory) {
        string memory memoryB2 = "1234"; // memory
        stateB1 = memoryB2;

        // memory 修改:不会改变状态变量
        string memory memoryB3 = stateB1;
        bytes(memoryB3)[0] = bytes1("9");
        return (stateB1, memoryB3);
        // returns (1234,9234)
        // 储存空间中 stateB1 = 1234
    }

    function testC1() external returns (MyStruct memory, MyStruct memory) {
        MyStruct memory memoryC2 = MyStruct({name: "Anbang1", age: 18});
        stateC1[msg.sender] = memoryC2;

        // storage 修改:会改变状态变量
        MyStruct storage stateC3 = stateC1[msg.sender];
        stateC3.age++;
        return (stateC1[msg.sender], stateC3);
        // returns ({name: "Anbang1", age: 19},{name: "Anbang1", age: 19})
        // 储存空间中 stateC1 = {name: "Anbang1", age: 19}
    }

    function testC2() external returns (MyStruct memory, MyStruct memory) {
        MyStruct memory memoryC2 = MyStruct({name: "Anbang2", age: 18});
        stateC1[msg.sender] = memoryC2;

        // memory 修改:不会改变状态变量
        MyStruct memory memoryC3 = stateC1[msg.sender];
        memoryC3.age++;
        return (stateC1[msg.sender], memoryC3);
        // returns ({name: "Anbang2", age: 18},{name: "Anbang2", age: 19})
        // 储存空间中 stateC1 = {name: "Anbang2", age: 18}
    }
}

问答题:

  • testB1 运行后,返回什么?
    • (9234,9234)
  • testB2 运行后,返回什么?
    • (1234,9234)
  • testC1 运行后,返回什么?
    • ({name: "Anbang1", age: 19},{name: "Anbang1", age: 19})
  • testC2 运行后,返回什么?
    • ({name: "Anbang2", age: 18},{name: "Anbang2", age: 19})

4.calldata 和 memeory 区别

本节配套视频

函数调用函数时的区别:

calldata可以隐式转换为memory

  • calldata 参数可以隐式转换为 memory
  • memory 参数不可以隐式转换为 calldata
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract DataLocations {
    function memoryFn(uint256[] memory _mm)
        private
        pure
        returns (uint256[] memory memArr)
    {
        memArr = new uint256[](_mm.length);
        for (uint256 index = 0; index < _mm.length; index++) {
            memArr[index] = _mm[index];
        }
    }

    function calldataFn(uint256[] calldata _mm)
        private
        pure
        returns (uint256[] memory memArr)
    {
        memArr = new uint256[](_mm.length);
        for (uint256 index = 0; index < _mm.length; index++) {
            memArr[index] = _mm[index];
        }
    }


    function examples1(uint256[] memory _mm)
        external
        pure
        returns (uint256[] memory memArr)
    {
        // memoryFn 参数是 memory,可以调用
        // calldataFn 参数是 calldata ,不可以调用, memory 不可以隐式转换为 calldata
        // memory 参数,调用需要 memory 参数的函数: 成功
        memArr = memoryFn(_mm);

        // memory 不能隐式转换为 calldata
        // memArr = calldataFn(_mm); // memory 参数,调用需要 calldata 参数的函数: 禁止
    }

    function examples2(uint256[] calldata _mm)
        external
        pure
        returns (uint256[] memory memArr)
    {
        // calldata 参数,调用需要 calldata 参数的函数: 成功
        memArr = calldataFn(_mm);

        // memoryFn 参数是 memory,可以调用,calldata可以隐式转换为 memory
        // calldataFn 参数是 calldata ,直接使用calldata更省gas
        // calldata(小:约束多) 可以隐式的转换 memory(大)
        // calldata 参数,调用需要 memory 参数的函数: 成功
        // memArr = memoryFn(_mm);
    }

}

⓶ calldata 和 memeory 对比

contract Test {
    function memoryFn(uint256[] memory _num)
        public
        pure
        returns (uint256[] memory)
    {
        _num[0] = 999; // 修改参数
        return (_num);
    }

    function calldataFn(uint256[] calldata _num)
        public
        pure
        returns (uint256[] calldata)
    {
        // _num[0] = 999; // 禁止修改 calldata 数据
        return (_num);
    }
}

7️⃣ 引用类型

Solidity 中,有一些数据类型由值类型组合而成,相比于简单的值类型,这些类型通常通过名称引用,被称为引用类型。

  • array
    • 基本类型组成的数组集合。
    uint256[5] public T1 = [1, 2, 3, 4, 5];
    address[5] public A = [0xff...6ac];
    byte[5] public A = [0xff...6ac];
    
    • 字符串与 bytes 是特殊的数组,所以也是引用类型
  • string: 是一个动态尺寸的 utf-8 编码字符串
    • 他其实是一个特殊的可变字节数组,同时其也是一个引用类型
  • bytes: 动态十六进制字节数组
    • bytes 类似于 byte[],但它在 calldata 中被紧密地打包。因此,相比于 byte[],bytes 应该优先使用,因为更便宜。
    • string 等价于 bytes,但不允许长度或索引访问。
  • mapping
  • struct:为了允许 evm 的优化,请确保 storage 中的变量和 struct 成员的书写顺序允许它们被紧密地打包。例如,应该按照 uint128,uint128,uint256 的顺序来声明状态变量,而不是使用 uint128,uint256,uint128,因为前者只占用两个存储插槽,而后者将占用三个。

1.array 数组

本节配套视频

数组是存储同类元素的有序集合。数组声明时可以是固定大小的,也可以是动态调整长度。

下面是 array 的总结:

  • 声明和初始化数组
    • 数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 存储 storage 中,并且公开访问函数的参数需要是 ABI 类型。
  • 访问和修改数组元素
    • arr[_index]:
      • 通过索引进行获取特定元素
      • 可以通过索引修改值
    • 状态变量标记 public 的数组,Solidity 创建一个 getter函数 。 下标的索引数字就是 getter函数 的参数。
    • 访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push() 方法在末尾追加一个新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。
  • 函数中返回数组
    • 如果想把数组全部返回,需要通过函数进行操作。在函数中返回数组
  • 动态数组和定长数组
    • 动态数组只能存在于状态变量中
    • 内存中只能创建定长数组
  • 创建内存数组
    • 对于 storage 数组,元素可以是任意类型(其他数组、映射或结构)。
    • 对于 memory 数组,元素类型不能是映射类型,如果它是一个 public 函数的参数,那么元素类型必须是 ABI 类型。
  • 数组的属性
    • length: 获取数组的长度
  • 数组的方法
    • push : 只有动态数组可以使用,只能用在动态数组上
    • pop: 删除最后一个长度,只能用在动态数组上
    • delete: 清空对应的索引;清空不是删除,并不会改变长度,索引位置的值会改为默认值。
    • 数组切片: x[start:end]
  • 写一个完全删除的 delete 方法

⓵ 数组的创建

数组长度上分为 固定长度数组可变长度数组,类型上分为一维数组多维数组

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]

  • 固定长度数组:创建
  • 可变长度数组:创建
  • 二维数组:创建
  • 其它
    • uint256[2][] public T = new uint256[2][](10);
1.固定长度数组:创建

固定长度数组:创建

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

contract Test {
    // 固定长度数组
    uint256[5] public T = [1, 2, 3, 4, 5];
    address[5] public A =   [0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac];
    uint256[10] public arr1 = [0, 1, 2]; // 赋值的数组长度不超过10都可以
    uint256[10] public arr2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    // 如果初始化超出了数组的预期长度,报错:
    // Type uint8[11] memory is not implicitly convertible to expected
    // type uint256[10] storage ref.
    // uint256[10] public arr3 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}
  • 语法: type[arraySize] arrayName;,这是一维数组,其中 arraySize 必须是一个大于零的整数数字,type 可以是任何数据类型。
  • 固定长度数组创建后不可对长度进行修改,但是可以对内容进行修改
    • (不可对长度进行修改是与不可变字节数组之间不同点)

数组先声明再赋值

通过索引进行赋值。

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

contract Demo {
    uint256[10] public arr1;

    function test() external {
        arr1[0] = 1;
        arr1[1] = 10;
        arr1[2] = 100;
        arr1[9] = 900;
    }
}
2.可变长度数组:创建
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Test {
    // 可变长度数组
    uint256[] public T1 = [1, 2, 3, 4, 5]; // 方式 1
    uint256[] public T2 = new uint256[](5); // 方式 2
}
  • 方式 1: uint256[] T1 = [1, 2, 3, 4, 5];
    • 该方式不可以在函数内创建
  • 方式 2: uint256[] T2 = new uint256[](5);
    • 用方式 2 创建数组时,若数组为成员变量, 则默认为 storage 类型;
    • 若为局部变量默认为 memory 类型,memory 类型的数组,必须声明长度,并且长度创建后不可变。
    • push 方法不能用在 memeory 的数组上,只能逐个索引的赋值。
3.内存中创建数组

不能直接创建:

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

contract Demo {
    uint256[] public x = [uint256(1), 3, 4];

    // 下面这段代码并不能编译。
    function f() public {
        uint256[] memory x = [uint256(1), 3, 4];
    }
}

可以使用 new 关键字在内存中创建动态数组。创建格式: uint256[] memory x = new uint256[](3);

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

contract C {
    function f() public pure {
        uint256[] memory x = new uint256[](3);
    }
}
  • 内存中创建的数组是局部变量。
  • 内存中不能创建动态数组,必须创建定长数组。
    • 思考: 插入排序的例子中,优化后的代码是动态数组,还是定长数组?(插入排序在后面算法那一章)

memory 类型的数组长度创建后不可变,不能通过修改成员变量 .push 改变 memory 数组的大小。必须提前计算数组大小,或者创建一个新的内存数组并复制每个元素。

例子 0 : 显示给各个元素赋值:

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

contract C {
    function f() public pure {
        uint256[] memory x = new uint256[](3);
        x[0] = 1;
        x[1] = 3;
        x[2] = 4;
    }
}

例子 1: 新分配的数组元素总是以 默认值 初始化。

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

contract FunctionOutputs {
    function examples1() external pure returns (uint256[] memory) {
        uint256[] memory a = new uint256[](5);
        a[1] = 1;
        a[2] = 2;
        a[3] = 3;
        a[4] = 4;
        return a;
    }

    // 在 Solidity 中的所有变量,新分配的数组元素总是以 默认值 初始化。
    function examples2(uint256 _len)
        external
        pure
        returns (uint256[] memory b)
    {
        require(_len > 1, "length > 1");
        b = new uint256[](_len);
        b[0] = 666;
    }

    function examples3(uint256 _len) external pure returns (bytes memory b) {
        require(_len > 1, "length > 1");
        b = new bytes(_len);
        b[0] = bytes1("A");
    }
}
4.动态数组和定长数组的 gas 区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract FunctionOutputs {
    // 26086 gas
    uint256[] public nums = [1, 2, 3];

    // 23913 gas
    uint256[3] public numsFixed = [1, 2, 3];
}

在 Remix 中部署后,如果获取 nums,需要传入索引获取 nums 的对应 inedx 值。其中动态数组 nums 查看需要 26086 gas,定长数组 numsFixed 查看仅需 23913 gas。如果能使用定长数组,就使用定长数组,因为它很便宜。

5.二维数组:创建:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    // length:3
    uint256[2][3] public T = [[1, 2], [3, 4], [5, 6]];

    function getLength() external view returns (uint256) {
        return T.length;
    }
}
  • 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组(二维数组),应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。在 Solidity 中, X[3] 总是一个包含三个 X 类型元素的数组,即使 X 本身就是一个数组.
  • uint256[2][3] public T = [[1, 2], [3, 4], [5, 6]];
  • T.length 为 3

⓶ 访问和修改数组元素

  • 通过索引访问数组元素
  • 通过索引修改数组元素

注意: arr[index] 中的 index 需要小于 arr.length

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

contract Demo {
    uint256[10] public arr1 = [0, 1, 2]; // 赋值的数组长度不超过10都可以
    uint256[10] public arr2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    function test() external view returns (uint256, uint256) {
        return (arr1[2], arr2[5]);
    }

    function modi() external {
        arr1[2] = 666;
        arr2[5] = 666;
    }
}

这种可以查看到元素的指定元素,但有时候我们可能想要查看元素的所有内容。这时候就需要函数处理一下。

⓷ 函数中返回整个数组

通过函数把数组的所有内容全部返回。

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

contract FunctionOutputs {
    uint256[] public nums1 = [1, 2, 3];
    uint256[3] public nums2 = [1, 2, 3];

    function test1() external view returns (uint256[] memory) {
        return nums1;
    }

    function test2() external view returns (uint256[3] memory) {
        return nums2;
    }
}

⓸ 数组常量

正常看到下方代码应该没什么问题,但是注意:函数 s 中数组类型是uint256,而函数 t 中输入的数组类型是uint8, 这里需要将 uint8 转换一下s([uint256(1), uint256(2)]);;

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

contract T {
    function s(uint256[2] memory _arr) public {}

    function t() public {
        // Invalid type for argument in function call.
        // Invalid implicit conversion from uint8[2] memory to uint256[2] memory requested.
        // s([1, 2]); // 默认这么写不行的 ❌
        s([uint256(1), uint256(2)]); // ✅
    }
}

数组常量(字面量)是在方括号中( [...] ) 包含一个或多个逗号分隔的表达式。例如 [1, a, f(3)]

数组常量的类型通过以下的方式确定:

  • 它总是一个静态大小的内存数组,其长度为表达式的数量。
  • 数组的基本类型是列表上的第一个表达式的类型,以便所有其他表达式可以隐式地转换为它。如果不可以转换,将出现类型错误。
  • 所有元素都都可以转换为基本类型也是不够的。其中一个元素必须是明确类型的。

在下面的例子中,[1, 2, 3] 的类型是 uint8[3] memory。 因为每个常量的类型都是 uint8 ,如果你希望结果是 uint256[3] memory 类型,你需要将第一个元素转换为 uint256 。虽然所有元素都都可以转换为uint256,但是默认是转换为uint8,能转成小的类型,就不会转成大的,这是数组常量的懒惰性

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

contract LBC {
    function f() public pure returns (uint256[3] memory) {
        return g([uint256(1), 2, 3]);
    }

    function g(uint256[3] memory _arr)
        internal
        pure
        returns (uint256[3] memory)
    {
        return _arr;
    }
}

如下是一个比较经典的例子

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

contract T {
    int8[2] public a = [1, -1];
    // int8[2] public a = [int8(1), -1];
}

数组常量 [1, -1] 是无效的,因为第一个表达式类型是 uint8 而第二个类似是 int8 他们不可以隐式的相互转换。 为了确保可以运行,你是可以使用例如: [int8(1), -1]

由于不同类型的固定大小的内存数组不能相互转换(尽管基础类型可以),如果你想使用二维数组常量,你必须显式地指定一个基础类型:

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

contract Demo {
    function f() public pure returns (uint24[2][4] memory) {
        // 下面代码无法工作,因为没有匹配内部类型
        // uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];

        uint24[2][4] memory x = [
            [uint24(0x1), 1],
            [0xffffff, 2],
            [uint24(0xff), 3],
            [uint24(0xffff), 4]
        ];

        return x;
    }
}

⓹ 数组的属性

length

数组有 length 属性表示当前数组的长度。 一经创建,内存 memory 数组的大小就是固定的(但却是动态的,也就是说,它可以根据运行时的参数创建)。

例子 1: 通过 arr.length 获取数组的长度

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

contract Demo {
    uint256[10] public arr1 = [0, 1, 2];
    uint256[10] public arr2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    function test1() external view returns (uint256) {
        return arr1.length;
    }

    function test2() external view returns (uint256) {
        return arr2.length;
    }
}

例子 2: 可以通过 length 属性来判断长度。

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

contract TX {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);

        assert(a.length == 7);
        assert(b.length == len);
    }
}

例子 3:不能通过设置 arr.length 来调整动态数组的长度。

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

contract Demo {
    uint256[10] public arr1 = [0, 1, 2];
    // Member "length" is read-only and cannot be used to resize arrays.
    function test1() external {
        arr1.length = 8;
    }
}

⓺ 数组的方法

  • push : 只有动态数组可以使用,动态的 storage 数组以及 bytes 类型可以用,string 类型不可以
    • push(): 它用来添加新的零初始化元素到数组末尾,并返回元素引用.因此可以这样:x.push().t = 2x.push() = b.
    • push(x): 用来在数组末尾添加一个给定的元素,这个函数没有返回值.
  • pop: 删除最后一个长度
    • 它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用 delete 。
  • delete: 删除对应的索引;删除并不会改变长度,索引位置的值会改为默认值。
  • x[start:end]: 数组切片,仅可使用于 calldata 数组.
push

通过 push() 增加 storage 数组的长度具有固定的 gas 消耗,因为 storage 总是被零初始化;

例子: 状态变量的定长数组可以通过 push 来改变长度。但是内存中不可以使用 push。

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

contract Demo {
    uint256[] public a1 = new uint256[](5);

    function setStorageA() external {
        a1.push(8);
    }

    function setMemoryA() external pure {
        uint256[] memory a2 = new uint256[](5);

        // Type uint8[5] memory is not implicitly convertible to expected
        // type uint256[] memory. uint256[] memory a3 = [1, 2, 3, 4, 5];

        // Member "push" is not available in uint256[] memory outside of storage.
        // a2.push(8);
    }
}
pop & delete
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract FunctionOutputs {
    uint256[] private nums = [1, 2, 3];
    uint256[3] private numsFixed = [1, 2, 3];

    function setArray()
        external
        returns (
            uint256 len1,
            uint256 len2,
            uint256 len3
        )
    {
        nums.push(4); // push
        len1 = nums.length;

        nums.pop(); // 删除
        len2 = nums.length;

        nums[2] = 666;

        delete nums[1];
        // delete nums;
        len3 = nums.length;
    }

    function getArray() external view returns (uint256[] memory) {
        return nums;
    }
}
  • pop 删除最后一个元素
  • delete array[x] 仅仅是清除元素对应索引为默认值
  • delete array array 的 length 重置为 0,且删除了所有的元素.

通过 pop() 删除数组成本是很高的,因为它包括已删除的元素的清理,类似于在这些元素上调用 delete

注意:如果需要在外部(external)函数中使用多维数组,这需要启用 ABI coder v2。 public 函数中是支持的使用多维数组。因为多维数组用的场景不多,这里就不介绍了。

数组切片: x[start:end]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    uint256[] internal nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
    uint256[] temp1;
    uint256[] temp2;
    uint256[] temp3;

    // 输入 [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
    function setTemp(uint256[] calldata _arr)
        external
        returns (
            uint256[] memory,
            uint256[] memory,
            uint256[] memory
        )
    {
        temp1 = _arr[0:2];
        temp2 = _arr[:2];
        temp3 = _arr[2:];

        // Index range access is only supported for dynamic calldata arrays.
        // temp3 = nums[2:];

        return (temp1, temp2, temp3);
    }
}

数组切片是数组连续部分的视图,用法如:x[start:end]startenduint256 类型(或结果为 uint256 的表达式)。 x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end - 1] 。(包含 start,不包含 end)

  • 目前数组切片,仅可使用于 calldata 数组.
  • 如果 startend 大或者 end 比数组长度还大,将会抛出异常。
  • startend 都可以是可选的: start 默认是 0, 而 end 默认是数组长度。

数组切片没有任何成员。 它们可以隐式转换为其“背后”类型的数组,并支持索引访问。 索引访问也是相对于切片的开始位置。 数组切片没有类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。

⓻ 模拟切片的 slice 方法

切片当前仅支持 calldata 的数据,如果是 memory 就不支持了。

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

contract FunctionOutputs {
    function slice(
        uint256[] memory arr,
        uint256 begin,
        uint256 end
    ) internal pure returns (uint256[] memory) {
        require(begin < arr.length, "index out of bound");

        //如果起始位置越界,返回空数组
        if (begin >= arr.length) return arr;

        //处理 begin 和 end小于0的情况,使用 uint256 ,不存在负数
        // if (begin < 0) {
        //     begin = begin + arr.length < 0 ? 0 : begin + arr.length;
        // }
        // if (end < 0) {
        //     end = end + arr.length < 0 ? 0 : end + arr.length;
        // }

        //声明一个空数组,作为复制后返回值
        uint256[] memory temp = new uint256[](end - begin);

        //复制begin至end的元素到 temp 中 包括arr[begin] 不包括arr[end]
        for (uint256 index = begin; index < end; index++) {
            temp[index - begin] = arr[index];
        }
        return temp;
    }

    function test()
        external
        pure
        returns (
            uint256[] memory arr,
            uint256[] memory temp1,
            uint256[] memory temp2,
            uint256[] memory temp3
        )
    {
        arr = new uint256[](5);
        arr[0] = 1;
        arr[1] = 2;
        arr[2] = 3;
        arr[3] = 4;
        arr[4] = 5;

        temp1 = slice(arr, 1, 3); // [2,3]
        temp2 = slice(arr, 1, 4); // [2,3,4]
        temp3 = slice(arr, 1, 5); // [2,3,4,5]
    }
}

⓼ delete 完全删除数组的指定索引

删除数组的指定索引,数组的长度也会改变

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

contract FunctionOutputs {
    function deletePro(uint256[] memory arr, uint256 _index)
        internal
        pure
        returns (uint256[] memory temp)
    {
        require(_index < arr.length, "index out of bound");
        temp = new uint256[](arr.length - 1);
        for (uint256 index = 0; index <= temp.length - 1; index++) {
            if (index >= _index) {
                temp[_index] = arr[_index + 1];
            } else {
                temp[index] = arr[index];
            }
        }
    }

    function test()
        external
        pure
        returns (uint256[] memory arr, uint256[] memory temp)
    {
        arr = new uint256[](3);
        arr[0] = 1;
        arr[1] = 2;
        arr[2] = 3;
        assert(arr[0] == 1);
        assert(arr[1] == 2);
        assert(arr[2] == 3);
        assert(arr.length == 3);

        temp = deletePro(arr, 1);
        assert(temp[0] == 1);
        assert(temp[1] == 3);
        assert(temp.length == 2);
    }
}

2.bytes

stringbytes 类型的变量是特殊的数组。 bytes 可以通过索引或者.length来访问数据。string 与 bytes 相同,但不允许用.length或索引来访问数据。

  • 对任意长度的原始字节数据使用 bytes,对任意长度字符串(UTF-8)数据使用 string
  • 如果使用一个长度限制的字节数组,应该使用一个 bytes1bytes32 的具体类型,因为它们便宜得多。
  • bytesN[]bytes 可以转换: bytes1 是值类型,比如 0x61; bytes是可变字节数组,如果 bytes1 想要借用 bytes 的方法,就需要转换成 bytes;
  • 基本规则:对任意长度的原始字节数据使用 bytes,对任意长度字符串(UTF-8)数据使用 string

本节配套视频

⓵ 创建

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

contract Demo {
    bytes public welcome = bytes("1.Welcome");
    bytes public temp1 = new bytes(2); // 可变字节数组创建方式

    function test1(uint256 len_) public pure  returns(bytes memory){
        bytes memory temp2 = new bytes(len_);
        temp2[0] = "a";
        return temp2;
    }
    function test2() public{
        temp1[0] = "a";
    }
}

状态变量的创建方式

bytes public welcome = bytes("1.Welcome");

函数中可变字节数组创建方式:

bytes memory temp2 = new bytes(length); // 可变字节数组创建方式

⓶ bytes 和 bytes32[] 区别

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

// bytes / bytes32 / bytes32[] 区别
// bytes:可变字节数组 : 引用类型
// bytes32: 固定长度的字节数组 : 值类型
// bytes32[]: 由“固定长度的字节数组” 组成的 数组类型
contract Demo {
    bytes public welcome1 = bytes("1.Welcome");
    bytes32 public welcome2 = "a";
    bytes32[] public welcome3 = [bytes32("a")];
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    bytes32[] public abcArray = [bytes1("a"), bytes1("b"), bytes1("c")];

    // 0x616263
    bytes public abcBytes = bytes("abc");

    function getAbcArr() external view returns (bytes32[] memory) {
        return abcArray;
    }
}

abcBytes 的值是: 0x616263;

abcArray 的值是:

[
    0x6100000000000000000000000000000000000000000000000000000000000000,
    0x6200000000000000000000000000000000000000000000000000000000000000,
    0x6300000000000000000000000000000000000000000000000000000000000000
]

bytes 有点类似于 bytes1[]的紧打包,我们可以把上面例子中 bytes32 改为 bytes1 类型进行对比。

我们更多时候应该使用 bytes 而不是 bytes32[]这种数组类型 ,因为 Gas 费用更低;

  • bytes32[] 会在元素之间添加 31 个填充字节。
  • bytes 由于紧密包装,这没有填充字节。

⓷ 属性

  • 获取 bytes 长度
    • bytesVar.length:以字节长度表示字符串的长度
  • 获取指定索引的数据
    bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1
    
  • 修改 bytes
    • bytesVar[7] = 'x'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    bytes public welcome = bytes("1.Welcome");

    function getLength() public view returns (uint256 welcomeLength) {
        welcomeLength = welcome.length;
    }

    function modi() public {
        welcome[0] = bytes1("2");
    }
}

⓸ 方法

  • bytes 拼接
    • bytes.concat(...) returns (bytes memory):
    • 如果不使用参数调用 bytes.concat 将返回空数组。
  • push 方法
    • a.push(b) 往字节数组添加字节
  • delete bys;:清空字节数组
  • x[start:end]: 数组切片
  • bytes(): 将字符串转换到 bytes
  • string():将 bytes 数据转换到字符串
  • 比较两个 bytes
    • keccak256(bytes1) == keccak256(bytes2)
bytes.concat 拼接
  • bytes.concat(...) returns (bytes memory)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    bytes public welcome = bytes("a");
    bytes public concatBytes = bytes.concat(welcome, bytes("b"), bytes1("c"),"a");
}

bytes.concat 函数可以连接任意数量的 bytesbytes1 ... bytes32 值。 该函数返回一个 bytes memory ,包含所有参数的内容,无填充方式拼接在一起。 如果你想使用字符串参数或其他不能隐式转换为 bytes 的类型,你需要先将它们转换为 bytesbytes1/…/ bytes32

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

contract Demo {
    bytes public welcome = bytes("a");
    bytes public concatBytes = bytes.concat();
}

如果你不使用参数调用 bytes.concat 将返回空数组。

push 方法

注意: push 是单个字节,是 bytes1的固定长度,而不是 bytes

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

contract Demo {
    bytes public welcome1 = bytes("Welcome");
    bytes public welcome2 = new bytes(10);

    function testPush() public {
        welcome1.push(bytes("A")[0]);
        welcome2.push(bytes("B")[0]);
    }
}
pop 方法

删除数组的最后一个元素。

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

contract Demo {
    bytes public welcome1 = bytes("Welcome");
    bytes public welcome2 = new bytes(10);

    function testPop() public {
        welcome1.pop();
        welcome2.pop();
    }
}
delete 清空字节数组

使用 delete 全局关键字;

  • delete bytesName
  • delete bytesName[index]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    bytes public welcome1 = bytes("Welcome");

    function deleteAll() public {
        delete welcome1;
    }

    function deleteIndex(uint256 index_) public {
        delete welcome1[index_];
    }
}
x[start:end]:数组切片

注意:数组切片只能用在 calldata 类型上。

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

contract Proxy {
    bytes public welcome1 = bytes("Welcome");
    bytes4 public temp1 = bytes4(welcome1); // 0x57656c63

    // 把 welcome1 的值传入参数
    function forward(bytes calldata payload)
        external pure
        returns(bytes memory temp2,bytes4 temp3)
    {
        // 切片方法只能用在 calldata 上。
        temp2 = payload[:4];
        temp3 = bytes4(payload[:4]);
    }
}

另一个例子: bts_[:4]bytes4(bts_) 结果不一样!

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

// x[start:end]
//      1.只能用在 caldata 类型的数据上
//      2.切出来的是数组:`bts_[:4]` 和 `bytes4(bts_)` 结果不一样!
//          两者虽然看起来值意义,但是类型不一样!处理的时候也需要注意
contract Demo {
    // 0x57656c636f6d65
    bytes public welcome1 = bytes("Welcome");
    bytes4 public welcome2 = bytes4(welcome1);

    // bytes: temp1 0x57656c63
    // bytes4: temp2 0x57656c63
    function test(bytes calldata bts_) public pure returns(
        bytes memory temp1,
        bytes4 temp2,
        bytes4 temp3
    ){
        temp1 = bts_[:4]; // 切的返回值是数组
        temp2 = bytes4(bts_[:4]); //
        temp3 = bytes4(bts_); // 切: 由大到小 => 切出来的是值类型
    }

}

例子: 数组切片在 ABI 解码数据的时候非常有用,如:

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

contract Proxy {
    /// 被当前合约管理的 客户端合约地址
    address client;

    constructor(address client_) {
        client = client_;
    }

    /// 在进行参数验证之后,转发到由client实现的 "setOwner(address)"
    function forward(bytes calldata payload) external {
        bytes4 sig = bytes4(payload[:4]);

        // 由于截断行为,与执行 bytes4(payload) 是相同的
        // bytes4 sig = bytes4(payload);

        if (sig == bytes4(keccak256("setOwner(address)"))) {
            address owner = abi.decode(payload[4:], (address));
            require(owner != address(0), "Address of owner cannot be zero.");
        }
        (bool status, ) = client.delegatecall(payload);
        require(status, "Forwarded call failed.");
    }
}

⓹ 字符串 到 bytes 的转换

转换方法: 可以使用 bytes() 构造函数将字符串转换为 bytes

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

contract Test {
    function trans(string memory _str) external pure returns (bytes memory) {
        return bytes(_str);
    }
}

⓺ bytes 到 字符串 的转换

转换方法: 可以使用 string() 构造函数将 bytes 转换为字符串。

注意: 字节数组分为动态大小和固定大小的。如果是固定大小字节数组,需要先转为动态大小字节数组。

  • 动态大小字节数组 —> string
  • 固定大小字节数组 —> 动态大小字节数组 —> string
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Test {
    string public data1;
    string public data2;

    // `动态大小字节数组` —> `string`
    function trans1() external {
        bytes memory bstr = new bytes(2);
        bstr[0] = "a";
        bstr[1] = "b";
        data1 = string(bstr);
    }

    // `固定大小字节数组` —> `动态大小字节数组` —> `string`
    function trans2() external {
        // 固定大小字节数组
        bytes2 ab = 0x6162;

        // `固定大小字节数组` —> `动态大小字节数组`
        bytes memory temp = new bytes(ab.length); // 可变字节数组创建方式
        for (uint256 i = 0; i < ab.length; i++) {
            temp[i] = ab[i];
        }

        // `动态大小字节数组` —> `string`
        data2 = string(temp);
    }
}

⓻ 比较 2 个 bytes 值是否相等

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

contract Demo {
    bytes welcome1 = bytes("Welcome");
    bytes welcome2 = bytes("Welcome");

    function test1() public view returns (bool) {
        return keccak256(welcome2) == keccak256(welcome1);
    }
}

3.string

Solidity 中,字符串值使用双引号("")或单引号('')包括,字符串类型用 string 表示。stringbytes 类型的变量是特殊的数组,是引用类型。

本节配套视频

⓵ 格式

"abc"
'hello'

例子

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

contract For {
    string public a = "a" "b" "c";
    string public b = "abc";
    string public c = 'x' 'y' 'z';
    string public d = 'xyz';
}

⓶ 属性

string 并没有获取其字符串长度的 length 属性; 也没提供获取某个索引字节码的索引属性。

我们可以通过把 string 转换成 bytes,借助bytes 的属性。

例子: 下面是使用 getLength() 获取长度,使用modi()修改字符串,使用 getIndexValue() 获取字符串的指定索引的数据。

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

contract Demo {
    string public welcome = "1.Welcome";

    function getLength() public view returns (uint256 welcomeLength) {
        welcomeLength = bytes(welcome).length;
    }

    function getIndexValue(uint256 _index) public view returns (string memory) {
        bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1
        bytes memory temp2 = new bytes(1); // 可变字节数组创建方式
        temp2[0] = temp1;
        return string(temp2);
    }

    function modi() public {
        bytes(welcome)[0] = bytes1("2");
    }
}
  • 获取字符串的长度
    • bytes(str).length:以字节长度表示字符串的长度
  • 某个字符串索引的字节码
    • bytes1 temp1 = bytes(str)[_index];
      function getIndexValue(uint256 _index) public view return(string memory) {
          bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1
          bytes memory temp2 = new bytes(1); // 可变字节数组创建方式
          temp2[0] = temp1;
          return string(temp2);
      }
      
  • 修改字符串
    • bytes(s)[7] = 'x'

⓷ 方法

Solidity string 本身并没有操作函数,需要借助全局的函数

  • 字符串拼接

    • string.concat()
    • 如果不使用参数调用 string.concat 将返回空数组。
  • 将 bytes 转换到 字符串

    • string()
  • 将 字符串 转换到 bytes

    • bytes()
  • 比较两个字符串

    • keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
    • keccak256(bytes(s1)) == keccak256(bytes(s2)):更省 gas
  • 有没有如下办法呢?

    • push
    • pop
    • delete
    • x[start:end]

⓸ 字符串拼接

可以使用 string.concat 连接任意数量的 string 字符串。 该函数返回一个 string memory ,包含所有参数的内容,无填充方式拼接在一起。 如果你想使用不能隐式转换为 string 的其他类型作为参数,你需要先把它们转换为 string。

string.concat 例子

输入字符串,输出拼接后的字符串

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

contract Demo {
    string public welcome = "Welcome";

    // 写一个 welcome username 的小方法
    // Welcome Anbang!
    function test(string memory name_)
        public
        view
        returns(string memory concatString){
            bytes memory bs = bytes("!");
            // welcome + name_ + bs
            // 内部是使用字符串,如果是bytes,需要转换为 string 类型
            concatString = string.concat(
                welcome,
                name_,
                string(bs)
            );
        }
}

如果你不使用参数调用 string.concatbytes.concat 将返回空数组。

推荐了解:

// 这是一种 string.concat 方法的实现
function strConcat(string memory _a, string memory _b)
    internal
    pure
    returns (string memory)
{
    bytes memory _ba = bytes(_a);
    bytes memory _bb = bytes(_b);
    string memory ret = new string(_ba.length + _bb.length);
    bytes memory bret = bytes(ret);
    uint256 k = 0;
    for (uint256 i = 0; i < _ba.length; i++) bret[k++] = _ba[i];
    for (uint256 i = 0; i < _bb.length; i++) bret[k++] = _bb[i];
    return string(ret);
}

⓹ bytes 和 字符串 之间转换

见 bytes 章节中的内容,这里不再重复介绍。

⓺ 比较两个字符串是否相等

比较两个字符串借助 keccak256 来使用:

  • keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
  • keccak256(bytes(s1)) == keccak256(bytes(s2)) : 更推荐这个,省 gas

注意:上面 abi.encodePacked 的返回值是 bytes 类型。

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

contract Demo {
    string public hello = "Hello";

    // string 转成 bytes
    function test1() public view
        returns (bytes memory,bytes memory) {
        return (abi.encodePacked(hello),bytes(hello));
    }

    function test2(string calldata hello_) public view returns (bool) {
        // 这里只要能够转换成 bytes的都可以。
        // 更多方法可以参考后面介绍的 全局 ABI 编码函数
        return
            keccak256(abi.encodePacked(hello)) ==
            keccak256(abi.encodePacked(hello_));
    }

    function test3(string calldata hello_) public view returns (bool) {
        return keccak256(bytes(hello)) == keccak256(bytes(hello_));
    }

}

几个常用的全局 ABI 编码函数的简单用法介绍:

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

contract Demo {
    string public hello = "Hello Anbang";
    bytes public temp1 = abi.encodePacked(hello);
    bytes public temp2 = abi.encode(hello);

    bytes public temp3 = abi.encodeWithSignature(hello);
    bytes public temp4 = abi.encodeWithSignature("Hello Anbang1");
}

如果比较多个参数的拼接字符串是否相等,谨慎使用 abi.encodePacked了,因为紧压缩机制的问题。详细可以在 abi.encodePacked 中了解

4.mapping 映射

mapping 可以看作一个哈希表,会执行虚拟化初始化,使所有可能的值都是该类型的默认值。其实 mapping 并不是一个哈希表,没有 key 集合,也没有 value 集合,所以 mapping 没办法遍历/迭代。

数组中找某一个值,需要循环遍历,这是很消耗 Gas 的,而使用 mapping 就可以很好的解决这个问题。映射可以很方便的获取某个值。映射并没有做迭代的方法。

  • 映射声明
  • 映射的设置,获取,删除

本节配套视频

⓵ 本节重点

声明映射类型的语法:mapping(_KeyType => _ValueType)

  • _KeyType:可以是任何内置类型,或者 bytes 和 字符串。
    • 键是唯一的,其赋值方式为:map[a]=test; 意思是键为 a,值为 test;。
  • _ValueType: 可以是任何类型,用户自定义类型也可以。
  • mapping 支持嵌套。
  • 映射的数据位置(data location)只能是 storage,通常用于状态变量。
  • mapping 不能用于 public 函数的参数或返回结果
    • 映射只能是 storage 的数据位置,因此只允许作为状态变量 或 作为函数内的 storage 引用 或 作为库函数的参数。它们不能用合约公有函数的参数或返回值。
    • 这些限制同样适用于包含映射的数组和结构体。
  • 映射可以标记为 public,Solidity 自动为它创建 getter 函数。
    • _KeyType 将成为 getter 的必须参数,并且 getter 会返回 _ValueType
    • 如果 ValueType 是一个映射。这时在使用 getter 时将需要递归地传入每个 KeyType 参数,

问答题:为什么映射不能像哈希表一样遍历?

映射与哈希表不同的地方:在映射中,并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。正因为如此,映射是没有长度的,也没有 key 的集合value 的集合的概念。映射只能是存储的数据位置,因此只允许作为状态变量或作为函数内的存储引用 或 作为库函数的参数。

⓶ 创建格式

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

contract Mapping {
    // 普通
    mapping(address => uint256) public balances;

    // 嵌套
    mapping(address => mapping(address => bool)) public friends;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// 不能像 array 一样返回所有
contract Demo {
    mapping(address => uint256) public balances;

    function getAllBalance() public view
        returns(mapping(address => uint256) memory){
        return balances;
    }

}

⓷ 如何获取-设置-删除

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

contract Mapping {
    // 普通
    mapping(address => uint256) public balances;

    // 嵌套
    mapping(address => mapping(address => bool)) public friends;

    constructor() {
        balances[msg.sender] = 100;
    }

    function blanceGet() external view returns (uint256) {
        // 获取
        return balances[msg.sender];
    }

    function blanceSet(uint256 amount) external {
        // 设置
        balances[msg.sender] += amount;
    }

    function blanceDelete() external {
        // 删除
        delete balances[msg.sender];
    }

    function friendGet() external view returns (bool) {
        // 获取
        return friends[msg.sender][address(0)];
    }

    function friendSet() external {
        // 设置
        friends[msg.sender][address(0)] = true;
    }

    function friendDelete() external {
        // 删除
        delete friends[msg.sender][address(0)];
        // delete friends[msg.sender];
    }
}

⓸ 作为局部变量的使用

mapping 类型可以用做局部变量,但只能引用状态变量,而且存储位置为 storage。

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

// 作为局部变量的使用
contract Demo {
    // 普通 mapping
    mapping(address => uint256) public balances; // 普通mapping

    // storage: 改变内部 ref,会影响 balances 的值
    // 不能声明为 memory
    function updataBalance() public returns(uint256){
        // mapping(address=>uint256) memory ref = balances; // ❌
        mapping(address=>uint256) storage ref = balances;
        ref[msg.sender] += 3;
        return ref[msg.sender];
    }

}

⓹ 在 ERC20 token 中的用法

下面的例子是  ERC20 token  的简单版本. _allowances 是一个嵌套 mapping 的例子. _allowances 用来记录其他的账号,可以允许从其账号使用多少数量的币.

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

// mapping 在 ERC20 token 中的用法
contract MappingExample {
    // 余额
    mapping(address => uint256) private _balances;
    // 授权:
    // 授权人 - 代理人 - 授权金额
    mapping(address => mapping(address => uint256)) private _allowances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );

    // 获取:授权金额
    function allowance(address owner, address spender)
        public
        view
        returns (uint256)
    {
        return _allowances[owner][spender];
    }

    // 检查:授权金额大于等于需要操作的金额
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) public returns (bool) {
        require(
            _allowances[sender][msg.sender] >= amount,
            "ERC20: Allowance not high enough."
        );
        _allowances[sender][msg.sender] -= amount; // 设置额度
        _transfer(sender, recipient, amount);
        return true;
    }
    // 设置:
    function approve(address spender, uint256 amount) public returns (bool) {
        require(spender != address(0), "ERC20: approve to the zero address");

        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function _transfer(
        address sender,
        address recipient,
        uint256 amount
    ) internal {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        require(_balances[sender] >= amount, "ERC20: Not enough funds.");

        _balances[sender] -= amount;
        _balances[recipient] += amount;
        emit Transfer(sender, recipient, amount);
    }
}

⓺ 可迭代映射

遍历所有 Mapping 内的数据,(Mapping 配合 array )

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

contract Demo {
    mapping(address => uint256) public balances;
    // 用于检查:地址是否已经存在于 balancesKey
    mapping(address => bool) public balancesInserted;
    address[] public balancesKey; // 所有地址

    // 设置
    function set(address ads_,uint256 amount_) external{
        balances[ads_] = amount_;
        // 1.检查
        if(!balancesInserted[ads_]){
            // 2.修改检查条件
            balancesInserted[ads_] = true;
            // 3.正在的操作
            balancesKey.push(ads_);
        }
    }
    // 获取
    function get(uint256 index_) external view returns(uint256){
        require(index_<balancesKey.length,"index_ error");
        return balances[balancesKey[index_]];
    }
    // 获取所有
    function totalAddress() external view returns(uint256){
        return balancesKey.length;
    }

    // 获取第一个值
    function first() external view returns(uint256){
        return balances[balancesKey[0]];
    }
    // 最后一个值
    function latest() external view returns(uint256){
        return balances[balancesKey[balancesKey.length-1]];
    }
}

5.struct 结构体

本节配套视频

⓵ 创建语法

要定义结构体,使用 struct 关键字。struct 关键字定义了一个新的数据类型,包含多个成员。结构体是可以将多个变量进行编组的自定义类型

struct 语句的格式如下

struct StructName {
   type1 typeName1;
   type2 typeName2;
   type3 typeName3;
}

例子:

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

contract Demo {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }
    Book public book = Book("Solidity", "Anbang", 1);
}

⓶ 三种创建方法

基础方式:Test t = Test(1,2);

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

contract Structs {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }

    uint256 private bookId;
    Book[] public bookcase; // 书柜:数组类型

    function setA1Bookcase() external {
        // 第1种生成方法:顺序一定要和结构一致
        Book memory temp = Book(
            unicode"Solidity 高级程序设计",
            "Anbang",
            ++bookId
        );
        bookcase.push(temp);
    }

    // ✅ 最优方案,推荐: 先写入内存,然后push
    function setB1Bookcase() external {
        // 第 2 种生成
        Book memory temp = Book({
            title: unicode"Solidity 高级程序设计",
            author: "Anbang",
            book_id: ++bookId
        });
        bookcase.push(temp);
    }

    function setB2Bookcase() external {
        // 第 2 种生成: 直接 push,无变量
        bookcase.push(
            Book({
                title: unicode"Solidity 高级程序设计",
                author: "Anbang",
                book_id: ++bookId
            })
        );
    }

    function setC1Bookcase() external {
        // 第 3 种生成: 推荐
        Book memory temp;
        temp.title = unicode"Solidity 高级程序设计";
        temp.author = "Anbang";
        temp.book_id = ++bookId;
        bookcase.push(temp);
    }
}

总结:

// 第 1 种生成
Book memory solidity1 = Book(unicode"Solidity 高级程序设计", "Anbang", ++bookId);

// 第 2 种生成
Book memory solidity2 = Book({
    title: unicode"Solidity 高级程序设计",
    author: "Anbang",
    book_id: ++bookId,
});

// 第 3 种生成
Book memory temp;
temp.title = unicode"Solidity 高级程序设计";
temp.author = "Anbang";
temp.book_id = ++bookId;

⓷ 读取

函数内仅读取结构体,使用 memory 和 storage 区别:

  1. 函数内读取并返回,如果使用 memory 变量接收:
    1. 从状态变量拷贝到内存中,然后内存中的变量拷贝到返回值。两次拷贝,消耗 gas 多
    2. Book memory _book = book;
  2. 函数内读取并返回,如果使用 storage 变量接收:
    1. 直接从状态变量读取,状态变量拷贝到返回值。1 次拷贝,消耗 gas 小
  3. 总结: 读取时候推荐使用 storage
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// 读取
contract Demo {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }
    Book public book = Book("Solidity", "Anbang", 1);

    // memory  30029 gas
    // 函数内读取并返回:使用 memory 变量接收
    //  两次拷贝,所以消耗的 gas 多
    function get1() external view
        returns(
            string memory,
            string memory,
            uint256
        )
    {
         // 从状态变量拷贝到内存中
        Book memory _book = book;
        // 内存中的变量拷贝到返回值;2次拷贝
        return (_book.title,_book.author,_book.book_id);
    }

    // storage 29983 gas
    // 函数内读取并返回:使用 storage 变量接收
    function get2() external view
        returns(
            string memory,
            string memory,
            uint256
        )
    {
        // 从状态变量读取,没有拷贝的行为
        Book storage _book = book;

        // 状态变量拷贝到返回值。1次拷贝
        return (_book.title,_book.author,_book.book_id);
    }

}

⓸ 修改

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

// 修改
contract Demo {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }
    Book public book = Book("Solidity", "Anbang", 1);

    function modi() external {
        book.title = "Solidity 666";
    }
}

函数内读取时,标记 memory / storage,会产生完全不同的结果;

特别注意:如果结构体内包含 mapping 类型,则必须使用 storage,不可以使用 memeory.,否则报错 Type struct ContractName.StructName memory is only valid in storage because it contains a (nested) mapping.

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

contract Demo {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }
    Book public book = Book("Solidity", "Anbang", 1);

    // view
    function test1() external view {
        Book memory bookLocal = book;
        bookLocal.author = "Anbang666";
    }
    // 不能用view:因为写状态变量了
    function test2() external {
        Book storage bookLocal = book;
        bookLocal.author = "Anbang777";
    }
}

函数内获取并修改结构体:

  • 因为要修改状态变量,所以使用 storage
  • 函数内直接修改变量; 在修改一个属性时比较省 Gas 费用
  • 函数内先获取存储到 storage 再修改:修改多个属性的时候比较省 Gas 费用(当多个属性在同一槽位内时)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Structs {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }
    uint256 private bookId;
    Book public book1; // Book类型
    Book public book2; // Book类型

    mapping(address => Book) public students; // mapping 类型

    // 设置 book1
    function setBook1() external {
        Book memory temp;
        temp.title = unicode"Solidity 高级程序设计";
        temp.author = "Anbang";
        temp.book_id = ++bookId;
        book1 = temp;
    }

    // 设置 book2
    // ✅ 最优方案,推荐:直接修改
    function setBook2() external {
        book2.title = unicode"Solidity 高级程序设计";
        book2.author = "Anbang";
        book2.book_id = ++bookId;
    }

    // ✅ 最优方案,推荐:直接修改
    function set1Student() external {
        Book storage temp = students[msg.sender];
        temp.title = unicode"Solidity 高级程序设计";
        temp.author = "Anbang";
        temp.book_id = ++bookId;
    }

    function set2Student() external {
        Book memory temp;
        temp.title = unicode"Solidity 高级程序设计";
        temp.author = "Anbang";
        temp.book_id = ++bookId;
        students[msg.sender] = temp;
    }
}

⓹ 删除

删除结构体的变量,仅仅是重置数据,并不是完全的删除。

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

contract Demo {
    struct Book {
        string title;
        string author;
        uint256 book_id;
    }
    Book public book = Book("Solidity", "Anbang", 1);

    function del() external
    {
        delete book;
    }
}

8️⃣ 类型转换

Solidity 允许类型之间进行隐式转换和显式转换。

前文回顾: bytes1 对应 uint8,对应两位连续的十六进制数字 0xXX

本节配套视频

1.隐式转换

⓵ 发生场景

赋值, 函数参数传递以及应用运算符时,会发生隐式转换。

⓶ 转换的标准

  1. 值类型
  2. 源类型必须是目标类型的子集。

例如,uint8 可以转换为 uint16/uint24../uint256,因为uint8uint16这些类型的子集。但是 int8 不可以转换为 uint256,因为 int8 可以包含 uint256 中不允许的负值,比如 -1

⓷ 相交集合的类型,不能隐式转换。

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

contract Demo {
    int8 public a1 = 3;

    // Type int8 is not implicitly convertible to expected type uint16.
    // uint16 public a2 = a1;

    uint8 public b1 = 3;
    uint16 public b2 = b1;
}

⓸ 把整数字面量赋值给整型时,不能超出范围而发生截断,否则会报错。

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

contract Demo1 {
    uint8 public a = 12; // no error
    uint32 public b = 1234; // no error
    uint16 public c = 0x01;

    // Type int_const 123456 is not implicitly convertible
    // to expected type uint8. Literal is too large to fit in uint8.
    // uint8 d = 123456;
}

⓹ 函数参数传递

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

// 函数的传参
contract Demo {
    uint256 public a;

    function test1(uint256 u_) public {
        a = u_;
    }

    function test2() external {
        uint8 temp = 3;
        test1(temp); //
    }
}

数组:

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

// 函数的传参
contract Demo {
    uint256 public a;

    function test1(uint256[3] memory u_) public {
        a = u_[0];
    }

    // 禁止的:
    function test2() external {
        // function call. Invalid implicit conversion from uint8[3]
        // memory to uint256[3] memory requested.
        // test1([1,2,3]);
        test1([uint256(1),uint256(2),uint256(3)]);
    }
}

⓺ 运算符应用

则编译器将尝试将其中一个操作数隐式转换为另一个操作数的类型(赋值也是如此)。 这意味着操作始终以操作数之一的类型执行。

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

// 运算符
contract Demo {
    uint8 public  x = 1;
    uint16 public  y = 2;

    // uint8 + uint16 => uint16 + uint16 = uint16
    // uint16 => uint32
    uint32 public  z = x + y;
}

在上面的示例中,加法的操作数 x 和 y 没有相同的类型,uint8 可以被隐式转换为 uint16,相反却不可以。 因此在执行加法之前,将 uint8 转换为 uint16 的类型,结果类型是 uint16。因为它被赋值给 uint32 类型的变量,又进行了另一个类似逻辑的隐式转换.

2.显式转换

可以使用类型关键字,显式地将数据类型转换为另一种类型。

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

contract Demo {
    // uint8 => uint16
    uint8 public a1 = 3;
    uint16 public a2 = uint16(a1);

    int8 public b1 = 3;
    //Explicit type conversion not allowed from "int8" to "uint256".
    // uint256 b2 = uint256(b1);
}

⓵ int/uint 整型转换

整型加大数据位置是从左侧增加,减小数据位置也是从左侧移除;(整型是右对齐

  • 整型转换成更大的类型,从左侧添加填充位。
  • 整型转换成更小的类型,会丢失左侧数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    // 整型转换成更大的类型,从左侧添加填充位。
    // uint16 => uint32
    uint16 public a1 = 22136;       // 等于 0x5678
    uint32 public a2 = uint32(a1); // a2 = 22136

    // uint16 => uint8
    uint8 public a3 = uint8(a1); // b4 = 0x78
    uint8 public a4 = 0x78;

    // 整型转换成更小的类型,会丢失左侧数据。
    // uint32 => uint16
    uint32 public b1 = 0x12345678; // 0x12345678
    uint16 public b2 = uint16(b1); // 0x5678 | b2 = 22136
}

整数显式转换为更大的类型

uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now

整数显式转换成更小的类型

uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678

⓶ bytes 字节类型转换

字节加大数据位置是从右侧增加,减小数据位置也是从右侧移除;(字节是左对齐

  • 字节转换为更大的类型时,从右侧添加填充位。
  • 字节转换到更小的类型时,丢失右侧数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    // 字节转换为更大的类型时,从右侧添加填充位。
    // bytes2 =>bytes4
    bytes2 public a1 = 0x5678;
    bytes4 public a2 = bytes4(a1); // a2 = 0x56780000

    // 字节转换到更小的类型时,丢失右侧数据。
    // bytes4 => bytes2
    bytes4 public b1 = 0x12345678;
    bytes2 public b2 = bytes2(b1); // b2 = 0x1234
}

bytes 显式转换成更小的类型

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b 为 0x12

bytes 显式转换成更大的类型

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b 为 0x12340000

⓷ bytes 与 uint 转换

只有当字节类型和整数类型大小相同时,才可以进行转换。

因为整数和定长字节数组在截断(或填充)时行为是不同的,如果要在不同的大小的整数和定长字节数组之间进行转换,必须使用一个中间类型来明确进行所需截断和填充的规则

bytes2 a = 0x1234;
uint32 b = uint16(a);           // b 为 0x00001234
uint32 c = uint32(bytes4(a));   // c 为 0x12340000

uint8  d = uint8(uint16(a));    // d 为 0x34
uint8  e = uint8(bytes1(a));    // e 为 0x12

1.bytes 转换成 uint: 先转类型,再转大小

  • 推荐先把 bytes 显示转换成数字类型后,再转换成更大或更小的数字
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

// - `uint8`  等于两位连续的十六进制数字 `0xXX`
// - `bytes1` 等于两位连续的十六进制数字 `0xXX`
// - `bytes1` 等于 `uint8`
contract Demo1 {
    // bytes => uint
    bytes2 public a1 = 0x5678; // : 十进制数字 = 22136
    bytes4 public a2 = bytes4(a1); // a2 = 0x56780000 : 十进制数字 = 1450704896
    bytes1 public a3 = bytes1(a1); // a3 = 0x56 : 十进制数字 = 86

    // -- 增大
    // bytes 显示转换成数字后,显示转换更大的数字 (这里也可以隐式完成)
    uint32 public a4 = uint32(uint16(a1)); // ✅ a4 = 0x00005678 : 十进制 = 22136
    // bytes 显示转换成更大数字对应的的bytes,然后bytes显示转换成匹配的数字
    uint32 public a5 = uint32(bytes4(a1)); // ❌ a5 = 0x56780000 : 十进制 = 1450704896

    // -- 减小
    // bytes 显示转换成数字后,显示转换成更小的数字
    uint8 public a6 = uint8(uint16(a1)); // ✅ a6 = 0x78 : 十进制 = 120
    // bytes 显示转换成更小数字对应的的bytes,然后bytes显示转换成匹配的数字
    uint8 public a7 = uint8(bytes1(a1)); // ❌ a7 = 0x56 : 十进制 = 86
}

2.uint 转换成 bytes: 先转大小,再转类型

  • 推荐先把 uint 显示转换成更大 bytes 对应的 uint,然后 uint 再显示转换成匹配的 bytes
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo2 {
    // uint => bytes
    uint16 public b1 = 0x5678; // 0x5678 : 十进制 = 22136
    uint32 public b2 = uint32(b1); // b2 = 0x00005678 : 十进制 = 22136
    uint8 public b3 = uint8(b1); // b3 = 0x78 : 十进制 = 120
    // -- 增大
    // uint 显示转换成bytes类型后,再显示转换成更大或更小的bytes
    bytes4 public b4 = bytes4(bytes2(b1)); // ❌ b4 = 0x56780000
    // uint 显示转换成更大bytes对应的uint,然后uint再显示转换成匹配的bytes
    bytes4 public b5 = bytes4(uint32(b1)); //  ✅ b5 = 0x00005678

    // -- 减小
    // uint 显示转换成bytes类型后,再显示转换成更大或更小的bytes
    bytes1 public b6 = bytes1(bytes2(b1)); // ❌ b4 = 0x56
    // uint 显示转换成更大bytes对应的uint,然后uint再显示转换成匹配的bytes
    bytes1 public b7 = bytes1(uint8(b1)); // ✅ b4 = 0x78
}

⓸ bytes 和 bytesN 之间转换

bytes 数组和 bytes calldata 切片可以显示转换为固定长度的 bytes 类型(bytes1...bytes32).

  • 如果数组比固定长度的 bytes 类型长,则在末尾处会发生截断。
  • 如果数组比目标类型短,它将在末尾用零填充。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract C {
    // 0x6162636465666768
    bytes public bts = "abcdefgh";
    bytes3 public b1 = bytes3(bts);
    bytes8 public b2 = bytes8(bts);
    bytes16 public b3 = bytes16(bts);
    bytes32 public b4 = bytes32(bts);
}

补充:使用切片也可以把数据从 bytes 转为 bytesN。

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

contract C {
    // 0x6162636465666768
    bytes public bts = "abcdefgh";

    function f(bytes calldata bts_)
        public
        pure
        returns (bytes3,bytes16)
    {

        bytes3 b1 = bytes3(bts_);
        bytes16 b2 = bytes16(bts_[:8]);
        return (b1, b2);
    }
}

⓹ bytes 与 address 转换

address 的格式是 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac,是一个 bytes20 的数据。

地址是取 bytes32 数据中的后 20 位。如果想删除前面的 12 位数据,可以使用 solidity assembly (内联汇编) 来截取,也可以借助 uint 转换成更小的类型,会丢失左侧数据的特性来完成。

代码如下:

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

contract Demo {
    // 获取即将部署的地址
    function getAddress(bytes memory bytecode, uint256 _salt)
        external
        view
        returns (address)
    {
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), // 固定字符串
                address(this), // 当前工厂合约地址
                _salt, // salt
                keccak256(bytecode) //部署合约的 bytecode
            )
        );
        // bytes 转换成 uint: 先转类型,再转大小
        //      bytes32 => uint256 => uint160
        // uint160 转 address
        //      uint160 => address
        return address(uint160(uint256(hash)));
    }
}

前文介绍过编码的方式: keccak256(abi.encodePacked()),返回的是 bytes32 类型。

这个小例子是合约部署合约那章节中 create2 代码的一部分,相关的更多演示请查看 create2 创建。

3.数字转换成字符串

本节配套视频

⓵ 直接借助 bytes 和 string(未完成)

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

contract Demo {
    // `固定大小字节数组` —> `动态大小字节数组` —> `string`
    function test(uint8 num_) public pure returns (bytes1 ab,string memory data) {
        // 固定大小字节数组
        ab = bytes1(num_);

        // `固定大小字节数组` —> `动态大小字节数组`
        bytes memory temp = new bytes(ab.length); // 可变字节数组创建方式
        for (uint8 i = 0; i < ab.length; i++) {
            temp[i] = ab[i];
        }

        // `动态大小字节数组` —> `string`
        data = string(temp);
    }
}

⓶ 借助单个数字转换(推荐)

这种方法是借助将 0-9 的数字进行转换,然后超过十位的数字,通过 % 来得到,并且拼接在一起。 推荐方法:

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

contract Demo {
    function uintToString(uint256 _uint)
        public
        pure
        returns (string memory str)
    {
        if (_uint == 0) return "0";
        while (_uint != 0) {
            //取模
            uint256 remainder = _uint % 10;
            //每取一位就移动一位,个位、十位、百位、千位……
            _uint = _uint / 10;
            //将字符拼接,注意字符位置
            str =  string.concat(toStr(remainder), str);
        }
    }

    function toStr(uint256 num_) internal pure returns (string memory) {
        require(num_ < 10,"error");
        bytes memory alphabet = "0123456789";
        bytes memory str = new bytes(1);
        str[0] = alphabet[num_];
        return string(str);
    }
}

上面代码的 toStr 千万不要写下面的这种垃圾代码,写下面这种垃圾是对自己职业的不尊重:

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

contract Demo {
    function toStr(uint8 step) external pure returns (string memory str) {
        string memory str;
        if (step == 0) {
            str = "0";
        } else if (step == 1) {
            str = "1";
        } else if (step == 2) {
            str = "2";
        } else if (step == 3) {
            str = "3";
        } else if (step == 4) {
            str = "4";
        } else {
            str = "?";
        }
    }
}

toStr 的另外一种实现,推荐了解一下。

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

contract Demo {
    // 返回的数据类型建议使用bytes, string 有可能没有对应的ascii码
    function toStr2(uint256 value) public pure returns (bytes memory out) {
        bytes32 data = bytes32(value);
        // 记录向前截取的长度
        uint256 trunLen = 32;
        for(uint256 i = 0; i< data.length;){
            if(data[i] != 0x00){
                // 当数值不在为0值时 break
                trunLen = i;
                break ;
            }
            unchecked{
                i++;
            }
        }
        out = new bytes(32-trunLen);
        assembly{
            mstore(add(out,0x20), shl(mul(trunLen,8), data))
        }
    }
}

9️⃣ 字面常量与基本类型的转换

本节配套视频

1.十进制和十六进制字面常量

十进制和十六进制字面常量可以隐式转换为任何足以表示它而不会截断的整数类型:

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

contract Demo {
    uint8 public a = 12; //  可行
    uint32 public b = 1234; // 可行
    uint16 public c = 0x01;
    // uint16 d = 0x123456; // 失败, 会截断为 0x3456
}

⚠️:在 0.8.0 之前,任何十进制和十六进制常量都可以显示转化为整型,不过从 0.8.0 开始,只有在匹配数据范围时,才能进行这个转换,就像隐式转换那样。

2.整型字面常量与 bytesN

  • 十进制字面常量不能隐式转换为定长字节数组。
  • 十六进制字面常量可以转换为定长字节数组,但仅当十六进制数字大小完全符合定长字节数组长度的时候。
  • 零的十进制十六进制字面常量都可以转换为任何定长字节数组类型,零值是例外,比较特殊
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Demo {
    // 十六进制
    bytes2 public a = 0x1234; // 可行
    bytes2 public b = 0x0012; // 可行
    // bytes2 public c = 0x12; // 0x12不可行 ,0x1200 可行,需要完全符合长度

    // 十进制
    // bytes4 public x = 1; // 不可行
    // bytes2 public y = 2; // 不可行

    // 0 和 0x0
    bytes4 public d = 0x0; // 可行
    bytes4 public e = 0; // 可行
}

3.字符串字面常量与 bytesN

字符串字面常量和十六进制字符串字面常量可以隐式转换为定长字节数组(需要它们的字符数与字节类型的大小相匹配)

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

contract Demo {
    bytes2 public a = hex"1234"; // 可行
    bytes2 public b = hex"12"; // 可行
    bytes2 public c = "xy"; // 可行
    bytes2 public d = "x"; // 可行
    // bytes2 public e = hex"123"; // 不可行
    // bytes2 public f = "xyz"; // 不可行
}

4.十六进制字面常量与地址类型

通过校验和测试的正确大小的十六进制字面常量会作为 address类型。没有其他字面常量可以隐式转换为 address 类型。

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

contract Demo {
    address public ads1 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac;
    // address public ads2 = 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ab; // ❌
}

🆗 实战 1: Todo List

TodoList: 是类似便签一样功能的东西,记录我们需要做的事情,以及完成状态。

本节配套视频

1.需要完成的功能

  • 创建任务
  • 修改任务名称
    • 任务名写错的时候
  • 修改完成状态:
    • 手动指定完成或者未完成
    • 自动切换
      • 如果未完成状态下,改为完成
      • 如果完成状态,改为未完成
  • 获取任务

2.思考代码内状态变量怎么安排?

思考 1:思考任务 ID 的来源?

我们在传统业务里,这里的任务都会有一个任务 ID,在区块链里怎么实现??

答:传统业务里,ID 可以是数据库自动生成的,也可以用算法来计算出来的,比如使用雪花算法计算出 ID 等。在区块链里我们使用数组的 index 索引作为任务的 ID,也可以使用自增的整型数据来表示。

思考 2: 我们使用什么数据类型比较好?

答:因为需要任务 ID,如果使用数组 index 作为任务 ID。则数据的元素内需要记录任务名称,任务完成状态,所以元素使用 struct 比较好。

如果使用自增的整型作为任务 ID,则整型 ID 对应任务,使用 mapping 类型比较符合。

3.演示代码

contract Demo {
    struct Todo {
        string name;
        bool isCompleted;
    }
    Todo[] public list; // 29414

    // 创建任务
    function create(string memory name_) external {
        list.push(
            Todo({
                name:name_, // ,
                isCompleted:false
            })
        );
    }

    // 修改任务名称
    function modiName1(uint256 index_,string memory name_) external {
        // 方法1: 直接修改,修改一个属性时候比较省 gas
        list[index_].name = name_;
    }

    function modiName2(uint256 index_,string memory name_) external {
        // 方法2: 先获取储存到 storage,在修改,在修改多个属性的时候比较省 gas
        Todo storage temp = list[index_];
        temp.name = name_;
    }

    // 修改完成状态1:手动指定完成或者未完成
    function modiStatus1(uint256 index_,bool status_) external {
        list[index_].isCompleted = status_;
    }

    // 修改完成状态2:自动切换 toggle
    function modiStatus2(uint256 index_) external {
        list[index_].isCompleted = !list[index_].isCompleted;
    }

    // 获取任务1: memory : 2次拷贝
    // 29448 gas
    function get1(uint256 index_) external view
        returns(string memory name_,bool status_){
        Todo memory temp = list[index_];
        return (temp.name,temp.isCompleted);
    }

    // 获取任务2: storage : 1次拷贝
    // 预期:get2 的 gas 费用比较低(相对 get1)
    // 29388 gas
    function get2(uint256 index_) external view
        returns(string memory name_,bool status_){
        Todo storage temp = list[index_];
        return (temp.name,temp.isCompleted);
    }

}

4. 自己动手

自动动手写一下,按照使用自增整型作为任务 ID,配合 mapping 实现上面逻辑。状态按照【未完成,进行中,已完成,已取消】四种状态来做。

🆗 实战 2: 众筹合约

众筹合约是一个募集资金的合约,在区块链上,我们是募集以太币,类似互联网业务的水滴筹。区块链早起的 ICO 就是类似业务。

本节配套视频

1.需求分析

众筹合约分为两种角色:一个是受益人,一个是资助者。

// 两种角色:
//      受益人   beneficiary => address         => address 类型
//      资助者   funders     => address:amount  => mapping 类型 或者 struct 类型

状态变量按照众筹的业务:

// 状态变量
//      筹资目标数量    fundingGoal
//      当前募集数量    fundingAmount
//      资助者列表      funders
//      资助者人数      fundersKey

需要部署时候传入的数据:

//      受益人
//      筹资目标数量

2.演示代码

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

contract CrowdFunding {
    address public immutable beneficiary;   // 受益人
    uint256 public immutable fundingGoal;   // 筹资目标数量

    uint256 public fundingAmount;       // 当前的 金额
    mapping(address=>uint256) public funders;

    // 可迭代的映射
    mapping(address=>bool) private fundersInserted;
    address[] public fundersKey; // length

    // 不用自销毁方法,使用变量来控制
    bool public AVAILABLED = true; // 状态

    // 部署的时候,写入受益人+筹资目标数量
    constructor(address beneficiary_,uint256 goal_){
        beneficiary = beneficiary_;
        fundingGoal = goal_;
    }

    // 资助
    //      可用的时候才可以捐
    //      合约关闭之后,就不能在操作了
    function contribute() external payable{
        require(AVAILABLED,"CrowdFunding is closed");
        funders[msg.sender] += msg.value;
        fundingAmount += msg.value;
        // 1.检查
        if(!fundersInserted[msg.sender]){
            // 2.修改
            fundersInserted[msg.sender] = true;
            // 3.操作
            fundersKey.push(msg.sender);
        }
    }

    // 关闭
    function close() external returns(bool){
        // 1.检查
        if(fundingAmount<fundingGoal){
            return false;
        }
        uint256 amount = fundingAmount;

        // 2.修改
        fundingAmount = 0;
        AVAILABLED = false;

        // 3. 操作
        payable(beneficiary).transfer(amount);
        return true;
    }

    function fundersLenght() public view returns(uint256){
        return fundersKey.length;
    }

}

上面的合约只是一个简化版的 众筹合约,但它已经足以让我们理解本节介绍的类型概念。

3.自己动手

上面写的是项目方角度的募集自己。如果是募集平台,肯定是会向 Todo List 那个练习中一样,是有众筹 ID 的;请按照众筹平台的角度来写一个众筹协议。

  • 使用平台角度写合约
  • 使用 stuct 格式。
  • 【选做】:增加众筹时间的限制
    • 如果规定时间内完成,则结束后转钱给受益人
    • 如果规定时间内没有完成,则资金释放,捐赠者自己取回捐赠资金。

🆗 实战 3: 同志们好增加提示

本节配套视频

1.需求分析

需要点击一个方法,查看当前的 step 到哪里了?然后提示下一步该干什么。

比如:当前的 step 是:0 可以执行 hello ,领导说:同志们好

难点:数字怎么转换成 string

2.代码如下

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

contract Demo {
    uint8 public step = 0;

    /// @notice 用于辅助获取下一步该做什么的方法
    /// @dev 整理step对应的错误,注意数字转为字符串时候的途径
    /// @return 当前的提示信息
    function helperInfo() external view returns (string memory) {
        string memory stepDes = unicode"当前的step是:";
        string memory info;

        if (step == 0) {
            info = unicode"可以执行 hello ,领导说:同志们好";
        } else if (step == 1) {
            info = unicode"可以执行 helloRes ,同志们说:领导好";
        } else if (step == 2) {
            info = unicode"可以执行 comfort ,领导必须给钱,并且说:同志们辛苦了";
        } else if (step == 3) {
            info = unicode"可以执行 comfortRes ,同志们说:为人民服务";
        } else if (step == 4) {
            info = unicode"可以执行 selfdestruct";
        } else {
            info = unicode"未知";
        }

        return string.concat(stepDes, uintToString(step), " ", info);
    }

    // 另外一种转换方法
    //调用这个函数,通过取模的方式,一位一位转换
    function uintToString(uint256 _uint)
        internal
        pure
        returns (string memory str)
    {
        if (_uint == 0) return "0";
        while (_uint != 0) {
            //取模
            uint256 remainder = _uint % 10;
            //每取一位就移动一位,个位、十位、百位、千位……
            _uint = _uint / 10;
            //将字符拼接,注意字符位置
            str =  string.concat(toStr(remainder), str);
        }
    }

    function toStr(uint256 num_) internal pure returns (string memory) {
        require(num_ < 10,"error");
        bytes memory alphabet = "0123456789";
        bytes memory str = new bytes(1);
        str[0] = alphabet[num_];
        return string(str);
    }
}

🆗 实战 4: ETH 钱包

本节配套视频

内容

这一个实战主要是加深大家对 3 个取钱方法的使用。

  • 任何人都可以发送金额到合约
  • 只有 owner 可以取款
  • 3 种取钱方式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract EtherWallet {
    address payable public immutable owner;
    event Log(string funName, address from, uint256 value, bytes data);

    constructor() {
        owner = payable(msg.sender);
    }

    receive() external payable {
        emit Log("receive", msg.sender, msg.value, "");
    }

    function withdraw1() external {
        require(msg.sender == owner, "Not owner");
        // owner.transfer 相比 msg.sender 更消耗Gas
        // owner.transfer(address(this).balance);
        payable(msg.sender).transfer(100);
    }

    function withdraw2() external {
        require(msg.sender == owner, "Not owner");
        bool success = payable(msg.sender).send(200);
        require(success, "Send Failed");
    }

    function withdraw3() external {
        require(msg.sender == owner, "Not owner");
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success, "Call Failed");
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

🆗 实战 5: 多签钱包

多签钱包的功能: 合约有多个 owner,一笔交易发出后,需要多个 owner 确认,确认数达到最低要求数之后,才可以真正的执行。

本节配套视频

1.原理

  • 部署时候传入地址参数和需要的签名数
    • 多个 owner 地址
    • 发起交易的最低签名数
  • 有接受 ETH 主币的方法,
  • 除了存款外,其他所有方法都需要 owner 地址才可以触发
  • 发送前需要检测是否获得了足够的签名数
  • 使用发出的交易数量值作为签名的凭据 ID(类似上么)
  • 每次修改状态变量都需要抛出事件
  • 允许批准的交易,在没有真正执行前取消。
  • 足够数量的 approve 后,才允许真正执行。

2.代码

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

contract MultiSigWallet {
    // 状态变量
    address[] public owners;
    mapping(address => bool) public isOwner;
    uint256 public required;

    struct Transaction {
        address to;
        uint256 value;
        bytes data;
        bool exected;
    }
    Transaction[] public transactions;
    mapping(uint256 => mapping(address => bool)) public approved;

    // 事件
    event Deposit(address indexed sender, uint256 amount);
    event Submit(uint256 indexed txId);
    event Approve(address indexed owner, uint256 indexed txId);
    event Revoke(address indexed owner, uint256 indexed txId);
    event Execute(uint256 indexed txId);

    // receive
    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    // 函数修改器
    modifier onlyOwner() {
        require(isOwner[msg.sender], "not owner");
        _;
    }
    modifier txExists(uint256 _txId) {
        require(_txId < transactions.length, "tx doesn't exist");
        _;
    }
    modifier notApproved(uint256 _txId) {
        require(!approved[_txId][msg.sender], "tx already approved");
        _;
    }
    modifier notExecuted(uint256 _txId) {
        require(!transactions[_txId].exected, "tx is exected");
        _;
    }

    // 构造函数
    constructor(address[] memory _owners, uint256 _required) {
        require(_owners.length > 0, "owner required");
        require(
            _required > 0 && _required <= _owners.length,
            "invalid required number of owners"
        );

        for (uint256 index = 0; index < _owners.length; index++) {
            address owner = _owners[index];
            require(owner != address(0), "invalid owner");
            require(!isOwner[owner], "owner is not unique"); // 如果重复会抛出错误
            isOwner[owner] = true;
            owners.push(owner);
        }
        required = _required;
    }

    // 函数
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    function submit(
        address _to,
        uint256 _value,
        bytes calldata _data
    ) external onlyOwner returns(uint256){
        transactions.push(
            Transaction({to: _to, value: _value, data: _data, exected: false})
        );
        emit Submit(transactions.length - 1);
        return transactions.length - 1;
    }

    function approv(uint256 _txId)
        external
        onlyOwner
        txExists(_txId)
        notApproved(_txId)
        notExecuted(_txId)
    {
        approved[_txId][msg.sender] = true;
        emit Approve(msg.sender, _txId);
    }

    function execute(uint256 _txId)
        external
        onlyOwner
        txExists(_txId)
        notExecuted(_txId)
    {
        require(getApprovalCount(_txId) >= required, "approvals < required");

        Transaction storage transaction = transactions[_txId];
        transaction.exected = true;

        (bool sucess, ) = transaction.to.call{value: transaction.value}(
            transaction.data
        );
        require(sucess, "tx failed");
        emit Execute(_txId);
    }

    function getApprovalCount(uint256 _txId)
        public
        view
        returns (uint256 count)
    {
        for (uint256 index = 0; index < owners.length; index++) {
            if (approved[_txId][owners[index]]) {
                count += 1;
            }
        }
    }

    function revoke(uint256 _txId)
        external
        onlyOwner
        txExists(_txId)
        notExecuted(_txId)
    {
        require(approved[_txId][msg.sender], "tx not approved");
        approved[_txId][msg.sender] = false;
        emit Revoke(msg.sender, _txId);
    }
}

3.测试流程

  1. 部署合约
    1. 传入所有 owner 地址
    2. 传入需要的批准数: 例子是 3
  2. 调用 required 查询需要的数量
  3. 获取 getBalance 查询当前地址的 ETH 主币余额
  4. 使用 isOwner 查询多个地址的权限,看是否符合预期
  5. 使用 owners 查询不同的索引是否符合预期
  6. 使用 transactions 查询初始是否为没有值
  7. 使用其他地址向合约内转入 ETH 主币
  8. 使用 getBalance 查询当前地址的 ETH 余额是否符合预期
  9. 调用 submit,申请转出一笔钱到指定地址
    1. _to 地址
    2. _value 需要的金额
    3. _data: 如果没有需要的执行的代码,传入默认的 0x即可
    4. 返回值为本次交易的 txId
    5. 记录 _to 的当前余额
    6. 记录 合约的当前余额,使用 getBalance 获取
  10. 使用 transactions 查询 txId 是否为预期
  11. 使用 getApprovalCount 查询批准数量是否为 0
  12. 使用 getBalance 查询当前地址的 ETH ,确认钱没有被转出
  13. 使用 approved 查询当前 txId 是否被 owner1 批准
  14. 使用 owner1 调用 approvtxId 进行批准
  15. 使用 getApprovalCount 查询批准数量是否为 1
  16. 使用 owner1 调用 approvtxId 进行再次批准
    1. 查看是否报错 tx already approved
  17. 使用 approved 查询当前 txId 是否被某个 owner1 批准
  18. 使用 getBalancetransactions 查询 txId 是否为预期
  19. 使用 owner2 owner3 地址调用 approvtxId 进行批准
    1. 使用 approved 查询当前 txId 是否被 owner1 批准
  20. getApprovalCount 查询批准数量是否为 3
  21. 使用 owner1 调用 revoketxId 进行批准撤销
  22. getApprovalCount 查询批准数量是否为 2
  23. 使用 owner1 调用 execute 进行正式执行
    1. 查看是否报错 "approvals < required".
  24. 使用 owner1 调用 revoketxId 进行再次批准
    1. 使用 approved 查询当前 txId 是否被 owner1 批准
  25. getApprovalCount 查询批准数量是否为 3
  26. 使用 owner1 再次调用 execute 进行正式执行
    1. 查询当前交易是否正确执行
  27. 确认执行的结果是否符合预期
    1. 使用 transactions 查询 txIdexected 是否为 true
    2. _to 地址是否收到金额,
    3. 使用 getBalance 查询当前地址的 ETH 余额是否正确

#️⃣ 问答题

  • 为什么 uint8/int8uint256/uint256 都是以 8 的倍数递增,且最大值是 256。
    • 1 字节是 8 位,所以后面 8,16,都需要是 8 的整数倍,int8 是 8 位。EVM 为地址设置的最大长度是 256 位,所以最大值是uint256/uint256
  • 为什么 uint256 的最大值是 2**256 -1,而不是 2**256 呢?
    • 1 字节是 8 位,int8 是 8 位,二进制表示为0000 00001000 0000,第一位是符号位;第一位为 0 是正值,第一位为 1 是负值;因为 int8 总共能够表示 2 的 8 次方,所以带符号的正值为 128 个数,负值为 128 个数;
    • 计算机里是将 0 算在正值内,负值的范围还是-128;但是 0 不是正数也不是负数,所以正值范围少了一个位置,就剩 127 个位置了。
  • 计算机中 字节 & bit & 十六进制数字的关系
    • bytes1 是指 1 个字节,1 个字节可以表示成 2 个连续的 16 进制数字。最大值是 0xff
    • bytes1 是指 1 个字节,1 个字节可以表示成 8 个连续的 bit 数字。最大值是 11111111
      • bytes1 等于两位连续的十六进制数字 0xXX
    • 8 个 bit 最大值是 11111111,8 个 bit 对应 2 个连续的十六进制数字,最大是 0xff;
      • uint8 等于两位连续的十六进制数字 0xXX
  • Solidity 的值类型和引用类型分别有哪些?
    • 值类型
      • boolean
      • integer 整型
      • integer 整型字面量123_456_789/uint8 public a = (2**800 + 1) - 2**800;
      • Fixed 定长浮点型,可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。
      • BytesN 定长字节数组
      • 字符串字面量,比如 bytes1 public a8 = "a";,也包括 unicode 字面常量。
      • 十六进制字面常量
        contract Test {
            string public a1 = "a";
            bytes1 public a2 = "a";
            bytes1 public a3 = 0x61;
            bytes1 public a4 = hex"61";
        }
        
      • Enum:枚举
      • 用户定义的值类型;用户定义值类型使用 type UserType is DefaultType 来定义,其中 UserType 是新引入的类型的名称, DefaultType 必须是内置的值类型(”底层类型”)。
      • 地址类型/合类型约
      • 函数类型
    • 引用类型
      • array
      • bytes(bytes 和 bytes32[] 区别)
      • string
      • mapping
      • struct

int/uint

  • 如何获取整型的最大值和最小值

    • 可以使用 type(int8).max 获取该类型的最大值
    • 可以使用 type(int8).min 获取该类型的最小值
  • 聊一聊 checkedunchecked

    • 0.8.0 开始,算术运算有两种计算模式:一种是checked(检查)模式,另一种是 unchecked(不检查)模式。 默认情况下,算术运算在 checked 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。 你也可以通过 unchecked{ ... } 切换到 “unchecked”模式,更多可参考 unchecked .
    • 现在不需要因为 safeMath 库了。
  • 下面代码会报错么?为什么?

    uint8 public a = (2**800 + 1) - 2**800; // 1
    uint8 public b = 0.5 * 8; // 4
    uint8 public c = 2.5 + b + 0.5;
    
    • (2**800 + 1) - 2**800 的结果是字面常量 1 (属于 uint8 类型),尽管计算的中间结果已经超过了 以太坊虚拟机的机器字长度(这是因为编译器编译阶段就已经计算过了并得到了其结果,实际以太坊虚拟机并未对其进行任何计算操作.)。 此外, .5 * 8 的结果是整型 4 (尽管有非整型参与了计算)。
    • 尽管我们知道 b 的值是一个整数,但 2.5 + a 这部分表达式并不进行类型检查,因此编译不能通过。
  • 下面代码中的c1/c2结果是什么?

    uint256 a = 1;
    uint256 b = 4;
    uint256 c1 = (1 / 4) * 4; // 1 => 未截断
    uint256 c2 = (a / b) * b; // 0 => 截断
    
    • 整数的除法会被截断(例如:1/4 结果为 0),但是使用字面量的方式不会被截断
  • 下面两个函数都会运行成功么?

    function test1() public pure returns (int256 a) {
        a = type(int256).min / (-2);
    }
    
    // VM error: revert.
    function test2() public pure returns (int256 a) {
        a = type(int256).min / (-1);
    }
    
    • 表达式 type(int).min / (-1) 是仅有的整除会发生向上溢出的情况。 在算术检查模式下,这会触发一个失败异常,在截断模式下,表达式的值将是 type(int).min
  • 下面例子输出什么?

    • 你可能认为像 255 + (true ? 1 : 0)255 + [1, 2, 3][0] 这样的表达式等同于直接使用 256 字面常量。 但事实上,它们是在 uint8 类型中计算的,会溢出。
    // VM error: revert.
    function testA1() public pure returns (uint256 a) {
        a = 255 + (true ? 1 : 0);
    }
    
    function testA2() public pure returns (uint256 a) {
        a = (true ? 1 : 0) + 255;
    }
    
    // VM error: revert.
    function testB1() public pure returns (uint256 a) {
        a = 255 + [1, 2, 3][0];
    }
    
    function testB2() public pure returns (uint256 a) {
        a = [1, 2, 3][0] + 255;
    }
    
    function testA3() public pure returns (uint256 a) {
        a = 255 + uint256(true ? 1 : 0);
    }
    
    function testB3() public pure returns (uint256 a) {
        a = 255 + uint256([1, 2, 3][0]);
    }
    

BytesN 定长字节数组

  • bytesN 有哪些属性,分别怎么使用。
    • 定义方式 bytesN,其中 N 可取 1~32 中的任意整数;bytes1 代表只能存储一个字节。一旦声明,其内部的字节长度不可修改,内部字节不可修改。注意这里 bytes32bytes 是不同的。bytes 是变长字节数组,是引用类型。bytebytes1 的别名,不推荐使用。
    • length (只读)
      • 返回字节个数,可以通过索引读取对应索引的字节。
    • 索引访问: bytesN[index]
      • index 取值范围[0, N],其中 N 表示长度。
      • 如果 xbytesI 类型,那么 x[k] (其中 0 <= k < I)返回第 k 个字节(只读)。
  • bytesN 有什么方法?
    • 自己没有方法,可以全局的,比如 delete。
  • "a" 是值类型还是引用类型?
    • 注:字符串字面常量是值类型,这不是字符串类型。比如bytes1 public b1 = "a";

Unicode

  • 怎么样输出中文字符串?(unicode"同志们好"

十六进制字面常量

  • bytes1 public a4 = hex"61"; 的值是什么?(0x61

Enum:枚举

  • 枚举类型的使用场景
    • enum 是一种用户自定义类型,用于表示多种状态,枚举可用来创建由一定数量的“常量值”构成的自定义类型。主要作用是用于限制某个事务的有限选择。比如将咖啡的容量大小限制为:大、中、小,这将确保任何人不能购买其他容量的咖啡,只能在这里选择。
  • Enum 的属性和方法
    • 选项从 0 开始的无符号整数值表示。
    • type(NameOfEnum).min/max
    • delete 恢复默认
  • 聊一聊枚举类型
    • 枚举类型,返回值是索引,默认值是 0;
    • 枚举类型的默认值是第一个值。
      • 枚举类型 enum 至少应该有一名成员。
    • 设置的时候,可以设置为索引,也可以对应的枚举名称;
    • 枚举类型 enum 可以与整数进行显式转换,但不能进行隐式转换。
      • 显示转换会在运行时检查数值范围,如果不匹配,将会引起异常。
    • 很多人感觉 enum 很少用,一是因为应用场景确实比较窄,二是因为可以被其他数据类型所代替;但按照编码规范,限制选择范围场景,除了 bool 以外的,推荐使用 enum 类型来定义。

UserType 用户定义的值类型

  • 聊一聊 UserType。
    • 用户定义值类型使用 type UserType is DefaultType 来定义,其中 UserType 是新引入的类型的名称, DefaultType 必须是内置的值类型(”底层类型”)。自定义类型的值的数据表示则继承自底层类型,并且 ABI 中也使用底层类型。
    • ⚠️: 用户定义的类型 UserType 没有任何运算符或绑定成员函数。即使是操作符 == 也没有定义。也不允许与其他类型进行显式和隐式转换。
  • UserType 有属性么?有方法么?
    • UserType.wrap()
    • UserType.unwrap()

address

  • addressaddress payable 有什么区别
    • address:保存一个 20 字节的值(以太坊地址的大小)。
    • address payable :可支付地址,与 address 相同,不过有成员函数 transfersend
    • 如果你需要 address 类型的变量,并计划发送以太币给这个地址,那么声明类型为 address payable 可以明确表达出你的需求。 同样,尽量更早对他们进行区分或转换。
    • 这种区别背后的思想是 address payable 可以向其发送以太币,而不能向一个普通的 address 发送以太币。比如,它可能是一个智能合约地址,并且不支持接收以太币。
  • address 类型分别有什么属性?介绍一下用途
    1. .balance : 以 Wei 为单位的余额。
      <address>.balance    returns(uint256)
      
    2. .code : 地址上的代码(可以为空)
      <address>.code        returns(bytes memory)
      
    3. .codehash : 地址的 codehash
      <address>.codehash    returns(bytes32)
      
  • address 类型有哪些方法以及各自的作用。
    1. address(): 可以将地址转换到地址类型。
    2. payable(): 将普通地址转为可支付地址。
      1. addressaddress payable 的转换。可以通过 payable(x) 进行 ,其中 x 必须是 address 类型。
    3. .transfer(uint256 amount): 将余额转到当前地址(合约地址转账)
      <address payable>.transfer(uint256 amount)
      
      1. 失败时抛出异常, 等价于require(send()) 使用固定(不可调节)的 2300 gas 的矿工费,错误会 reverts
      2. 需要 payable address
    4. .send(uint256 amount): 将余额转到当前地址,并返回交易成功状态(合约地址转账)
      <address payable>.send(uint256 amount) returns (bool)
      
      1. 失败时仅会返回 false,不会终止执行(合约地址转账);使用固定(不可调节)的 2300 gas 的矿工费。
      2. 需要 payable address
      3. 补充:send 与 transfer 对应,但 send 更底层。如果执行失败,transfer 不会因异常停止,而 send 会返回 false。transfer 相对 send 较安全
      4. send() 执行有一些风险:如果调用栈的深度超过 1024 或 gas 耗光,交易都会失败。因此,为了保证安全,必须检查 send 的返回值,如果交易失败,会回退以太币。
    5. .call(bytes memory): 用给定的有效载荷(payload)发出低级 CALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账)
      <address>.call(bytes memory) returns (bool, bytes memory)
      
      1. 发送所有可用 gas,也可以自己调节 gas。
      2. 返回两个参数,一个 bool 值代表成功或者失败,另外一个是可能存在的 data
      3. 低级 CALL 调用:不需要 payable address, 普通地址即可
    6. .delegatecall(bytes memory): 用给定的有效载荷(payload)发出低级 DELEGATECALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账)
      <address>.delegatecall(bytes memory) returns (bool, bytes memory)
      
      1. 发出低级函数 DELEGATECALL,失败时返回 false,发送所有可用 gas,也可以自己调节 gas。
    7. staticcall(bytes memory): 用给定的有效载荷(payload)发出低级 STATICCALL 调用,并返回交易成功状态和返回数据(调用合约的方法并转账)
      <address>.staticcall(bytes memory) returns (bool, bytes memory)
      
      1. 发送所有可用 gas,也可以自己调节 gas。
  • 地址的三种转帐有什么区别?transfer / send /call
    • 相同点
      • 三种方法都可以进行转账
      • _to.transfer(100)_to.send(100)_to.call{value: 100}("") 的接收方都是_to
        • 如果_to合约中必须增加 fallback 或者 receive 函数!
        • 否则报错In order to receive Ether transfer the contract should have either 'receive' or payable 'fallback' function
    • 不同点:
      • 低级 CALL 调用:不需要 payable address
        • transfer 和 send 只能是 payable address
      • call 的 gas 可以动态调整
        • transfer 和 send 只能是固定制 2300
      • call 除了可以转账外,可以还可以调用不知道 ABI 的方法,还可以调用的时候转账
        • 当调用不存在的合约方法时候,会触发对方合约内的 fallback 或者 receive
        • 如果使用 _to.call{value: 100}(data),那么data中被调用的方法必须添加 payable 修饰符,否则转账失败!
        • 因为可以调用方法,所以 call 有两个参数,除了一个 bool 值代表成功或者失败,另外一个是可能存在的 data,比如创建合约时候得到部署的地址,调用函数时候得到的函数放回值。
  • delegatecall 和 call 的区别
    • delegatecall 使用方法和 call 完全一样。区别在于,delegatecall 只调用给定地址的代码(函数),其他状态属性如(存储,余额 …)都来自当前合约。delegatecall 的目的是使用另一个合约中的库代码。
    • 委托调用是:委托对方调用自己数据的。类似授权转账,比如我部署一个 Bank 合约, 授权 ContractA 使用 Bank 地址内的资金,ContractA 只拥有控制权,但是没有拥有权。
    • 委托调用后,所有变量修改都是发生在委托合约内部,并不会保存在被委托合约中。
      • 利用这个特性,可以通过更换被委托合约,来升级委托合约。
    • 委托调用合约内部,需要和被委托合约的内部参数完全一样,否则容易导致数据混乱
      • 可以通过顺序来避免这个问题,但是推荐完全一样
  • 聊一聊三种低级 call
    1. calldelegatecallstaticcall 都是非常低级的函数,应该只把它们当作最后一招来使用,它们破坏了 Solidity 的类型安全性。
    2. 三种方法都提供 gas 选项,而 value 选项仅 call 支持 。所以三种 call 里只有 call 可以进行 ETH 转账,其他两种不可以进行转账。
    3. 不管是读取状态还是写入状态,最好避免在合约代码中硬编码使用的 gas 值。这可能会引入错误,而且 gas 的消耗也是动态改变的。
    4. 如果在通过低级函数 delegatecall 发起调用时需要访问存储中的变量,那么这两个合约的存储布局需要一致,以便被调用的合约代码可以正确地通过变量名访问合约的存储变量。 这不是指在库函数调用(高级的调用方式)时所传递的存储变量指针需要满足那样情况。
  • 编写合约的时候,如果地址不是 checksum address ,该怎么处理?
    • 通过浏览器内转
  • 聊一下合约类型
    • 每一个合约定义都有他自己的类型。
    • 可以隐式地将合约转换为从他们继承的合约。
    • 合约可以显式转换为 address 类型。
    • 可以转换为 address payable 类型
    • ⚠️ 注意:合约不支持任何运算符。
  • 合约的属性
    • type(C).name
      • 获得合约名
    • type(C).creationCode
      • 获得包含创建合约字节码的内存字节数组。
      • 该值和合约内使用 address(this).code; 结果一样。
      • 它可以在内联汇编中构建自定义创建例程,尤其是使用 create2 操作码。
      • 不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
    • type(C).runtimeCode
      • 获得合约的运行时字节码的内存字节数组。这是通常由 C 的构造函数部署的代码。
      • 如果 C 有一个使用内联汇编的构造函数,那么可能与实际部署的字节码不同。
      • 还要注意库在部署时修改其运行时字节码以防范定期调用(guard against regular calls)。 与 .creationCode 有相同的限制,不能在合约本身或派生的合约访问此属性。 因为会引起循环引用。
  • 如何获取合约本身的 bytecode?
    • type(C).creationCode

数据位置

  • 聊一聊 storage/memory/calldata 三种数据位置

    • 存储 storage : 状态变量保存的位置,只要合约存在就一直存储.
    • 内存 memory : 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
    • 调用数据 calldata : 用来保存函数参数的特殊数据位置,是一个只读位置。
      • 调用数据 calldata 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存 memory 。
      • 主要用于外部函数的参数,但也可用于其他变量,无论外部内部函数都可以使用。
    • 核心:更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储 storage 来说)的复制仅在某些情况下进行拷贝。
  • 三种数据位置相互赋值,以及相同数据位置之间赋值都是拷贝么?详细介绍一下。

    1. 存储变量 赋值给 存储变量 (同类型)
      • 值 类 型: 创建一个新副本。
      • 引用类型: 创建一个新副本。
    2. 内存变量 赋值给 存储变量
      • 值 类 型: 创建一个新副本。
      • 引用类型: 创建一个新副本。
    3. 存储变量 赋值给 内存变量
      • 值 类 型: 创建一个新副本。
      • 引用类型: 创建一个新副本。
    4. 内存变量 赋值给 内存变量 (同类型)
      • 值 类 型: 创建一个新副本。
      • 引用类型: 不会创建副本。(重要)
  • memory 和 calldata 之间的区别

    • 函数调用函数时的区别:calldata可以隐式转换为memory
    • calldata 可以隐式转换为 memory
    • memory 不可以隐式转换为 calldata
    • 作为参数:
      • memory 可以修改参数
      • calldata 禁止修改参数

array

  • 创建数组的方法有哪些。(定长数组,动态数组)

    • 固定长度数组:创建
      uint256[5] public T1 = [1, 2, 3, 4, 5];
      address[5] public A =   [0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac];
      bytes1[5] public B = [bytes1(0x61)];
      
    • 可变长度数组:创建
      • 方式 1: uint256[] T1 = [1, 2, 3, 4, 5];
        • 该方式不可以在函数内创建
      • 方式 2: uint256[] T2 = new uint256[](5);
        • 用方式 2 创建数组时,若数组为成员变量, 则默认为 storage 类型;
        • 若为局部变量默认为 memory 类型,memory 类型的数组,必须声明长度,并且长度创建后不可变。
        • push 方法不能用在 memeory 的数组上,只能逐个索引的赋值。
    • 二维数组:创建
      • 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组(二维数组),应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。在 Solidity 中, X[3] 总是一个包含三个 X 类型元素的数组,即使 X 本身就是一个数组.
      • uint256[2][3] public T = [[1, 2], [3, 4], [5, 6]];
      • T.length 为 3
    • 其它
      • uint256[2][] public T = new uint256[2][](10);
  • 内存中如何创建数组

    • 可以使用 new 关键字在内存中创建动态数组。与存储数组相反,不能通过设置 .length 成员来调整内存动态数组的长度。 (需要例子来演示)。memory 类型的数组长度创建后不可变,不能通过修改成员变量 .push 改变 memory 数组的大小。必须提前计算数组大小,或者创建一个新的内存数组并复制每个元素(当然也可以通过 assemly 来修改memory 可变数组的长度, 长度缩短是安全的,增加则需自行管控风险)。
    • 创建格式: uint256[] memory a = new uint256[](5);
    • 内存中创建的数组是局部变量。
    • 内存中不能创建动态数组,必须创建定长数组。
  • 下面代码可以正常运行么?如果可以,值分别是什么?

    • 目前需要注意的是,定长的 memory 数组并不能赋值给变长的 memory 数组,下面的例子 f 函数是无法运行的:
    • 引发了一个类型错误,因为 unint[3] memory,不能转换成 uint[] memory
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    contract Demo {
        uint256[] public x = [1, 3, 4];
        uint256[] public y = [uint256(1), 3, 4];
        uint256[] public z = [uint8(1), 3, 4];
    
        function f() public pure returns(uint256[] memory x){
            x = [uint256(1), 3, 4];
        }
    }
    
  • 下面代码可以正常运行么?

    • 不可以,需要 s([uint256(1), uint256(2)])
    contract T {
        function t() public {
            s([1, 2]);
        }
    
        function s(uint256[2] memory _arr) public {}
    }
    
  • 有哪些可以操作数组的方法,分别功能是什么?(push / pop / delete/x[start:end])

    • push 增加,改变长度
    • pop 删除最后一位,改变长度
    • delete: 删除对应的索引;删除并不会改变长度,索引位置的值会改为默认值。
    • x[start:end]: 数组切片,仅可使用于 calldata 数组.
  • 能否实现一个完全 delete 数组的方法(删除数据,长度改变),说下实现逻辑也可以。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    contract FunctionOutputs {
        function deletePro(uint256[] memory arr, uint256 _index)
            internal
            pure
            returns (uint256[] memory temp)
        {
            require(_index < arr.length, "index out of bound");
            temp = new uint256[](arr.length - 1);
            for (uint256 index = 0; index <= temp.length - 1; index++) {
                if (index >= _index) {
                    temp[_index] = arr[_index + 1];
                } else {
                    temp[index] = arr[index];
                }
            }
        }
    
        function test()
            external
            pure
            returns (uint256[] memory arr, uint256[] memory temp)
        {
            arr = new uint256[](3);
            arr[0] = 1;
            arr[1] = 2;
            arr[2] = 3;
            assert(arr[0] == 1);
            assert(arr[1] == 2);
            assert(arr[2] == 3);
            assert(arr.length == 3);
    
            temp = deletePro(arr, 1);
            assert(temp[0] == 1);
            assert(temp[1] == 3);
            assert(temp.length == 2);
        }
    }
    

bytes

  • bytes 的创建方法有哪些?
    • 状态变量的创建方式
      bytes public welcome = bytes("1.Welcome");
      
    • 函数中可变字节数组创建方式:
      // 可变字节数组创建方式
      bytes memory temp2 = new bytes(length);
      
  • bytes bytes32 ,和 bytes32[] 区别 是什么?
    • abcBytes 的值是: 0x616263;
    • abcArray 的值是:[0x61..00,0x62..00,0x63..00]
      bytes32[] public abcArray = [bytes1("a"), bytes1("b"), bytes1("c")];
      // 0x616263
      bytes public abcBytes = bytes("abc");
    
  • 下面的值分别是什么?
    bytes32[] public abcArray = [bytes1("a"), bytes1("b"), bytes1("c")];
    bytes public abcBytes = bytes("abc");
    
  • bytes 的值如何修改?(bytesVar[7] = 'x'
  • 有哪些方法可以操作 bytes 类型数据?分别是什么功能?
    • concat/ push / x[start:end] / delete / bytes()
  • bytesstring 之间如何转换?
    • 可以使用 bytes() 构造函数将字符串转换为 bytes
    • 可以使用 string() 构造函数将 bytes 转换为字符串。
  • 如何比较两个 bytes 数据是否相同?
    • keccak256(welcome2) == keccak256(welcome1);

string

  • 如何修改 string 类型的字符串?
    • 获取字符串的长度
      • bytes(str).length:以字节长度表示字符串的长度
    • 某个字符串索引的字节码
      • bytes1 temp1 = bytes(str)[_index];
        function getIndexValue(uint256 _index) public view return(string memory) {
            bytes1 temp1 = bytes(welcome)[_index]; // 返回固定长度的 bytes1
            bytes memory temp2 = new bytes(1); // 可变字节数组创建方式
            temp2[0] = temp1;
            return string(temp2);
        }
        
    • 修改字符串
      • bytes(s)[7] = 'x'
  • 如何比较两个 string 类型的字符串是否相等?
    • keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
    • keccak256(bytes(s1)) == keccak256(bytes(s2)) : 更推荐这个,省 gas

mapping

  • mapping 使用的时候有哪些需要注意的?

    • _KeyType:可以是任何内置类型,或者 bytes 和 字符串。
      • 不允许使用引用类型或复杂对象
      • 键是唯一的,其赋值方式为:map[a]=test; 意思是键为 a,值为 test;。
    • _ValueType: 可以是任何类型。
    • mapping 支持嵌套。
    • 映射的数据位置(data location)只能是 storage,通常用于状态变量。
    • mapping 不能用于 public 函数的参数或返回结果
      • 映射只能是 存储 storage 的数据位置,因此只允许作为状态变量 或 作为函数内的 存储 storage 引用 或 作为库函数的参数。 它们不能用合约公有函数的参数或返回值。
      • 这些限制同样适用于包含映射的数组和结构体。
    • 映射可以标记为 public,Solidity 自动为它创建 getter 函数。
      • _KeyType 将成为 getter 的必须参数,并且 getter 会返回 _ValueType
      • 如果 ValueType 是一个映射。这时在使用 getter 时将需要递归地传入每个 KeyType 参数,
    • 映射与哈希表不同的地方:在映射中,并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。正因为如此,映射是没有长度的,也没有 key 的集合value 的集合的概念。映射只能是存储的数据位置,因此只允许作为状态变量或作为函数内的存储引用 或 作为库函数的参数。
  • mapping 如何获取-设置-删除?

    • balances[msg.sender];
    • balances[msg.sender] += amount;
    • delete balances[msg.sender];
  • mapping 怎么做局部变量?

    • mapping 类型可以用做局部变量,但只能引用状态变量,而且存储位置为 storage。
    contract Mapping {
      mapping(address => uint256) public balances;
    
      function updateBalance() public returns (uint256) {
          // mapping 局部变量 ref 引用状态变量 balances
          mapping(address => uint256) storage ref = balances;
          ref[msg.sender] = 3;
          return ref[msg.sender];
      }
    }
    
  • 如何实现一个可迭代的 mapping?

    • (Mapping+array)
    contract Mapping {
      mapping(address => uint256) public balances;
      mapping(address => bool) public balancesInserted;
      address[] public balancesKey;
    
      function set(address _ads, uint256 amount) external {
          balances[_ads] = amount;
          if (!balancesInserted[_ads]) {
              balancesInserted[_ads] = true;
              balancesKey.push(_ads);
          }
      }
    
      function totalAddress() external view returns (uint256) {
          return balancesKey.length;
      }
    
      function first() external view returns (uint256) {
          return balances[balancesKey[0]];
      }
    
      function latest() external view returns (uint256) {
          return balances[balancesKey[balancesKey.length - 1]];
      }
    
      function get(uint256 i) external view returns (uint256) {
          require(i < balancesKey.length, "length error");
          return balances[balancesKey[i]];
      }
    }
    

struct

  • struct 使用的时候有哪些需要注意的?

  • struct 的创建方法有哪些?(3 种)

    // 第 1 种生成
    Book memory solidity = Book("Solidity", "LiSi", ++bookId, false, 0);
    
    // 第 2 种生成
    Book memory rust = Book({
        title: "Solidity",
        author: "LiSi",
        book_id: ++bookId,
        is_lost: false,
        uv: 0
    });
    
    // 第 3 种生成
    Book memory temp;
    temp.title = "Solidity";
    temp.author = "LiSi";
    temp.book_id = ++bookId;
    
  • struct 如何获取-设置-删除?

    1. 函数内仅读取结构体,变量使用 memory: Book memory _book = bookcase[_index];
      1. 函数内读取并返回,如果使用 memory 变量接收:从状态变量拷贝到内存中,然后内存中的变量拷贝到返回值。两次拷贝,消耗 gas 多
        1. Todo memory temp = list[_index];
      2. 函数内读取并返回,如果使用 storage 变量接收:直接从状态变量读取,状态变量拷贝到返回值。1 次拷贝,消耗 gas 小
      3. 总结: 读取时候推荐使用 storage
    2. 函数内获取并修改结构体,变量使用 storage
      Book storage _book = bookcase[_index]; // 因为要修改状态变量,所以使用 storage
      _book.author = "Anbang";
      
      • 函数内直接修改变量;在修改一个属性时比较省 Gas 费用
      • 函数内先获取存储到 storage 再修改:修改多个属性的时候比较省 Gas 费用
    3. 删除结构体的变量,仅仅是重置数据,并不是完全的删除。
  • 函数内使用 struct,标记 memory / storage 有什么区别?

    • 函数内读取时,标记 memory / storage,会产生完全不同的结果;
    • 特别注意:如果结构体内包含 mapping 类型,则必须使用 storage,不可以使用 memeory.
  • 聊一下众筹合约的实现逻辑

类型转换

  • 隐式转换的方式有哪些?
    • 隐式转换的场景: 在赋值, 参数传递给函数以及应用运算符时。
    • 隐式转换的场景:
      • 可以进行值类型之间的隐式转换
      • 不会丢失任何信息
    • 例如,uint8 可以转换为 uint16/uint24../uint256,因为uint8uint16这些类型的子集。但是 int8 不可以转换为 uint256,因为 int8 可以包含 uint256 中不允许的负值,比如 -1
  • 显示转换有哪些需要注意的?
    • uint8 - uint256 之间转换的原理
    • bytes1 - bytes32 之间转换的原理
    • 整型加大数据位置是从左侧增加,减小数据位置也是从左侧移除;(整型是右对齐)
    • 字节加大数据位置是从右侧增加,减小数据位置也是从右侧移除;(字节是左对齐)
  • 聊一聊 int/uint 类型之间的转换
    • 因为整型加大数据位置是从左侧增加,减小数据位置也是从左侧移除;(整型是右对齐
    • 整型转换成更大的类型,从左侧添加填充位。
    • 整型转换成更小的类型,会丢失左侧数据。
  • 聊一聊 bytes 字节类型之间的转换
    • 因为字节加大数据位置是从右侧增加,减小数据位置也是从右侧移除;(字节是左对齐
    • 字节转换为更大的类型时,从右侧添加填充位。
    • 字节转换到更小的类型时,丢失右侧数据。
  • bytesuint 转换
    • bytes 转换成 uint: 先转类型,再转大小
    • uint 转换成 bytes: 先转大小,再转类型
  • bytesaddress 转换
    • address 的格式是 0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac,是一个 bytes20 的数据.
    • 而由字符串生成 bytes 的方式是 keccak256(abi.encodePacked()),返回的是 bytes32 类型。地址是取 bytes32 数据中的后 20 位。如果删除前面的 12 位数据,可以使用 solidity assembly (内联汇编) 来截取,也可以借助 uint 转换成更小的类型,会丢失左侧数据的特性来完成。
  • uintaddress 转换
  • bytesbytes32 之间的转换
    • 创建长度,for 循环

字面常量与基本类型的转换

  • 十进制和十六进制字面常量之间的转换需要注意什么问题?
    • 只有在匹配数据范围时,才能进行隐形转换,如果超出,不会截断,而是报错。