Appearance
Solidity
Solidity 简介
- Solidity 是一种面向智能合约的高级编程语言,专门用于在以太坊区块链上编写去中心化应用(DApps)。它语法类似 JavaScript 和 C++,支持继承、库和复杂数据类型,开发者可以用它创建自动执行的数字协议(智能合约),实现代币发行、DeFi、NFT 等区块链功能。Solidity 代码最终会编译为 EVM(以太坊虚拟机)字节码运行,具有安全性优先、Gas 成本敏感等特性,是当前最主流的区块链开发语言之一。
Solidity 基础语法
Solidity 是一种静态类型语言,语法类似于 JavaScript 和 C++。以下是 Solidity 的基础语法要点:
合约结构
js
// SPDX-License-Identifier: MIT // 指定许可证
pragma solidity ^0.8.0; // 声明编译器版本
contract MyContract { // 合约定义
// 状态变量
uint public myNumber;
// 构造函数
constructor(uint initialNumber) {
myNumber = initialNumber;
}
// 函数
function setNumber(uint newNumber) public {
myNumber = newNumber;
}
function getNumber() public view returns (uint) {
return myNumber;
}
}
Solidity 数据类型
1. 基础值类型
布尔值 (bool)
js
bool isActive = true; // 仅存储 true/false
- 逻辑运算:
!a
,a && b
,a || b
- 比较运算:
==
,!=
整数类型
js
int32 negative = -1; // 有符号整数
uint256 large = 2**256 - 1; // 无符号整数
- 范围:
intX
: -2^(X-1) 到 2^(X-1)-1uintX
: 0 到 2^X-1
- 最佳实践:优先使用
uint256
/int256
(EVM 原生优化)
地址类型
js
address user = 0x1234567890123456789012345678901234567890;
address payable recipient = payable(user); // 可支付地址
- 关键操作:js
// 调用方给recipient转账 recipient.transfer(1 ether); // 转账(自动处理失败) bool success = recipient.send(1 ether); // 转账(需检查返回值)
字节类型
js
bytes1 singleByte = 0x01; // 固定大小
bytes dynamicBytes; // 动态大小
- 应用场景:
bytes32
:存储哈希值bytes
:处理原始二进制数据
2. 引用类型
数组
js
uint[3] fixedArr; // 固定长度
uint[] dynamicArr; // 可变长度
string[] stringArr;
- 操作:solidity
dynamicArr.push(10); // 添加元素 dynamicArr.pop(); // 移除末尾元素
结构体
js
struct NFT {
address owner;
uint256 id;
string metadataURI;
}
- 使用方式:js
NFT memory newNFT = NFT(msg.sender, 1, "ipfs://...");
映射
js
mapping(address => uint) balances;
mapping(uint => string) idToName;
- 特性:
- 所有可能的键默认映射到零值
- 不支持遍历(需额外维护键列表)
3. 特殊类型
枚举
js
enum TradeStatus { Created, Filled, Cancelled }
TradeStatus status = TradeStatus.Created;
函数类型
js
function(uint) external callbackFunc;
4. 数据存储位置
位置 | 存储周期 | 修改成本 | 典型用途 |
---|---|---|---|
storage | 永久 | 高 | 合约状态变量 |
memory | 临时 | 低 | 函数内部临时变量 |
calldata | 临时 | 最低 | 外部函数参数(只读) |
5. 类型转换技巧
安全转换
js
uint256 big = 100;
uint64 small = uint64(big); // 需确保值在目标范围内
地址处理
js
// 地址与整型互转
address addr = address(uint160(12345));
uint160 asInt = uint160(addr);
// 支付地址转换
address payable payAddr = payable(regularAddr);
6. 实用全局函数
js
// 类型极值
type(uint8).max; // 255
// 字节操作
bytes.concat(bytes1(0x01), bytes1(0x02));
// 字符串拼接
string.concat("Hello", " ", "World");
函数
1. 函数基础
Solidity 函数是智能合约中可执行的代码块,用于封装特定的功能逻辑。
1.1 基本语法
js
function functionName(parameterList) visibilityModifier stateMutability returns(returnType) {
// 函数体
}
1.2 示例
js
pragma solidity ^0.8.0;
contract SimpleFunction {
// 简单函数
function add(uint a, uint b) public pure returns(uint) {
return a + b;
}
// 无参数函数
function getMessage() public pure returns(string memory) {
return "Hello, Solidity!";
}
}
2. 函数可见性
Solidity 提供了四种可见性修饰符:
public
- 可以从任何地方访问(包括外部合约和交易)private
- 只能在当前合约内部访问internal
- 只能在当前合约和继承合约中访问external
- 只能从合约外部访问(内部this.xx调用)
示例
js
contract VisibilityExample {
function publicFunc() public pure returns(string memory) {
return "Public function";
}
function privateFunc() private pure returns(string memory) {
return "Private function";
}
function internalFunc() internal pure returns(string memory) {
return "Internal function";
}
function externalFunc() external pure returns(string memory) {
return "External function";
}
function testAccess() public pure {
publicFunc(); // 可以访问
privateFunc(); // 可以访问
internalFunc(); // 可以访问
// externalFunc(); // 不能这样直接调用,会报错
this.externalFunc(); //需要通过this访问
}
}
3. 状态可变性
函数可以声明它们如何与区块链状态交互:
pure
- 不读取也不修改状态view
- 只读取状态但不修改- 不指定 - 可以读取和修改状态
payable
- 可以接收以太币
示例
js
contract MutabilityExample {
uint public value;
// pure函数
function add(uint a, uint b) public pure returns(uint) {
return a + b;
}
// view函数
function getValue() public view returns(uint) {
return value;
}
// 修改状态的函数
function setValue(uint _value) public {
value = _value;
}
// payable函数
function deposit() public payable {
value += msg.value;
}
}
4. 函数参数和返回值
4.1 参数
函数可以接受多个参数,每个参数需要指定类型和名称。
js
function registerUser(string memory name, uint age, address wallet) public {
// 函数体
}
4.2 返回值
函数可以返回多个值,通过 returns
关键字声明返回类型。
js
function calculate(uint a, uint b) public pure returns(uint sum, uint product) {
sum = a + b;
product = a * b;
// 也可以直接 return (a+b, a*b);
}
5. 特殊函数
5.1 构造函数
在合约部署时只执行一次的特殊函数。
js
contract ConstructorExample {
address public owner;
uint public creationTime;
constructor() {
owner = msg.sender;
creationTime = block.timestamp;
}
}
5.2 回退函数
当调用合约中不存在的函数时执行,或者向合约发送以太币但没有指定接收函数时执行。
js
contract FallbackExample {
event Log(string message);
// 接收以太币的回退函数
receive() external payable {
emit Log("Received Ether");
}
// 通用回退函数
fallback() external {
emit Log("Fallback called");
}
}
6. 函数修饰器
修饰器可以用来修改函数的行为,常用于权限检查、输入验证等。
js
contract ModifierExample {
address public owner;
bool public paused;
constructor() {
owner = msg.sender;
}
// 自定义修饰器
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_; // 继续执行函数体
}
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
function pause() public onlyOwner {
paused = true;
}
function unpause() public onlyOwner {
paused = false;
}
function sensitiveAction() public whenNotPaused {
// 重要操作
}
}
7. 函数重载
Solidity 支持函数重载,即同一作用域内可以有多个同名函数,但参数类型或数量不同。
js
contract OverloadingExample {
function getSum(uint a, uint b) public pure returns(uint) {
return a + b;
}
function getSum(uint a, uint b, uint c) public pure returns(uint) {
return a + b + c;
}
function getSum(string memory a, string memory b) public pure returns(string memory) {
return string(abi.encodePacked(a, b));
}
}
8. 函数调用
8.1 内部调用
直接使用函数名调用同一合约内的其他函数。
js
function internalCallExample() public pure returns(uint) {
return add(1, 2);
}
function add(uint a, uint b) private pure returns(uint) {
return a + b;
}
8.2 外部调用
调用其他合约的函数需要合约实例和函数签名。
js
contract Caller {
function callOtherContract(address _contractAddr) public returns(uint) {
OtherContract other = OtherContract(_contractAddr);
return other.getNumber();
}
}
contract OtherContract {
uint public number = 42;
function getNumber() public view returns(uint) {
return number;
}
}
10. 常见模式
10.1 检查-效果-交互模式
js
function withdraw(uint amount) public {
// 检查
require(balances[msg.sender] >= amount, "Insufficient balance");
// 效果
balances[msg.sender] -= amount;
// 交互
payable(msg.sender).transfer(amount);
}
10.2 紧急停止模式
js
bool public stopped = false;
modifier stopInEmergency {
require(!stopped, "Contract is stopped");
_;
}
function emergencyStop() public onlyOwner {
stopped = true;
}
function safeAction() public stopInEmergency {
// 安全操作
}
数据存储位置storage|memory|calldata
Solidity 有三种主要的数据存储位置,理解它们的区别对编写高效、安全的智能合约至关重要。
1. 存储位置概述
- 引用类型才能被修饰(storage|memory|calldata)
位置 | 持久性 | 修改性 | Gas成本 | 使用场景 |
---|---|---|---|---|
storage | 永久存储 | 可修改 | 高 | 合约状态变量 |
memory | 临时内存 | 可修改 | 中 | 函数内部临时变量 |
calldata | 临时只读 | 不可修改 | 低 | 函数参数(外部调用) |
2. storage(存储)
特点
- 永久存储在区块链上
- 相当于合约的"硬盘存储"
- 读写操作消耗大量gas
- 所有合约状态变量默认存储在storage
示例
js
contract StorageExample {
uint public count; // 自动存储在storage
function updateCount(uint newCount) public {
count = newCount; // 修改storage变量
}
}
关键点
- 每次修改都会永久记录在区块链上
- 存储槽优化可以节省gas(连续小变量会打包到一个存储槽)
3. memory(内存)
特点
- 临时存储,仅在函数执行期间存在
- 函数调用结束后被清除
- 比storage操作便宜很多
- 用于函数内的局部变量
示例
js
function calculate(uint a, uint b) public pure returns (uint) {
uint result = a + b; // result存储在memory
return result;
}
特殊用法:数组和结构体
js
function processArray(uint[] memory arr) public {
// 必须显式指定memory
uint[] memory temp = new uint[](arr.length);
// 操作memory数组...
}
4. calldata(调用数据)
特点
- 只读的临时存储
- 存储函数参数的原始调用数据
- 最省gas的外部调用参数存储方式
- 仅适用于external函数的参数
示例
js
function externalFunc(uint[] calldata data) external {
// 只能读取data,不能修改
uint first = data[0];
// data[0] = 1; // 会报错!
}
最佳实践
- 对于external函数,优先使用calldata而非memory
- 节省gas(避免从calldata到memory的拷贝)
5. 数据位置规则
5.1 默认规则
- 函数参数:
- external函数:默认calldata
- 其他函数:默认memory
- 引用类型局部变量:必须显式指定memory或storage
- 返回值:默认memory
5.2 赋值行为
- storage → memory:创建独立副本
- memory → memory:创建引用(修改一个会影响另一个)
- storage → storage:创建引用
- 其他组合通常需要显式转换
6. 高级用法
6.1 storage指针
js
contract PointerExample {
struct User {
uint id;
string name;
}
User[] public users;
function updateUser(uint index, string memory newName) public {
User storage user = users[index]; // storage指针
user.name = newName; // 直接修改原数据
}
}
6.2 memory与calldata性能对比
js
// 更高效(calldata)
function processData(uint[] calldata data) external {
// 直接读取调用数据
}
// 较低效(memory)
function processData(uint[] memory data) public {
// 需要先将calldata拷贝到memory
}
7. 常见错误
错误1:未指定数据位置
js
function wrongFunc(uint[] arr) public {
// 错误!引用类型必须指定memory或storage
}
错误2:误用storage引用
js
function dangerousRef() public {
uint[] storage arr; // 未初始化storage指针
arr.push(1); // 会导致严重问题!
}
错误3:错误地修改calldata
js
function modifyCalldata(uint[] calldata data) external {
data[0] = 1; // 错误!calldata不可修改
}
constant
和 immutable
1. constant
(常量)
特点:
- 值必须在 编译时 确定(硬编码在合约中)。
- 不能通过构造函数或任何函数修改。
- 存储在合约的 字节码 中,不占用存储插槽(
storage
),因此 Gas 成本更低。 - 适用于永远不会改变的固定值(如数学常数、固定配置)。
语法:
jsuint256 public constant MY_CONSTANT = 123; address public constant DEFAULT_ADDRESS = 0x123...;
2. immutable
(不可变量)
特点:
- 值可以在 构造函数 中设置一次,之后不能修改。
- 存储在合约的 代码 中(类似
constant
),但允许在部署时动态赋值。 - 比
constant
更灵活,但仍比普通状态变量更节省 Gas(因为不占用storage
)。 - 适用于部署时确定但之后不再更改的值(如合约的创建者地址、初始化参数)。
语法:
jsuint256 public immutable maxSupply; address public immutable owner; constructor(uint256 _maxSupply) { maxSupply = _maxSupply; // 只能在构造函数赋值 owner = msg.sender; }
modifier
(修饰器)
在 Solidity 中,modifier
(修饰器)是一种特殊的函数,用于 在函数执行前或执行后添加检查或修改逻辑,类似于其他编程语言中的 装饰器(Decorator) 或 AOP(面向切面编程)。
它通常用于:
- 权限控制(如
onlyOwner
) - 输入验证(如
nonReentrant
防重入) - 状态检查(如
whenNotPaused
)
定义修饰器
js
modifier minAmount(uint256 amount) {
require(msg.value >= amount, "Amount too low");
_;
}
function buyToken() public payable minAmount(1 ether) {
// 必须至少发送 1 ETH
// ...
}
_;
表示 原始函数的执行位置,可以放在 modifier
的开头、中间或结尾。
event
(事件)
在 Solidity 中,event
(事件)是一种 低成本的日志记录机制,允许智能合约在区块链上 存储特定的数据,供外部应用程序(如前端 DApp)监听和响应。
事件的主要用途:
- 记录重要状态变化(如转账、交易、所有权变更)。
- 节省 Gas 成本(比直接存储
storage
更便宜)。 - 提供链下可查询的数据(前端可以通过 Web3.js/ethers.js 监听事件)。
1. 基本语法
js
event Transfer(address indexed from, address indexed to, uint256 amount);
event
关键字声明一个事件。indexed
表示该参数可以被 高效过滤(最多 3 个indexed
参数)。
触发事件
js
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount); // 触发事件
}
emit
关键字用于触发事件。
2. 事件的特点
(1) 数据存储位置
- 事件数据存储在 交易日志(Logs) 中,而不是合约存储(
storage
),因此 Gas 成本更低。 - 适用于 不需要链上计算,但需要链下查询 的数据。
(2) indexed
参数的作用
indexed
参数允许 高效过滤,例如:javascript// 前端监听特定地址的转账 contract.on("Transfer", { from: "0x123..." }, (event) => { console.log(event.args.to, event.args.amount); });
- 最多 3 个参数 可以标记为
indexed
。
(3) 不可修改
- 事件一旦触发,无法修改或删除(区块链不可篡改特性)。
3. 常见应用场景
(1) 记录代币转账(ERC20 标准)
js
event Transfer(address indexed from, address indexed to, uint256 value);
function _transfer(address from, address to, uint256 amount) internal {
balances[from] -= amount;
balances[to] += amount;
emit Transfer(from, to, amount); // 记录转账
}
(2) 记录用户操作(如存款、取款)
js
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
function deposit() public payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value); // 记录存款
}
function withdraw(uint256 amount) public {
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdraw(msg.sender, amount); // 记录取款
}
(3) 治理投票记录
js
event VoteCast(address indexed voter, uint256 proposalId, bool support);
function vote(uint256 proposalId, bool support) public {
votes[msg.sender][proposalId] = support;
emit VoteCast(msg.sender, proposalId, support); // 记录投票
}
4. 前端如何监听事件?
使用 ethers.js
javascript
const provider = new ethers.providers.Web3Provider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);
// 监听 Transfer 事件
contract.on("Transfer", (from, to, amount, event) => {
console.log(`${from} sent ${amount} tokens to ${to}`);
});
// 过滤特定地址的转账
contract.on("Transfer", { from: "0x123..." }, (event) => {
console.log("Filtered transfer:", event.args.to, event.args.amount);
});
使用 Web3.js
javascript
const web3 = new Web3(window.ethereum);
const contract = new web3.eth.Contract(abi, contractAddress);
// 监听所有 Transfer 事件
contract.events.Transfer({})
.on("data", (event) => {
console.log(event.returnValues);
});
继承
- Solidity 支持多重继承,包括多级继承和多重继承。合约可以通过
is
关键字继承其他合约。
基本语法
js
contract Parent {
// 父合约代码
}
contract Child is Parent {
// 子合约代码
}
继承类型
1. 单继承
一个合约继承另一个合约。
js
contract A {
function foo() public pure returns (string memory) {
return "A";
}
}
contract B is A {
// 继承A的所有功能
}
2. 多级继承
继承链有多个层级。
js
contract Grandparent {
// 祖父合约
}
contract Parent is Grandparent {
// 父合约
}
contract Child is Parent {
// 子合约
}
3. 多重继承
一个合约继承多个合约。
js
contract A {
function foo() public pure returns (string memory) {
return "A";
}
}
contract B {
function bar() public pure returns (string memory) {
return "B";
}
}
contract C is A, B {
// 继承A和B的功能
}
继承规则
1. 构造函数执行顺序
构造函数按照从最基础到最派生的顺序执行。
js
contract A {
constructor() {
// 最先执行
}
}
contract B is A {
constructor() A() {
// 然后执行
}
}
2. 函数重写
子合约可以重写父合约的函数,需要使用 override
关键字。
js
contract A {
function foo() public virtual pure returns (string memory) {
return "A";
}
}
contract B is A {
function foo() public pure override returns (string memory) {
return "B";
}
}
3. 多重继承中的同名函数
当从多个父合约继承同名函数时,必须明确重写该函数。
js
contract A {
function foo() public virtual pure returns (string memory) {
return "A";
}
}
contract B {
function foo() public virtual pure returns (string memory) {
return "B";
}
}
contract C is A, B {
function foo() public pure override(A, B) returns (string memory) {
return "C";
}
}
抽象合约
抽象合约包含至少一个未实现的函数(抽象函数)。
js
abstract contract Abstract {
function foo() public virtual returns (uint);
}
contract Concrete is Abstract {
function foo() public pure override returns (uint) {
return 1;
}
}