Solidity05 Solidity 引用类型


引用类型, 在内存中存储的是对数据的引用,而不是实际的数据本身,实际数据存放在单独的位置。当我们创建引用类型的变量时,实际上分配了一个存储指向数据的地址的内存位置。这与 C 语言中的指针非常相似。

Solidity 中引用类型共有三种:数组array、结构体 struct 和映射 mapping

当调用一个函数时,如果参数为引用类型,那么在函数内部对参数数据的修改会影响到原始数据。这是因为操作的是引用,而这个引用指向了原始数据,而不是数据的副本。

一、数组 array

Solidity 中,数组 是一种用于存储相同类型元素的数据结构。

例如,声明一个数组变量 uint numbers[10],那么变量 numbers 存储了 10 个 uint 类型的数据。

数组中的特定元素可以通过索引访问,索引值从 0 开始。

例如,上面声明的数组 numbers,可以使用 numbers[0]、numbers[1] 访问前两个变量。

Solidity 支持 固定长度数组动态长度数组 两种类型。

1.1 固定长度数组

固定长度数组 在声明时需要指定数组的长度,并且这个长度在编译时就确定了,部署后无法更改。

声明一个 固定长度数组 ,需要指定元素类型和数量,语法如下:

type_name arrayName [length];

其中数组长度 length 必须是一个大于零的整数数字,type_name 可以是任何数据类型。

例如,声明一个 uint 类型,长度为 10 的数组 balance,如下所示:

uint balance[10];

初始化一个 固定长度数组,可以使用下面的语句:

uint balance[3] = [uint(1), 2, 3]; // 初始化固定长度数组
balance[2] = 5; // 设置第 3 个元素的值为 5

这里的数组 balance 的初值,为什么写为 [uint(1), 2, 3],而不是 [1,2,3] 呢?

这是因为在 Solidity 中,如果一个类型的字面量没有显式指定,那么就会由编译器自行推断。

整数字面量的解析规则是根据其值的范围来确定默认的类型。当你提供一个整数字面量时,编译器会尝试确定最小的类型来容纳该值。

如果整数字面量的值在 uint8 的取值范围内(0 到 255),编译器会将其默认解析为 uint8 类型。因为在这种情况下,使用 uint8 类型可以使用更少的存储空间,来提高存储效率。

如果整数字面量的值超出了 uint8 的取值范围,编译器会根据需要自动升级到更大的整数类型,如 uint16uint32

所以,我们在编写程序的时候,尽量显式指定某些字面量的类型,防止异常情况发生。

我们可以通过数组的索引来访问数组元素,例如:

uint salary = balance[2];

我们可以使用下面的合约,来展示 固定长度数组 的用法。

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

contract StaticArray {
    // 初始化固定长度数组为[1,2,3]
    uint[3] balance = [uint(1), 2, 3];

    function staticArray() external returns (uint length, uint[3] memory array) {
        // 获取第 3 个元素的值
        uint element = balance[2];

        // 设置第 3 个元素的值乘以 2
        balance[2] = element * 2;

        // 返回数组的长度和数组所有元素
        return (balance.length, balance);
    }
}

我们将合约代码复制到 Remix,进行编译,并部署到区块链上:点击 staticArray,返回结果:数组长度为 3,数组元素为 1,2,6。

1.2 动态长度数组

动态长度数组固定长度数组 相比,就是 动态长度数组 的长度是可以改变的。

声明一个 动态长度数组,只需要指定元素类型,而不需要指定元素的数量,语法如下:

type_name arrayName[];

动态长度数组 初始化后,是一个长度为 0 的空数组。我们可以使用 push 方法,在数组末尾追加一个元素;使用 pop 方法,截掉末尾的一个元素。

其中,push 有两种用法:一种是带参数 push(x),第二种是不带参数 push(),如果不带参数,那么就会在数组的末尾添加一个具有默认值的元素。

动态长度数组 元素的访问方法,与固定长度数组相同,也是通过索引来访问数组元素。

我们可以使用下面的合约,来展示动态数组的用法。

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

contract DynamicArray {
    // 初始化动态长度数组为[1,2,3]
    uint[] balance = [uint(1), 2, 3];

    function dynamicArray() external returns (uint length, uint[] memory array) {
        // 追加两个新元素4、5
        balance.push(4);
        balance.push(5);

        // 删除最后一个元素 5
        balance.pop();

        // 返回数组的长度和数组所有元素
        return (balance.length, balance);
    }
}

我们将合约代码复制到 Remix,进行编译,并部署到区块链上。点击 dynamicArray,返回结果:数组长度为 4,数组元素为 1,2,3,4。

可以注意到:可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如:

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

注意bytes比较特殊,是数组,但是不用加[]。另外,不能用byte[]声明单字节数组,可以使用bytesbytes1[]bytesbytes1[] 省gas。

1.3 创建数组的规则

在Solidity里,创建数组有一些规则:

  • 对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:

    // memory动态数组
    uint[] memory array8 = new uint[](5);
    bytes memory array9 = new bytes(9);
    
  • 数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如[1,2,3]里面所有的元素都是uint8类型,因为在Solidity中,如果一个值没有指定type的话,会根据上下文推断出元素的类型,默认就是最小单位的type,这里默认最小单位类型是uint8。而[uint(1),2,3]里面的元素都是uint类型,因为第一个元素指定了是uint类型了,里面每一个元素的type都以第一个元素为准。

    下面的例子中,如果没有对传入 g() 函数的数组进行 uint 转换,是会报错的。

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.4.16 <0.9.0;
    
    contract C {
        function f() public pure {
            g([uint(1), 2, 3]);
        }
        function g(uint[3] memory _data) public pure {
            // ...
        }
    }
    
  • 如果创建的是动态数组,你需要一个一个元素的赋值。

    uint[] memory x = new uint[](3);
    x[0] = 1;
    x[1] = 3;
    x[2] = 4;
    

1.4 数组成员

  • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
  • push(): 动态数组拥有push()成员,可以在数组最后添加一个0元素,并返回该元素的引用。
  • push(x): 动态数组拥有push(x)成员,可以在数组最后添加一个x元素。
  • pop(): 动态数组拥有pop()成员,可以移除数组最后一个元素。

在这里插入图片描述

二、结构体 struct

Solidity 中的结构体是一种用户自定义的数据类型,它通过组合多个不同类型的变量成员来创建一个新的复合类型。

例如,一本书包含以下信息:

  • Title:标题
  • Author:作者
  • Book ID:书号

那么,我们就可以定义一个结构体类型,用来表示一本书。

2.1 结构体定义

定义一个结构体类型使用 struct 关键字,它的语法如下:

struct struct_name { 
   type1 type_name_1;
   type2 type_name_2;
   type3 type_name_3;
   ...
}

其中 struct_name 是结构体的名称,typeN 是结构体成员的数据类型,type_name_N 是结构体成员的名字。

比如,我们定义一个结构体类型 Book,用来表示一本书:

struct Book { 
   string title;
   string author;
   uint ID;
}

2.2 使用方法

我们要访问结构体的成员,需要使用成员访问操作符 (.)。

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

contract StructAccess {
   struct Book { 
     string title;
     string author;
     uint ID;
   }  // 定义结构体 Book

   Book book;  // 使用结构体 Book 声明变量

   function setBook() public {
      book.title = "Learn Solidity"; // 设置结构体的成员 title
      book.author = "BinSchool";
      book.ID = 1;
   }

  function getBookAuthor() public view returns(string memory) {
      return book.author; // 读取结构体的成员 author
  }
}

我们把合约代码复制到 Remix,进行编译,并部署到区块链上。

我们先点击 setBook 来设置书的信息,然后点击 getBookAuthor,来获取这本书的作者,返回的调用结果为:

string: BinSchool

2.3 初始化方式

结构体变量共有 3 种初始化数据的方式:按字段顺序初始化按字段名称初始化按默认值初始化

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

contract StructInit {
  struct Book { 
    string title;
    string author;
    uint ID;
  }  // 定义结构体 Book
 

  function getBooks() public pure returns(Book memory,Book memory,Book memory) {
    // 按字段顺序初始化
    Book memory book1 = Book('Learn Java', 'BinSchool', 1);

    // 按字段名称初始化
    Book memory book2 = Book({title:"Learn JS", author:"BinSchool", ID:2});

    // 按默认值初始化
    Book memory book3;
    book3.ID = 3;
    book3.title = 'Learn Solidity';
    book3.author = 'BinSchool';
    return (book1, book2, book3);
  }
}

我们把合约代码复制到 Remix,进行编译,并部署到区块链上。

点击 getBooks 后,返回的调用结果为:

0: tuple(string,string,uint256): Learn Java,BinSchool,1
1: tuple(string,string,uint256): Learn JS,BinSchool,2
2: tuple(string,string,uint256): Learn Solidity,BinSchool,3

三、映射 mapping

Solidity 中的映射类型 mapping,用来以键值对的形式存储数据。它的主要作用是提供高效的查找功能,类似于其它编程语言中的哈希表或者字典。

例如,在使用 白名单 的场景中,我们可以通过 映射类型 将用户地址映射到一个布尔值,以标识哪些地址是被允许的,哪些地址是不被允许的,从而高效地控制访问权限。

映射类型是智能合约中最常用的数据类型之一,我们必须熟练掌握。

3.1 映射定义

定义一个映射类型使用 mapping 关键字,它的语法如下:

mapping(key_type => value_type)

mapping 类型是将一个键 (key) 映射到一个值 (value)。

其中:key_type 可以是任何基本数据类型,比如:整型、地址型、布尔型、枚举型,以及 bytesstring,但是部分复杂对象不允许使用,比如:动态数组、结构体、映射。

value_type 可以是任何数据类型。

例如,在合约中声明一个 mapping 类型的变量:

struct MyStruct { uint256 value; } // 定义一个结构体

  mapping(address => uint256) a; // 正确
  mapping(string => bool[]) b; // 正确
  mapping(int => MyStruct) c; // 正确
  mapping(address => mapping(address => uint)) d; // 正确

  // mapping(uint[] => uint) public e;  // 错误
  // mapping(MyStruct => addrss) public f; // 错误
  // mapping(mapping(string=>int)) => uint) g; // 错误

3.2 使用方法

在智能合约中,mapping 类型的使用非常普遍的。比如,在 ERC20 代币合约中,经常会使用 mapping 类型的变量作为一个内部账本,用来记录每一个钱包地址拥有的代币余额。

下面,我们模拟一个稳定币 USDT 的合约:

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

contract USDT {
  mapping(address => uint256) balances; // 保存所有持有 USDT 账户的余额
  
  // 构造函数,合约部署时自动调用
  constructor() {
    balances[msg.sender] = 100; // 初始设定合约部署者的账户余额为 100 USDT
  }

  // 查询某一个账户的USDT余额
  function balanceOf(address account) public view returns(uint256) {
    return balances[account];
  }
}

在这个合约中,首先定义了一个保存所有持有 USDT 账户余额的 mapping 类型的变量 balances,用来作为一个内部账本。

mapping(address => uint256) balances; // 保存所有持有 USDT 账户的余额

获取某个账号地址 0x5B38…ddC 持有 USDT 的数量:

uint256 value = balances[0x5B38...ddC];

设置某个账号地址 0x5B38…ddC 持有 USDT 的数量:

balances[0x5B38...ddC4] = 100 ;

在这里插入图片描述
将上方的合约部署者地址 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,填写到函数 balanceOf 的参数位置,点击 balanceOf 后,返回的结果为 100。

我们使用另外一个地址作为参数,比如 0xAA6663F64d9C6d1EcF9b849Ae677dD3315835666,点击 balanceOf 后,返回的调用结果为 0。

注意:mapping 类型的变量中获取一个不存在的键的值,并不会报错,而是会返回值类型的默认值。比如,整型会返回 0,布尔型会返回 false 等。

3.3 映射的规则

  • 规则1:映射的_KeyType只能选择Solidity内置的值类型,比如uintaddress等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。下面这个例子会报错,因为_KeyType使用了我们自定义的结构体:

    // 我们定义一个结构体 Struct
    struct Student{
        uint256 id;
        uint256 score; 
    }
    mapping(Student => uint) public testVar;
    
  • 规则2:映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量和library函数的参数(见例子)。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。

  • 规则3:如果映射声明为public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value

  • 规则4:给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key_Value对应新增的键值对。例子:

    function writeMap (uint _Key, address _Value) public{
        idToAddress[_Key] = _Value;
    }
    

3.4 映射的原理

  • 原理1: 映射不储存任何键(Key)的资讯,也没有length的资讯。

  • 原理2: 对于映射使用keccak256(h(key) . slot)计算存取value的位置。感兴趣的可以去阅读 WTF Solidity 内部规则: 映射存储布局

  • 原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是各个type的默认值,如uint的默认值是0。

3.5 优缺点

优点

mapping 可以用来存储数据集,并且提供了高效的查找功能。它可以根据“键”快速定位到特定元素。

我们知道,数组也可以用来存储数据集,但是在数组里面查找特定元素,就必须通过循环语句,遍历整个数组进行匹配,所以查找效率非常低,使用也不方便。

在数据集很大的情况下,通过 mapping 查找数据,效率要比数组高很多。

缺点

mapping 最大的问题是无法直接遍历。你不能使用 for 循环遍历 mapping 中的键值对,它也没有提供获取全部 key 或者 value 的功能。因此,在合约中遍历或迭代 mapping 类型的数据时,通常需要设计其他数据结构来辅助实现。

四、代码示例

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

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

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

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

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

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

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

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

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

pragma solidity ^0.8.21;
contract Mapping {
    mapping(uint => address) public idToAddress; // id映射到地址
    mapping(address => address) public swapPair; // 币对的映射,地址到地址
    
    // 规则1. _KeyType不能是自定义的 下面这个例子会报错
    // 我们定义一个结构体 Struct
    // struct Student{
    //    uint256 id;
    //    uint256 score; 
    //}
    // mapping(Struct => uint) public testVar;

    function writeMap (uint _Key, address _Value) public{
        idToAddress[_Key] = _Value;
    }
}
### Solidity 中引用数据类型的使用方法与特性 #### 1. 引用类型概述 在 Solidity 中,引用类型是指那些通过内存地址来访问的数据类型。这类变量并不直接保存实际值,而是指向存储该值的内存位置。这使得引用类型能够高效地传递大型对象而不必每次都创建完整的副本[^3]。 #### 2. 常见引用类型及其特点 - **结构体 (struct)** 结构体允许定义复杂的数据结构,可以包含多种不同类型的字段。当声明一个新的 struct 实例时,默认会初始化为其成员类型的零值。 - **数组 (array)** 数组分为固定大小和动态两种形式。对于动态数组而言,在对其进行赋值操作时可能会触发深拷贝行为;而对于静态数组来说,则通常只会在必要时候才发生浅层复制。 - **映射 (mapping)** 映射是一种键值对集合,其中 key 的范围是有限制的(比如 uint 或 address),value 可以为任意类型。值得注意的是,映射总是存放在 storage 存储区,并且无法遍历所有的 keys[]。 #### 3. 数据位置说明 每一种引用类型都有关联的数据位置属性——`storage`, `memory` 和 `calldata`. 这些关键字决定了变量是在哪分配以及如何处理它: - Storage 表示持久化状态变量; - Memory 是临时性的,函数调用结束后即消失; - Calldata 则专门用于接收外部传入的消息参数列表。 #### 4. 示例代码展示 下面给出一段简单的 solidity 合约片段,展示了上述三种主要引用类型的定义方式及基本操作: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Example { // 定义一个名为 Person 的结构体 struct Person { string name; uint age; } // 创建一个 person 类型的状态变量并设置初始值 Person public founder = Person("Alice", 25); // 动态长度字符串数组 string[] private namesList; // mapping from addresses to balances mapping(address => uint) internal accountBalances; function addName(string memory _name) external returns(bool success){ namesList.push(_name); return true; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杰哥的技术杂货铺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值