solidity的变量学习小结
一、 变量的定义
1. 状态变量格式
变量类型 [可见性] [可变状态] 变量名 [= 初始值];
- 可见性: 可选 (public/internal/private), 缺省时表示internal
- 可变状态: 可选 (constant/immutable), 缺省时表示普通变量,可以修改
- 初始值: 可选
2. 局部变量格式
变量类型 [数据位置] 变量名 [= 初始值];
- 数据位置: 引用类型必须指定 (memory/calldata/storage),值类型不需要指定
- 初始值: 推荐初始化
二、 变量的可见性
1. 可见性关键字用来控制变量是否可以被外部使用
2. 可见性关键字仅用来修饰状态变量,而不用来修饰局部变量
- 状态变量存在于区块链存储中,需要可见性控制;
- 局部变量存在于临时内存中,只在函数执行期间存在,不需要可见性控制
3. 状态变量的三种可见性
public
:自动生成一个同名的 getter 函数,可以在合约内外被调用。状态变量本身在合约内部可直接访问。private
:仅在当前合约内部可见,派生合约都无法访问。internal
:仅在当前合约及其派生合约内部可见。这是状态变量的默认可见性。
三、 变量的可变状态
1. 变量的可变状态有两个关键字
constant
:必须在编译时确定值,使用硬编码,gas效率高。immutable
:允许在构造函数中赋值一次,然后不可变。比constant
更灵活,gas 效率同样很高。
2. 变量的可变状态关键字仅用来修饰状态变量,而不用来修饰局部变量
3.当状态变量前边没有constant
和immutable
修饰时,表示普通的状态变量,它的值可以随时修改
4.并不是所有类型的状态变量都可以用constant
或immutable
修饰,只有值类型、string、bytes支持。
四、 变量的数据位置(存储位置)
1. EVM的存储布局
2. 所有状态变量都存储在 storage 位置,无论它们是值类型还是引用类型
3. 对于局部变量而言,引用类型必须指定数据位置,值类型不需要指定数据位置
-
值类型不需要指定数据位置的原因是编译器知道它们应该在栈上
- 固定大小 - 编译时就知道需要多少空间
- 栈存储 - EVM 自动管理栈空间
- 简单复制 - 赋值时直接复制值
- 无需生命周期管理 - 函数结束时自动清理
-
引用类型需要指定数据位置的原因是编译器需要知道是在内存还是存储中
- 动态大小 - 编译时不知道需要多少空间
- 需要显式分配 - 必须在 memory 或 storage 中分配空间
- 指针管理 - 需要管理内存指针和布局
- 生命周期管理 - 需要明确数据的生存期
Q:如果对值类型的局部变量使用数据位置修饰会怎样呢?
A:我验证了一下: uint memory a = 0;会编译报错: Data location can only be specified for array, struct or mapping types, but "memory" was given.
4. 理解赋值行为
-
4.1 内存
memory
中的赋值行为
出于“安全性考虑”、“Gas 成本优化”、“简化存储管理”这几个原因,字符串string
被设为不可变的 -
4.2 存储
storage
中的赋值行为理解storage引用,要区分“storage引用” 与 “状态变量”
当您为局部引用类型变量使用 storage 关键字时,您并不是在创建一个新的、独立的数据副本。相反,您是在创建一个指向已存在于存储中的某个状态的指针或引用,可以理解为一个指向链上存储中某个具体位置的“快捷方式”或“别名”
storage 引用不能跨越函数调用边界返回(只在函数执行期间存在),因为它只是一个函数内部的临时指针
试图在函数返回参数中使用storage修饰会编译报错:
Data location must be "memory" or "calldata" for return parameter in function, but "storage" was given.
通过storage引用进行操作,就是在操作它所指向的那个状态变量,对存储数据所做的修改会被永久保存下来
创建storage引用不同于直接状态变量赋值
contract DirectAssignment {struct User {uint256 id;string name;}User public user = User(1, "Alice");User public anotherUser;function directAssign() public {// 【直接状态变量赋值】这会创建完整的副本(包括所有字段)anotherUser = user; // 复制整个结构体}
}
contract StorageReference {struct User {uint256 id;string name;}User public user = User(1, "Alice");function useStorageRef() public {// 【创建 storage 引用】,不是复制数据User storage userRef = user;// 通过引用修改原数据userRef.id = 2; // 直接修改 user.iduserRef.name = "Bob"; // 直接修改 user.name}
}
创建storage引用 VS 直接状态变量赋值
创建storage引用后,就不能再通过赋值指向memory引用了
- 4.3 内存
memory
和存储storage
之间的赋值行为
尽量避免在 memory 和 storage 之间不必要的数据复制,因为这会消耗大量Gas
使用 storage 引用来直接操作存储数据,这是最Gas高效的方式
5. 存储位置对比总结
transient关键字只是提案,仅作了解即可
五、 变量的类型
1. 变量类型的整体概览
2. 值类型
- 布尔型
bool public isActive = true;
bool public isCompleted = false;function toggle() public {isActive = !isActive;
}function logicalOperations(bool a, bool b) public pure returns (bool, bool, bool) {return (a && b, a || b, !a);
}
- 整数
// 有符号整型
int8 public smallInt = -128;
int256 public largeInt = 1000;// 无符号整型
uint8 public smallUint = 255;
uint256 public largeUint = 1000000;function arithmeticOperations(uint a, uint b) public pure returns (uint, uint, uint, uint) {return (a + b, a - b, a * b, a / b);
}function bitOperations(uint a, uint b) public pure returns (uint, uint, uint) {return (a & b, a | b, a ^ b); // 与、或、异或
}
int 就是 int256
uint 就是 uint256
获取整型的最小值和最大值
注意整数可能会发生溢出
contract BestPractices {using SafeMath for uint256; // 对于 Solidity < 0.8// 使用合适的类型存储时间uint32 public constant MAX_UINT32 = 2**32 - 1;uint32 public creationTime;// 代币余额使用 uint256mapping(address => uint256) public balances;// 计数器使用合适的范围uint16 public userCount; // 假设最多 65,535 用户constructor() {creationTime = uint32(block.timestamp); // 时间戳适合用 uint32}// 安全的数学运算function safeTransfer(address to, uint256 amount) public {require(balances[msg.sender] >= amount, "Insufficient balance");require(to != address(0), "Invalid recipient");balances[msg.sender] -= amount;balances[to] += amount;}// 处理除法的精度function calculateShare(uint256 total, uint256 percentage) public pure returns (uint256) {require(percentage <= 100, "Invalid percentage");return (total * percentage) / 100;}
}
- 地址
address public owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
address payable public payableAddress = payable(owner);function addressOperations() public view returns (uint, address) {return (payableAddress.balance, address(this));
}function transfer() public payable {payableAddress.transfer(msg.value);
}
address payable表示可支付地址,可以调用transfer和send
特性 | transfer | send | call |
---|---|---|---|
转账资产 | 原生 ETH | 原生 ETH | 原生 ETH |
Gas 限制 | 固定 2300 | 固定 2300 | 转发所有剩余 Gas(可自定义) |
错误处理 | 失败则回滚 | 返回 false | 返回 (bool, bytes) |
安全性 | 高(防重入) | 高(防重入) | 低(有重入风险),需手动防护 |
推荐度 | 推荐 | 不推荐 | 推荐(但需加防护) |
Q: 为什么要用payable做区分?
A: 为了明确表示地址是否具备接收以太币的逻辑,避免开发者误将以太币转入无法处理的合约地址导致资产锁定
特性维度 | 外部账户 | 合约账户 |
---|---|---|
英文全称 | Externally Owned Account | Contract Account |
简称 | EOA | CA |
创建者 | 用户(通过钱包) | 一个 EOA 或另一个 CA(通过部署合约的交易) |
控制方式 | 私钥 | 合约代码 |
能否主动发起交易 | 可以(签名并广播交易) | 不可以。只能通过接收到的交易或消息调用来被动执行代码。 |
是否有代码 | 否 | 是,包含编译后的智能合约字节码 |
是否有存储空间 | 否 | 是,拥有独立的存储(storage),可以持久化存储状态变量 |
费用 | 需要 ETH 支付 Gas 费 | 需要 ETH 支付 Gas 费(由其创建者或调用者间接支付) |
通俗比喻 | 个人:可以主动做事,用私钥签名代表自己的意志。 | 自动售货机:其行为完全由预先设定的代码(规则)决定。你不能让它做规则之外的事,它自己也不会动,只有当你(EOA)向它发起交互时,它才按照规则运行。 |
- 枚举
Solidity中的枚举本质上就是uint8类型
enum Status { Pending, Approved, Rejected }
// 实际上相当于:
// uint8 constant Pending = 0;
// uint8 constant Approved = 1;
// uint8 constant Rejected = 2;
Status public currentStatus = Status.Pending;function setStatus(Status _status) public {currentStatus = _status;
}function getStatus() public view returns (Status) {return currentStatus;
}// 枚举与uint8的互操作
function enumUint8Interop() public pure {// 从 uint8 创建枚举Status statusFromUint8 = Status(1); // Status.Approved// 转换为 uint8uint8 asUint8 = uint8(Status.Rejected); // 2// 枚举值范围检查require(asUint8 <= 2, "Enum value within range");
}
- 自定义类型
只有值类型可以支持 自定义类型
验证type Duration is string;编译报错:The underlying type of the user defined value type "Duration" is not a value type.
3. 引用类型
- 数组
a、动态数组 VS 固定长度数组
即便是动态数组,如果数据位置是memory,也不能进行push/pop操作
Member "push" is not available in uint256[] memory outside of storage.
b、对数组的操作
1️⃣length属性获取长度 :使用 array.length
2️⃣pop操作:从数组末尾删除元素
Q: 对一个已初始化但无元素的动态数组进行pop操作,会怎样呢?
A: 交易回滚,抛出panic异常(错误码0x32)
3️⃣push(value): 在数组末尾添加一个给定的元素,没有返回值
4️⃣push(): 添加新的零初始化元素到数组末尾
push() 的用法示例:
5️⃣数组切片,仅支持calldata, 主要用于bytes
c、Solidity 的类型系统和数组长度推断规则
我在solidity的智能合约函数体内声明了一个变量:uint[] memory myArr = [7,8,9];Remix给我红色提示:Type uint8[3] memory is not implicitly convertible to expected type uint256[] memory.
[7,8,9] 被推断为 uint8[3] 类型,调整方案:
uint[] memory myArr = new uint;
myArr = [uint(7), uint(8), uint(9)];
d、数组操作中的gas问题
删除数组中指定索引位置的元素
最佳实践: 先把最后一个元素覆盖到指定索引位置,然后把最后一个元素pop掉
如果遍历查找,Gas效率会较低。
e、bytes、string是一种特殊的数组
存储优化:紧密打包,节省Gas
特殊编码:长度字段包含紧凑存储标志
内置函数:bytes有专门的成员函数
类型限制:string不可直接索引访问
Gas效率:相比普通数组更节省Gas
专用场景:专门为字节数据和文本数据优化
生成的getter函数:在作为public的状态变量时,生成的getter函数不带参数,而其他数组是带参数的
1️⃣string没有长度和下标
2️⃣将string赋值为中文字符串
Solidity默认字符串字面量仅支持ASCII字符集,直接使用中文等Unicode字符会触发编译错误
从Solidity 0.8.0版本开始,引入了unicode前缀来支持多语言字符。该语法要求字符串必须显式声明为unicode类型,否则编译器无法正确处理非ASCII编码。
正确的写法: string memory country = unicode"中国";
3️⃣字符串拼接方法对比
contract Comparison {using Strings for uint256;function compareMethods() public pure returns (string[3] memory) {string memory a = "Hello";string memory b = "World";// 方法1: abi.encodePacked (所有版本),还支持所有值类型、动态类型string memory result1 = string(abi.encodePacked(a, " ", b));// 方法2: bytes.concat (0.8.4+)string memory result2 = string(bytes.concat(bytes(a), " ", bytes(b)));// 方法3: string.concat (0.8.12+) - 最简洁!string memory result3 = string.concat(a, " ", b);return [result1, result2, result3];// 所有结果都是: "Hello World"}// Gas 消耗比较function gasComparison() public pure {string memory str1 = "Test";string memory str2 = "String";// 三种方法的Gas消耗大致相当// string.concat 通常是最易读的选择string(abi.encodePacked(str1, str2));string(bytes.concat(bytes(str1), bytes(str2)));string.concat(str1, str2);}
}
当然,也可以用循环方式手动拼接
4️⃣理解十六进制
- 结构体
4. 映射类型
1️⃣基本用法
// 地址到余额的映射mapping(address => uint256) public balances;// 地址到布尔值的映射(常用于白名单)mapping(address => bool) public isWhitelisted;// 用户ID到地址的映射mapping(uint256 => address) public idToAddress;
2️⃣嵌套映射
mapping(address => UserInfo) public users;mapping(uint256 => mapping(address => uint256)) public poolDeposits; // 池ID -> (用户 -> 存款金额)mapping(uint256 => address[]) public categoryMembers; // 分类ID -> 成员地址列表
3️⃣最佳实践
4️⃣结构体和映射组合使用,两种做法均有使用
- 把结构体作为映射的value
- 把映射作为结构体的成员属性