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
:无符号整数,值大于等于0int
:有符号整数bool
:布尔值address
:用于储存账户地址bytes
:定长字节数组(1~32)enum
:枚举类型
1.2 引用类型
「引用类型」的变量保存的是数据存储的地址,而不是数据本身。这样一来,在赋值或是传参时所传递的是数据的地址(pass by reference)。
Solidity一共只有三种引用类型分别为:
- 数组 一堆类型相同的变量集合
- 结构体 一堆不同类型的变量集合
- 映射类型 一堆键值对的集合
1.2.1 引用传递
传数据地址又被称为「引用传递」
我们一般将赋值或是传参时所传递的是数据地址称之为「引用传递」(pass by reference)
下面的示例中我们定义了两个字节数组 bts1
和 bts2
。在第2行中 bts2 = bts1
,这时 bts2
和 bts1
指向了同一个数据地址。当修改它们中任何一个时,另一个的值也会跟着发生变化。
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 小结
- 在Solidity中,数据类型可分为值类型和引用类型
- 值类型的变量保存的是实际的数据内容,在赋值或传参时是值传递,即直接拷贝数据
- 引用类型的变量保存的是数据存储地址,在赋值或传参时是引用传递,即拷贝地址(不完全是,还与
datalocation
有关) - Solidity中的值类型包括:布尔类型、整型、枚举类型、静态浮点型、静态字节数组、自定义值类型等
- Solidity中的引用类型包括:数组、结构体、映射类型
- 在函数内部改变引用类型变量的值会影响原变量的值,而值类型变量则不会。
二、数据位置
我们上面有提到变量是记录在存储介质上面的。普通应用程序的数据可能存在内存里或者磁盘里。其中存在内存的数据是易失的,程序退出运行后,就不再存在了。存在磁盘的数据是永久的,下次程序运行数据会被重新读取。同理,合约也会有不同的数据位置(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); }
对于 storage
和 memory
这两个概念我们应该不难理解,可以想象成 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.sender
,block.number
和msg.data
,他们分别代表请求发起地址,当前区块高度,和请求数据。下面是一些常用的全局变量,更完整的列表请看这个链接:
blockhash(uint blockNumber)
: (bytes32
) 给定区块的哈希值 – 只适用于最近的256个区块, 不包含当前区块。block.coinbase
: (address payable
) 当前区块矿工的地址block.gaslimit
: (uint
) 当前区块的gaslimitblock.number
: (uint
) 当前区块的numberblock.timestamp
: (uint
) 当前区块的时间戳,为unix纪元以来的秒gasleft()
: (uint256
) 剩余 gasmsg.data
: (bytes calldata
) 完整call datamsg.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
: 1gwei
: 1e9 = 1000000000ether
: 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
: 1minutes
: 60 seconds = 60hours
: 60 minutes = 3600days
: 24 hours = 86400weeks
: 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
, memory
和calldata
三个关键字的用法。他们出现的原因是为了节省链上有限的存储空间和降低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;
}
}