3、智能合约基础:Solidity语法速览、合约结构、状态变量与函数、事件与修饰符

好,咱们进入第三章节。说实话,很多同学学NFT开发,一看到Solidity就头大。我当年也是,觉得这玩意儿跟JavaScript长得像,但用起来处处是坑。别急,今天我用一节课的时间,带你快速过一遍Solidity的核心骨架。

你想想看,智能合约说白了就是跑在区块链上的程序。它不能重启,不能随便改,部署了就跟你签了合同一样。所以,写合约的时候,每一行代码都得想清楚。

3.1 Solidity语法速览

Solidity是一门静态类型语言,语法上跟JavaScript、C++都有点像。我个人习惯把它理解为「带类型检查的JavaScript,但跑在以太坊上」。

先看一个最简单的合约骨架:

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

contract HelloNFT {
    string public name = "MyFirstNFT";
    uint256 public totalSupply = 10000;
}

这里有几个关键点:

  • SPDX许可证:开源协议声明,不加的话有些编译器会报警告。我一般用MIT。
  • pragma solidity:指定编译器版本。^0.8.0表示兼容0.8.x系列。嗯,这里要注意,版本不对应会导致部署失败。
  • contract:合约关键字,相当于Java里的class。
我的小习惯:每次新建合约文件,第一件事就是写版本声明。曾经有一次我忘了写,结果用Remix编译时报了一堆看不懂的错误,排查了半小时才发现是版本问题。

3.2 合约结构

一个完整的Solidity合约,通常包含以下几部分:

组成部分 说明 是否必须
SPDX许可证 开源协议标识 推荐
pragma指令 编译器版本声明 必须
import语句 引入其他合约或库 按需
contract定义 合约主体 必须
状态变量 存储在链上的数据 按需
函数 合约的行为逻辑 按需
事件 链上日志输出 按需
修饰符 函数访问控制 按需

我建议你写合约时,按照这个顺序来组织代码。这样别人读你的代码,一眼就能找到想要的东西。

3.3 状态变量与函数

状态变量是合约的灵魂。它们被永久存储在区块链上,每次读写都要花Gas。说白了,这就是你的「数据库」。

contract NFTMarket {
    // 状态变量
    address public owner;
    uint256 public listingFee = 0.01 ether;
    mapping(uint256 => address) public tokenOwner;
    
    // 构造函数:部署时执行一次
    constructor() {
        owner = msg.sender;
    }
    
    // 函数:修改状态
    function mint(address to, uint256 tokenId) public {
        require(tokenOwner[tokenId] == address(0), "Token already exists");
        tokenOwner[tokenId] = to;
    }
    
    // 函数:只读,不花Gas
    function getOwner(uint256 tokenId) public view returns (address) {
        return tokenOwner[tokenId];
    }
}

这里有几个关键概念:

  • public:自动生成getter函数,外部可以直接读取。我建议状态变量尽量用public,方便调试。
  • mapping:键值对存储,相当于哈希表。NFT项目里90%的数据都用mapping存。
  • msg.sender:调用者地址。这是Solidity里最重要的全局变量之一。
  • view:声明函数不修改状态,调用不花Gas。但注意,如果被另一个修改状态的函数调用,还是会花Gas。
我曾经踩过的坑:有一次我把一个需要修改状态的函数忘了加payable,结果用户转ETH进来直接revert。记住:凡是涉及转账的函数,必须加payable关键字。

3.4 事件与修饰符

事件是合约向外部世界「喊话」的方式。前端DApp通过监听事件,实时获取链上状态变化。

contract NFTEvent {
    // 定义事件
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Mint(address indexed owner, uint256 tokenId, uint256 price);
    
    function mintNFT(uint256 tokenId) public {
        // ... 铸造逻辑
        emit Mint(msg.sender, tokenId, 0.1 ether);
    }
    
    function transferNFT(address to, uint256 tokenId) public {
        // ... 转账逻辑
        emit Transfer(msg.sender, to, tokenId);
    }
}

事件里的indexed关键字很重要。加了indexed的参数可以被前端过滤查询。我一般把地址和ID都设为indexed,方便前端做搜索。

修饰符是Solidity里很优雅的设计。它让你把重复的权限检查抽出来,代码干净很多。

contract WithModifier {
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }
    
    // 定义修饰符
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;  // 继续执行原函数
    }
    
    modifier validTokenId(uint256 tokenId) {
        require(tokenId > 0, "Invalid token ID");
        _;
    }
    
    // 使用修饰符
    function withdraw() public onlyOwner {
        // 只有owner能调用
        payable(owner).transfer(address(this).balance);
    }
    
    function burn(uint256 tokenId) public onlyOwner validTokenId(tokenId) {
        // 多个修饰符叠加使用
        // 销毁逻辑
    }
}

修饰符的执行顺序是从左到右。上面的例子中,先检查onlyOwner,再检查validTokenId。如果第一个检查失败,后面的就不会执行了。

核心要点:
  • 状态变量是链上存储,读写都花Gas
  • 事件是链上日志,前端通过监听事件获取数据
  • 修饰符用于权限控制和参数校验
  • view函数不花Gas,但只能读不能写
  • payable函数才能接收ETH

好了,这一章的内容就到这里。说实话,Solidity入门不难,难的是写出安全、高效的合约。我建议你每学一个知识点,就去Remix里敲一遍代码,亲手部署试试。下一章我们会讲ERC-721标准,那是NFT的核心协议,敬请期待。

课后练习:写一个简单的NFT合约,包含mint函数和transfer函数,加上onlyOwner修饰符,并定义Transfer事件。部署到测试网试试看。