一、抽象合约 (Abstract Contract)
1.1 定义与特点
// 使用 abstract 关键字声明
abstract contract AbstractExample {
// 抽象函数:只有声明,没有实现体,以分号结尾
function abstractFunction() public virtual returns(uint256);
// 可以有已实现的函数
function concreteFunction() public pure returns(uint256) {
return 42;
}
// 可以有状态变量
uint256 public data;
// 可以有构造函数
constructor(uint256 _data) {
data = _data;
}
}
1.2 主要特点
- 包含未实现的函数:至少有一个函数只有声明没有实现
- 不能被直接实例化:需要被子合约完全实现后才能部署
- 可以包含状态变量:可以有存储变量
- 可以有构造函数:可以初始化状态
- 可以继承其他合约:可以多重继承
1.3 使用场景
// 场景1:定义标准模板
abstract contract ERC20Base {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function transfer(address to, uint256 value) public virtual returns(bool);
function approve(address spender, uint256 value) public virtual returns(bool);
function transferFrom(address from, address to, uint256 value) public virtual returns(bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// 场景2:部分实现框架
abstract contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Invalid address");
owner = newOwner;
}
}
二、接口合约 (Interface)
2.1 定义与特点
// 使用 interface 关键字声明
interface IERC20 {
// 接口函数:只有声明,自动 virtual
function totalSupply() external view returns(uint256);
function balanceOf(address account) external view returns(uint256);
function transfer(address recipient, uint256 amount) external returns(bool);
// 不能有函数体
// function test() external { } // × 错误
// 不能有状态变量
// uint256 public data; // × 错误
// 不能有构造函数
// constructor() {} // × 错误
// 可以有事件定义
event Transfer(address indexed from, address indexed to, uint256 value);
}
2.2 主要特点
- 纯函数声明:只能包含函数签名,没有实现
- 自动 virtual:所有函数自动是 virtual
- 不能有状态变量:不能定义任何存储变量
- 不能有构造函数:不能初始化
- 函数必须 external:所有函数必须是 external 可见性
- 不能继承其他合约:只能继承其他接口
- 可以定义事件、结构体、枚举
2.3 使用场景
// 场景1:定义标准接口
interface IERC721 {
function balanceOf(address owner) external view returns(uint256 balance);
function ownerOf(uint256 tokenId) external view returns(address owner);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns(address operator);
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
}
// 场景2:合约间交互
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns(uint256[] memory amounts);
function getAmountsOut(
uint256 amountIn,
address[] calldata path
) external view returns(uint256[] memory amounts);
}
三、对比表格
| 特性 | 抽象合约 | 接口合约 |
|---|---|---|
| 关键字 | abstract contract |
interface |
| 函数实现 | 可以有未实现和已实现的函数 | 只能有函数声明 |
| 状态变量 | ✅ 可以有 | ❌ 不能有 |
| 构造函数 | ✅ 可以有 | ❌ 不能有 |
| 函数可见性 | 任意 (public, internal等) | 只能是 external |
| 继承 | 可以继承合约和接口 | 只能继承接口 |
| virtual | 需要显式声明 | 自动 virtual |
| 使用场景 | 代码复用、模板模式 | 标准定义、合约交互 |
四、实际应用示例
4.1 完整示例:代币系统
// 接口定义标准
interface IERC20 {
function totalSupply() external view returns(uint256);
function balanceOf(address account) external view returns(uint256);
function transfer(address recipient, uint256 amount) external returns(bool);
function allowance(address owner, address spender) external view returns(uint256);
function approve(address spender, uint256 amount) external returns(bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns(bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// 抽象合约提供部分实现
abstract contract ERC20 is IERC20 {
string public name;
string public symbol;
uint8 public decimals;
uint256 public override totalSupply;
mapping(address => uint256) public override balanceOf;
mapping(address => mapping(address => uint256)) public override allowance;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
}
function transfer(address to, uint256 value) external override returns(bool) {
require(balanceOf[msg.sender] >= value, "Insufficient balance");
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
// 其他函数实现...
}
// 具体合约
contract MyToken is ERC20 {
constructor() ERC20("My Token", "MTK", 18) {
totalSupply = 1000000 * 10**18;
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
}
4.2 工厂模式示例
// 产品接口
interface IProduct {
function use() external returns(string memory);
}
// 抽象工厂
abstract contract Factory {
// 抽象创建方法
function createProduct() public virtual returns(IProduct);
// 已实现的方法
function createAndUse() public returns(string memory) {
IProduct product = createProduct();
return product.use();
}
}
// 具体产品
contract ProductA is IProduct {
function use() external pure override returns(string memory) {
return "Product A used";
}
}
// 具体工厂
contract FactoryA is Factory {
function createProduct() public override returns(IProduct) {
return new ProductA();
}
}
五、最佳实践
5.1 命名约定
// 接口以 I 开头
interface IMyContract { }
// 抽象合约描述其抽象特性
abstract contract BaseContract { }
// 实现合约使用具体名称
contract MyImplementation { }
5.2 何时使用抽象合约 vs 接口
// 使用接口的场景:
// 1. 定义外部合约需要遵循的标准
// 2. 只需要函数声明,不需要共享代码
// 3. 用于类型检查和合约交互
// 使用抽象合约的场景:
// 1. 需要共享代码和状态变量
// 2. 提供部分实现,子合约完成剩余
// 3. 需要构造函数初始化
// 4. 复杂的继承结构
5.3 多重继承示例
interface IA {
function funcA() external;
}
interface IB {
function funcB() external;
}
abstract contract Base {
uint256 public value;
function baseFunc() public pure returns(string memory) {
return "base";
}
}
contract MyContract is Base, IA, IB {
// 必须实现所有接口函数
function funcA() external override { }
function funcB() external override { }
}
六、注意事项
- 抽象合约可以继承接口,但必须实现所有接口函数
- 接口函数的参数和返回值必须与实现完全匹配
- override 关键字在多重继承中很重要
- 抽象合约可以有未实现的函数,但必须标记为 abstract
- 接口更轻量,gas 消耗更少
通过合理使用抽象合约和接口,可以构建出模块化、可维护、可扩展的智能合约系统。