3. Solidity语言核心特性:数据类型与存储布局、函数可见性与修饰符、事件与日志、继承与库合约
好,咱们直接进入正题。Solidity 这门语言,说难不难,说简单也不简单。很多新手写合约,漏洞百出,其实根源就在于没吃透这几个核心特性。今天我就带你把这些硬骨头啃下来。
3.1 数据类型与存储布局
先聊聊数据类型。Solidity 是静态类型语言,每个变量都得明确类型。我个人习惯把类型分成两类:值类型和引用类型。
值类型包括:
bool:布尔值,true/falseint/uint:有符号/无符号整数,从8位到256位address:以太坊地址,20字节bytes1~bytes32:定长字节数组enum:枚举类型
引用类型包括:
string:动态字符串bytes:动态字节数组array:数组struct:结构体mapping:映射表
这里有个关键点——存储位置。Solidity 有三种存储位置:storage、memory 和 calldata。我见过太多人在这上面栽跟头了。
核心区别:
storage:永久存储在区块链上,修改会写进状态memory:临时存储,函数执行完就释放calldata:只读的临时存储,用于外部函数参数
举个例子你就明白了:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageDemo {
uint[] public arr; // 默认是 storage
function f(uint[] memory _input) public {
uint[] storage localArr = arr; // 引用 storage,修改会影响全局
localArr.push(1);
uint[] memory memArr = _input; // memory 副本,修改不影响原数据
memArr[0] = 999;
}
}
我曾经审计过一个项目,开发者在函数内部把 storage 变量赋值给 memory 变量,结果改了副本以为改了原数据,导致状态一直不对。嗯,这种 bug 排查起来特别头疼。
3.2 函数可见性与修饰符
函数的可见性,说白了就是「谁能调用这个函数」。Solidity 提供了四种:
| 可见性 | 外部调用 | 内部调用 | 子合约调用 |
|---|---|---|---|
public |
✅ | ✅ | ✅ |
external |
✅ | ❌(需用 this) | ✅ |
internal |
❌ | ✅ | ✅ |
private |
❌ | ✅ | ❌ |
你想想看,external 和 public 有什么区别?external 函数的参数可以直接用 calldata,省 gas。我一般建议:如果函数只给外部调用,就用 external。
再来说说修饰符。修饰符是 Solidity 里很强大的特性,可以用来做权限控制、重入锁等。
contract ModifierDemo {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // 继续执行原函数
}
function sensitiveAction() public onlyOwner {
// 只有 owner 能调用
}
}
这里有个小技巧:_ 的位置决定了原函数代码插入的位置。你可以把 _ 放在修饰符的开头、中间或结尾,实现不同的逻辑。
我的经验:写修饰符时,记得把 _ 放在 require 之后。否则 require 失败时,状态可能已经被修改了。我曾经审计过一个合约,修饰符里先执行了转账再检查权限,结果...你懂的。
3.3 事件与日志
事件是 Solidity 里和前端交互的主要方式。说白了,合约里发生什么事,通过事件告诉外部世界。
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 amount) public {
// 转账逻辑...
emit Transfer(msg.sender, to, amount);
}
这里要注意 indexed 关键字。被 indexed 的参数会被索引,方便前端快速检索。但最多只能有 3 个 indexed 参数。
事件的数据存储在交易收据的 logs 字段里。你想想看,这比直接存到 storage 里便宜多了。所以很多项目会用事件来记录历史数据,而不是存到状态变量里。
避坑指南:我曾经审计过一个 DeFi 项目,他们把用户的所有操作都记录在 storage 里。结果 gas 费高得离谱,用户根本用不起。后来改成用事件记录,成本降了 90%。记住:能用事件就别用 storage。
3.4 继承与库合约
Solidity 支持多重继承,但它的继承机制和传统 OOP 语言不太一样。核心是 C3 线性化 算法,用来解决菱形继承问题。
contract A {
function foo() public virtual pure returns (string memory) {
return "A";
}
}
contract B is A {
function foo() public virtual override pure returns (string memory) {
return "B";
}
}
contract C is A {
function foo() public virtual override pure returns (string memory) {
return "C";
}
}
contract D is B, C {
function foo() public override(B, C) pure returns (string memory) {
return super.foo(); // 调用 C 的 foo()
}
}
这里 super.foo() 调用的是哪个?答案是 C。因为 Solidity 的继承顺序是从右到左,从最派生到最基类。D 继承 B 和 C,C 在右边,所以优先调用 C。
再说说库合约。库合约是用 library 关键字定义的,它有几个特点:
- 不能有状态变量
- 不能继承或被继承
- 不能接收以太币
- 函数默认是
internal
库合约最常见的用法是 using for 语法,给某个类型添加方法:
library Math {
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
}
contract Test {
using Math for uint;
function testAdd() public pure returns (uint) {
uint x = 5;
return x.add(3); // 相当于 Math.add(5, 3)
}
}
我个人特别喜欢用库合约来封装通用的数学运算、地址校验等逻辑。这样主合约代码更干净,也更容易测试。
核心总结:
- 数据类型要分清值类型和引用类型,存储位置要选对
- 函数可见性按需选择,external 比 public 省 gas
- 事件是低成本的数据记录方式,能不用 storage 就别用
- 继承用 C3 线性化,库合约用 using for 更优雅
好了,这一章的内容就到这里。这些核心特性是写安全合约的基石,建议你多写多练,把每个细节都吃透。