4、Solidity智能合约基础(上):Solidity版本与编译器、合约结构、状态变量与局部变量、基本数据类型(uint、address、string、bool)、映射(mapping)与数组(array)、构造函数与修饰符(modifier)、事件(event)的声明与触发

好,咱们正式开始写智能合约了。

这一章是 Solidity 的入门基础,说白了就是盖房子的砖头和水泥。你把这些基础搞扎实了,后面写复杂合约才不会翻车。我个人习惯是,每学一个新语言,先搞清楚它的「骨架」长什么样——也就是合约结构。

4.1 Solidity 版本与编译器

Solidity 这语言迭代挺快的。我记得我刚入行那会儿还在用 0.4.x,现在已经到 0.8.x 了。版本号不是随便写的,它直接决定了你能用什么语法、有什么安全机制。

每个 Solidity 源文件的第一行,几乎都是版本声明:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

这里有两个东西要注意:

  • SPDX 许可证:这是开源规范。不加的话,有些编译器会报警告。我一般用 MIT,省事。
  • pragma solidity:声明编译器版本。^0.8.18 表示兼容 0.8.18 及以上,但不到 0.9.0。为什么?因为大版本升级往往有破坏性变更,你不想合约突然编译不过吧?
⚠️ 避坑指南:我曾经在项目里忘了写版本声明,结果同事用不同版本的编译器一跑,合约行为完全不一样。后来我们团队强制要求:每个文件第一行必须是 pragma。

4.2 合约结构

一个 Solidity 合约,其实就是一个类。你想想看,它有状态、有函数、有构造函数。基本骨架长这样:

contract MyContract {
    // 状态变量
    uint public myNumber;

    // 构造函数
    constructor() {
        myNumber = 42;
    }

    // 函数
    function setNumber(uint _newNumber) public {
        myNumber = _newNumber;
    }
}

结构很清晰对吧?从上到下依次是:状态变量声明 → 构造函数 → 自定义函数。我建议你也按这个顺序写,团队协作时别人一眼就能找到关键信息。

4.3 状态变量与局部变量

这两个概念特别重要,很多新手搞混。

  • 状态变量:写在合约顶层,永久存储在区块链上。每次调用合约,它都在。
  • 局部变量:写在函数内部,函数执行完就消失。不占链上存储,省 gas。

举个例子:

contract Demo {
    uint public storedData; // 状态变量,存在链上

    function doSomething(uint input) public {
        uint temp = input + 1; // 局部变量,函数结束就没了
        storedData = temp;
    }
}

嗯,这里要注意:状态变量操作是要花 gas 的,而且很贵。局部变量几乎免费。所以能不用状态变量就别用,能缓存就缓存。我在项目中遇到过有人把循环计数器都写成状态变量,结果一笔交易花了 0.1 ETH 的 gas……老板脸都绿了。

4.4 基本数据类型

Solidity 的数据类型不多,但每个都有讲究。

类型 说明 示例
uint 无符号整数,默认 uint256 uint public count = 100;
address 以太坊地址,20字节 address public owner = msg.sender;
string 字符串,动态长度 string public name = "NFT";
bool 布尔值,true/false bool public isActive = true;

这里我多说两句:

  • uint:可以指定位数,比如 uint8、uint256。位数越小越省 gas。但别为了省 gas 用 uint8 存大数,会溢出。我吃过这个亏。
  • address:0.8.x 版本后,address 分成了 address 和 address payable。后者才能接收 ETH。如果你要转币,记得用 payable。
  • string:不能直接比较大小,也不能直接用 == 比较(除非用 keccak256 哈希)。操作字符串很贵,能不用就不用。
  • bool:简单,但注意 Solidity 里没有「假值」的概念,if 条件里必须显式写布尔表达式。

4.5 映射(mapping)与数组(array)

这两个是 Solidity 里最常用的数据结构。说白了,一个用来查,一个用来存列表。

映射(mapping)

映射就像字典,键值对。声明方式:

mapping(address => uint) public balances;

这表示:给定一个地址,查到他有多少余额。映射的特点是:

  • 不能遍历(没有 length 或 keys 方法)
  • 所有键默认都有值(uint 默认 0,bool 默认 false)
  • 非常省 gas,因为它是哈希表结构

我一般用映射来存用户数据、权限、余额等。但如果你需要遍历所有用户,那就得用数组。

数组(array)

数组分两种:固定长度和动态长度。

uint[10] public fixedArray;      // 固定长度
uint[] public dynamicArray;      // 动态长度

动态数组可以 push、pop、获取 length。但注意:遍历数组很贵,动辄几十万 gas。我建议:

  • 能用映射就别用数组
  • 如果必须用数组,加一个映射来记录索引位置,方便删除
💡 实战技巧:我在 NFT 项目里常用「映射 + 数组」组合。映射存数据,数组存 ID 列表。这样既能快速查询,又能遍历所有 NFT。

4.6 构造函数与修饰符(modifier)

构造函数

构造函数在合约部署时执行一次,之后就不能再调用了。通常用来初始化 owner、设置初始参数等。

address public owner;

constructor() {
    owner = msg.sender;
}

注意:0.8.x 之后,构造函数不再需要 function 关键字,直接写 constructor 就行。老版本里是 function ContractName(),现在别那么写了。

修饰符(modifier)

修饰符是 Solidity 的一大特色。它可以在函数执行前后插入逻辑,常用于权限控制。

modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

function withdraw() public onlyOwner {
    // 只有 owner 能调用
}

那个 _; 是关键,它表示「继续执行原函数」。你可以把 modifier 想象成一个包装器。我习惯把常用的权限控制、重入锁、暂停开关都写成 modifier,代码干净很多。

⚠️ 避坑指南:我曾经在 modifier 里写了 _; 之后又写了 return,结果函数执行了两遍。记住:_; 之后不要再加 return,除非你真的知道自己在干嘛。

4.7 事件(event)的声明与触发

事件是 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,加了之后前端可以按这个参数过滤事件。比如监听某个地址的所有转账。
  • emit:触发事件的关键字。老版本用 Transfer(...) 直接写,现在必须加 emit。
  • 事件不存链上:事件日志存在区块的日志里,但合约不能读取。它只用于外部监听。

我个人习惯是:所有状态变更操作都触发事件。这样前端可以实时更新 UI,也方便调试。你想想看,如果合约里改了数据但没发事件,前端怎么知道?只能轮询,那体验就差了。

📌 核心总结:这一章我们覆盖了 Solidity 最基础但最重要的概念。版本声明是门面,合约结构是骨架,变量类型是血肉,映射和数组是工具,构造函数和修饰符是控制逻辑,事件是沟通桥梁。把这些吃透了,下一章我们就能开始写真正的 NFT 合约了。