当前位置: 首页 > article >正文

Solidity06 Solidity变量数据存储和作用域

合约里面常常需要记录一些状态数据,例如 Token 余额,合约 Owner 等等。那么这些状态是放在哪里呢?答案是:这些数据被放在变量里,我们可以对变量进行运算操作。在 Solidity 中,变量就是用来存储数据的容器。

我们学过初等代数,那么我们可以知道 x=3, y=4 这个表达式表示的是把 3 赋值给 x , 把 4 赋值给 y 。合约变量的表达式也类似。例如,下面的表达式中定义了两个变量,一个是 x ,它的值是 3 , 另一个是 y , 它的值是 4

uint x = 3;
uint y = 4;

我们说过变量是用来存储数据的容器,所以它们会在存储介质里面占据一定的空间。如下面所示:
在这里插入图片描述

一、数据类型

上面我们定义了两个变量,它们的类型都是 uint 。Solidity 是静态类型语言,需要为每个变量都指定类型。你可以为变量指定下面的类型:

在这里插入图片描述

合约本质上也是计算机程序,所以与其他程序一样可以处理多种多样的数据类型。而每种类型都可能有不同表示方式和操作方式。Solidity根据传参时是值传递还是引用传递可以分为两大类型,一种是「值类型」,另一种是「引用类型」

1.1 值类型

「值类型」的变量保存的是实际的数据内容。值类型在进行赋值或者传参时永远都是值传递,也就是把数据直接拷贝过去的。这样赋值后的值和原来的值是完全独立互不影响的。

示例:值传递

uint8 a = 1;
uint8 b = a;

值类型列表

  • uint:无符号整数,值大于等于0
  • int:有符号整数
  • bool:布尔值
  • address:用于储存账户地址
  • bytes:定长字节数组(1~32)
  • enum:枚举类型

1.2 引用类型

「引用类型」的变量保存的是数据存储的地址,而不是数据本身。这样一来,在赋值或是传参时所传递的是数据的地址(pass by reference)。

Solidity一共只有三种引用类型分别为:

  • 数组 一堆类型相同的变量集合
  • 结构体 一堆不同类型的变量集合
  • 映射类型 一堆键值对的集合

1.2.1 引用传递

传数据地址又被称为「引用传递」

我们一般将赋值或是传参时所传递的是数据地址称之为「引用传递」(pass by reference

下面的示例中我们定义了两个字节数组 bts1bts2 。在第2行中 bts2 = bts1 ,这时 bts2bts1 指向了同一个数据地址。当修改它们中任何一个时,另一个的值也会跟着发生变化。

bytes memory bts1 = "btc";
bytes memory bts2 = bts1;

console.log("bts1: %s", string(bts1)); // bts1: btc
console.log("bts2: %s", string(bts2)); // bts2: btc

bts2[0] = 'e'; //这里只改了bts2[0]的值,但是你会发现bts1[0]的值也会跟着变动

console.log("bts1: %s", string(bts1)); // bts1: etc
console.log("bts2: %s", string(bts2)); // bts2: etc

现在我们明白什么是引用类型,什么是引用传递。那么所有的引用类型都是使用引用传递吗?答案揭晓:不是!引用类型到底是「值传递」还是「引用传递」还得看一个修饰符:「数据位置修饰符」。我们下面「数据位置」会解释一共有哪几种数据位置,引用类型在何种情况是「值传递」何种情况是「引用传递」。

1.3 小结

  1. 在Solidity中,数据类型可分为值类型和引用类型
  2. 值类型的变量保存的是实际的数据内容,在赋值或传参时是值传递,即直接拷贝数据
  3. 引用类型的变量保存的是数据存储地址,在赋值或传参时是引用传递,即拷贝地址(不完全是,还与 datalocation 有关)
  4. Solidity中的值类型包括:布尔类型、整型、枚举类型、静态浮点型、静态字节数组、自定义值类型等
  5. Solidity中的引用类型包括:数组、结构体、映射类型
  6. 在函数内部改变引用类型变量的值会影响原变量的值,而值类型变量则不会。

二、数据位置

我们上面有提到变量是记录在存储介质上面的。普通应用程序的数据可能存在内存里或者磁盘里。其中存在内存的数据是易失的,程序退出运行后,就不再存在了。存在磁盘的数据是永久的,下次程序运行数据会被重新读取。同理,合约也会有不同的数据位置(data location)。比如,有些变量是永久记录在链上的;有些变量是存在 EVM 内存里的,函数退出后就消失了。

Solidity 一共有三种数据位置,指定了变量数据位置。它们分别为:

  • storage :合约里的状态变量默认都是storage,存储在链上。(数据会被存储在链上,是永久记录的,其生命周期与合约生命周期一致)

  • memory :函数里的参数和临时变量一般用memory,存储在内存中,不上链。(数据存储在内存,是易失的,其生命周期与函数调用生命周期一致,函数调用结束数据就消失了)

  • calldata :和memory类似,存储在内存中,不上链。(与memory类似,数据会被存在一个专门存放函数参数的地方,与memory不同的是calldata数据是不可更改的。另外相比于memory,它消耗更少的Gas)

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

在这里插入图片描述

对于 storagememory 这两个概念我们应该不难理解,可以想象成 storage = 磁盘, memory = RAM。但是对于 calldata 也许你会觉得陌生,到底其与 memory 有什么区别,为什么非要分出来这样的一种数据位置。由于我们这篇文章属于是入门教程,我们暂时不引入过多的复杂性,使得大家觉得概念太多太复杂,所以我们暂时不再延伸。

目前你只需要知道 calldata 相对于 memory 有这下面几个区别即可:

  • 只能在引用类型的函数参数使用
  • 数据不可更改(immutable)
  • 易失的(non-persistent)
  • 消耗更少的gas(gas efficient)

所以一条简单的原则是:如果你的引用类型函数参数不需要修改,你应该尽可能使用 calldata 而不是 memory

三、数据位置和赋值规则

在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)。规则如下:

  • 赋值本质上是创建引用指向本体,因此修改本体或者是引用,变化可以被同步:

    • storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。例子:

      uint[] x = [1,2,3]; // 状态变量:数组 x
      
      function fStorage() public{
          //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
          uint[] storage xStorage = x;
          xStorage[0] = 100;
      }
      

在这里插入图片描述

  • memory赋值给memory,会创建引用,改变新变量会影响原变量。

  • 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方。这有时会涉及到开发中的问题,比如从storage中读取数据,赋值给memory,然后修改memory的数据,但如果没有将memory的数据赋值回storage,那么storage的数据是不会改变的。

四、变量的作用域

Solidity中变量按作用域划分有三种,分别是状态变量(state variable),局部变量(local variable)和全局变量(global variable)

4.1 状态变量

状态变量是数据存储在链上的变量,所有合约内函数都可以访问,gas消耗高。状态变量在合约内、函数外声明:

contract Variables {
    uint public x = 1;
    uint public y;
    string public z;
}

我们可以在函数里更改状态变量的值:

function foo() external{
    // 可以在函数里更改状态变量的值
    x = 5;
    y = 2;
    z = "0xAA";
}

4.2 局部变量

局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,gas低。局部变量在函数内声明:

function bar() external pure returns(uint){
    uint xx = 1;
    uint yy = 3;
    uint zz = xx + yy;
    return(zz);
}

4.3 全局变量

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

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

在上面例子里,我们使用了3个常用的全局变量:msg.senderblock.numbermsg.data,他们分别代表请求发起地址,当前区块高度,和请求数据。下面是一些常用的全局变量,更完整的列表请看这个链接:

  • blockhash(uint blockNumber): (bytes32) 给定区块的哈希值 – 只适用于最近的256个区块, 不包含当前区块。
  • block.coinbase: (address payable) 当前区块矿工的地址
  • block.gaslimit: (uint) 当前区块的gaslimit
  • block.number: (uint) 当前区块的number
  • block.timestamp: (uint) 当前区块的时间戳,为unix纪元以来的秒
  • gasleft(): (uint256) 剩余 gas
  • msg.data: (bytes calldata) 完整call data
  • msg.sender: (address payable) 消息发送者 (当前 caller)
  • msg.sig: (bytes4) calldata的前四个字节 (function identifier)
  • msg.value: (uint) 当前交易发送的 wei
  • block.blobbasefee: (uint) 当前区块的blob基础费用。这是Cancun升级新增的全局变量。
  • blobhash(uint index): (bytes32) 返回跟当前交易关联的第 index 个blob的版本化哈希(第一个字节为版本号,当前为0x01,后面接KZG承诺的SHA256哈希的最后31个字节)。若当前交易不包含blob,则返回空字节。这是Cancun升级新增的全局变量。

在这里插入图片描述

4.4 全局变量-以太单位与时间单位

以太单位

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

  • wei: 1
  • gwei: 1e9 = 1000000000
  • ether: 1e18 = 1000000000000000000
function weiUnit() external pure returns(uint) {
    assert(1 wei == 1e0);
    assert(1 wei == 1);
    return 1 wei;
}

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

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

在这里插入图片描述

时间单位

可以在合约中规定一个操作必须在一周内完成,或者某个事件在一个月后发生。这样就能让合约的执行可以更加精确,不会因为技术上的误差而影响合约的结果。因此,时间单位在Solidity中是一个重要的概念,有助于提高合约的可读性和可维护性。

  • seconds: 1
  • minutes: 60 seconds = 60
  • hours: 60 minutes = 3600
  • days: 24 hours = 86400
  • weeks: 7 days = 604800
function secondsUnit() external pure returns(uint) {
    assert(1 seconds == 1);
    return 1 seconds;
}

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

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

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

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

在这里插入图片描述

在这一讲,我们介绍了Solidity中的引用类型,数据位置和变量的作用域。重点是storage, memorycalldata三个关键字的用法。他们出现的原因是为了节省链上有限的存储空间和降低gas

五、Solidity代码示例

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

contract DataStorage {
    // The data location of x is storage.
    // This is the only place where the
    // data location can be omitted.
    uint[] public x = [1,2,3];

    function fStorage() public{
        //声明一个storage的变量xStorage,指向x。修改xStorage也会影响x
        uint[] storage xStorage = x;
        xStorage[0] = 100;
    }

    function fMemory() public view{
        //声明一个Memory的变量xMemory,复制x。修改xMemory不会影响x
        uint[] memory xMemory = x;
        xMemory[0] = 100;
        xMemory[1] = 200;
        uint[] memory xMemory2 = x;
        xMemory2[0] = 300;
    }

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

contract Variables {
    uint public x = 1;
    uint public y;
    string public z;

    function foo() external{
        // 可以在函数里更改状态变量的值
        x = 5;
        y = 2;
        z = "0xAA";
    }

    function bar() external pure returns(uint){
        uint xx = 1;
        uint yy = 3;
        uint zz = xx + yy;
        return(zz);
    }

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

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

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

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

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

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

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

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

http://www.kler.cn/a/514887.html

相关文章:

  • 数学规划问题2 .有代码(非线性规划模型,最大最小化模型,多目标规划模型)
  • c#配置config文件
  • vim练级攻略(精简版)
  • 变频器硬件接线
  • LAYA3.0 组件装饰器说明
  • qiankun+vite+vue3
  • 安装centos7之后问题解决
  • 根除埃博拉病毒(2015MCM美赛A)
  • 嵌入式入门(一)-STM32CubeMX
  • c++中的链表list
  • 【Android】创建基类BaseActivity和BaseFragment
  • Spring注解篇:@RestController详解
  • AI大模型-提示工程学习笔记11-思维树
  • 【线性代数】列主元法求矩阵的逆
  • 云原生架构下的AI智能编排:ScriptEcho赋能前端开发
  • 2025_1_22_进程替换
  • Simula语言的云计算
  • C语言进阶习题【1】指针和数组(4)——指针笔试题3
  • RabbitMQ的消息可靠性保证
  • 网络(一)
  • C语言程序环境与预处理—从源文件到执行程序,这里面有怎么的工序?绝对0基础!
  • 【 MySQL 学习4】排序
  • Kafka 源码分析(一) 日志段
  • java中的String类、StringBuffer类、StringBuilder类的详细讲解(包含相互之间的比较)
  • BUG解决:安装问题transformer_engine+pytorch
  • 基于springboot+vue的高校社团管理系统的设计与实现