Solidity 第一周(上):合约基础、数据存储与函数核心
大家好!如果你正准备踏入Web3和智能合约开发的世界,那么这篇文章就是为你量身打造的。最近我系统性地学习了一套由浅入深的Solidity教程,从一个简单的计数器开始,逐步构建出功能丰富的去中心化应用。
在第一周**《上篇》**中,我将带你回顾整个学习路径的基础部分,将ClickCounter、SaveMyName和PollStation等合约中的核心知识点进行系统性地梳理和总结。我们的目标是:为你构建一个坚不可摧的Solidity知识地基。
准备好了吗?让我们开始吧!
一、 智能合约的“骨架”:许可证、版本与合约结构
在我们写下任何逻辑之前,一个Solidity文件需要一个标准的“开场白”。这不仅是好的编码习惯,更是合约安全与兼容性的保证。
1. 许可证标识 (SPDX License Identifier)
// SPDX-License-Identifier: MIT
几乎所有教程的第一个合约 ClickCounter 都以这行注释开始。它的作用是声明代码的开源许可证。由于智能合约一旦部署,源码通常是公开可验证的,明确的许可证(如MIT)能告诉社区成员他们可以如何使用、修改和分发你的代码。
2. 编译器版本指令 (Pragma Directive)
pragma solidity ^0.8.0;
这行代码告诉编译器:“请使用0.8.0版本或任何不包含重大更新的更高版本(如0.8.1, 0.8.2)来编译我,但不要使用0.9.0或更高版本。” Solidity语言在快速迭代,pragma指令确保了你的代码不会在未来因为编译器的破坏性更新而出错。
3. 合约的定义
contract ClickCounter {
// 合约的所有代码都写在这里...
}
contract关键字是定义一个智能合约的入口。它就像是Java或C#中的class,将所有相关的状态变量和函数包裹在一个独立的单元里。
二、 区块链的“记忆”:状态变量与数据类型
智能合约的核心价值在于它能够在区块链上永久存储状态。这些状态由状态变量 (State Variables) 承载。
1. 基础数据类型:uint 与 string
在 ClickCounter 合约中,我们遇到了第一个状态变量:
// 存储一个256位的无符号整数
uint256 public counter;
uint256是Solidity中最常用的整数类型。关键字public非常神奇,它会自动为我们生成一个免费的、可供任何人调用的counter() getter函数,用于读取变量的值。
接着,在 SaveMyName 合约中,我们学会了存储文本:
// 存储字符串,默认可见性为 internal
string name;
string bio;
string类型用于处理文本数据。与uint不同,字符串在Solidity中是复杂类型,处理它们时需要特别注意存储位置(我们稍后会讲)。
2. 复杂数据结构:数组 Array 与映射 Mapping
当我们需要管理一组数据时,PollStation 合约向我们展示了两种强大的工具:
-
数组 (Array): 用于存储一个有序的、同类型的元素列表。
// 一个存储字符串的动态数组,用于存放候选人列表 string[] public candidateNames;数组非常适合需要遍历或按顺序访问元素的场景。我们可以使用
.push()方法向动态数组末尾添加新元素。 -
映射 (Mapping): 用于存储键值对,提供极其高效的查找。
// 一个映射,键是候选人名字(string),值是票数(uint256) mapping(string => uint256) public voteCount;映射就像其他语言中的哈希表或字典。它没有“长度”的概念,也不能被遍历,但可以通过键(Key)在O(1)时间内直接定位到值(Value)。这在记录用户余额、投票数等场景下非常高效。
三、 合约的“大脑”:函数、可见性与关键字
如果说状态变量是合约的“记忆”,那么函数就是合约的“大脑”,负责处理逻辑和与外部世界交互。
1. 函数的定义与可见性
一个典型的函数定义如下:
// 一个公共函数,任何人都可以调用
function click() public {
counter++;
}
public是函数的可见性修饰符之一,代表任何人(包括外部用户和其他合约)都可以调用。其他常见的可见性还有:
private: 只能在当前合约内部调用。internal: 只能在当前合约及继承它的子合约中调用。external: 只能从合约外部调用(不能在合约内部直接调用)。
2. 理解 view 与 pure:免费的读取操作
在 SaveMyName 合约中,我们遇到了一个特殊的关键字 view:
function retrieve() public view returns (string memory, string memory) {
return (name, bio);
}
view: 表明该函数只读取合约的状态,但不修改它。pure: 比view更严格,表明该函数既不读取也不修改合约状态(例如,只对输入参数进行计算)。
标记为view或pure的函数,如果从外部调用(例如通过Web3.js),不会产生交易,也不消耗Gas,因为它们不对区块链产生任何“写”操作。这是区块链经济模型的核心:写操作昂贵,读操作免费。
3. 理解 memory vs storage:数据的临时与永恒
处理 string 或 array 等复杂类型时,我们必须明确它们的存储位置。
function add(string memory _name, string memory _bio) public {
name = _name; // 将 memory 中的临时数据写入 storage
bio = _bio;
}
storage: 指向状态变量,数据被永久存储在区块链上。这是默认的存储位置,操作它会消耗大量Gas。memory: 临时存储,数据只在函数执行期间存在。函数参数、返回值和函数内部声明的复杂类型变量通常使用memory,它比storage便宜得多。
简单记忆:storage是硬盘,memory是内存。
四、 访问控制的“守卫”:msg.sender 与 modifier
在 Admin-Only 合约中,我们学习了如何限制某些函数只能由特定的人调用。
1. 全局变量 msg.sender
msg.sender是一个非常重要的全局变量,它代表当前函数调用者的地址。
address public owner;
constructor() {
// 在合约部署时,将部署者的地址设为 owner
owner = msg.sender;
}
constructor是一个特殊的函数,只在合约部署时执行一次。通过在这里设置owner,我们就确立了合约的“所有者”。
2. 可复用的权限检查:modifier
如果我们有很多函数都只允许owner调用,每次都写require(msg.sender == owner)会很繁琐。modifier就是为此而生的:
// 定义一个名为 onlyOwner 的修饰符
modifier onlyOwner() {
require(msg.sender == owner, "Access denied: Only the owner can perform this action");
_; // 如果检查通过,执行函数体
}
// 在函数上使用修饰符
function addTreasure(uint256 amount) public onlyOwner {
treasureAmount += amount;
}
modifier就像一个函数的前置守卫。_;是一个占位符,代表原始函数体的代码。如果require条件不满足,函数将在此处中断;如果满足,则继续执行。这让我们的访问控制代码变得极其干净和可复用。
到这里,我们已经系统性地梳理了Solidity最核心的基础知识。你现在应该理解:
- 一个标准Solidity合约的完整结构。
- 如何使用
uint,string,array,mapping等数据类型来存储链上状态。 - 函数是什么,以及
public,view,memory等关键字如何影响它们的行为和成本。 - 如何通过
msg.sender和modifier实现简单而强大的访问控制。