第四讲:Solidity语言基础(上)——合约结构、变量与函数

好,咱们开始进入正题。这一讲是Solidity语言基础的上半部分,我会带你从零搭建一个合约的骨架。说白了,就是搞清楚一个智能合约长什么样、里面能放什么数据、函数该怎么写。

我个人习惯把Solidity合约想象成一个“有规矩的保险箱”。你往里面存数据,然后通过函数来控制谁能取、怎么取。嗯,这个比喻虽然简单,但很实用。

4.1 合约的基本结构

一个最简单的Solidity合约,长这样:

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

contract MyFirstContract {
    // 状态变量
    uint256 public myNumber;

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

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

这里有几个关键点:

  • SPDX许可证声明:这不是可选的。现在大部分链都要求你声明开源许可证,否则合约验证会报错。我一般用MIT,省事。
  • pragma版本声明^0.8.0表示兼容0.8.x系列。千万别写pragma solidity >=0.4.0这种,太老的版本有溢出漏洞,我踩过这个坑。
  • contract关键字:类似其他语言的class。合约名首字母大写,这是约定俗成的规矩。

核心要点:每个Solidity文件至少要有许可证 + pragma + contract 这三样,缺一不可。

4.2 状态变量 vs 局部变量

这是新手最容易搞混的地方。我直接说人话:

  • 状态变量:写在合约里、函数外。永久存储在区块链上,会消耗gas。
  • 局部变量:写在函数内部。只在函数执行时存在,不消耗存储gas。

举个例子:

contract VariableDemo {
    // 状态变量 —— 存在链上
    uint256 public storedData;

    function doSomething() public {
        // 局部变量 —— 只在内存中
        uint256 temp = 100;
        storedData = temp + 1;
    }
}

我在项目中遇到过一个问题:有人把一个大数组定义成局部变量,结果每次调用都重新创建,gas费高得离谱。记住,能省则省,能用局部变量就别用状态变量

小技巧:状态变量默认初始值是0(uint)、false(bool)、空地址(address)。局部变量必须显式赋值,否则编译会报错。

4.3 基本数据类型详解

Solidity的数据类型不多,但每个都有讲究。咱们重点看三个最常用的:

4.3.1 uint(无符号整数)

说白了就是正整数。有uint8uint16……一直到uint256,不写数字默认是uint256

uint256 public bigNumber = 1000000;
uint8 public smallNumber = 255;  // 最大255

避坑指南:我曾经在项目里用uint8存一个计数器,结果跑到256就溢出了,直接归零。Solidity 0.8.0之后默认带溢出检查,但如果你用unchecked块,还是会出问题。所以,选对类型很重要

4.3.2 string(字符串)

字符串在Solidity里有点特殊。它不能直接比较,也不能直接拼接(需要借助库)。

string public name = "Alice";

你想想看,为什么字符串这么“弱”?因为Solidity是为区块链设计的,字符串操作太灵活,容易导致gas不可控。所以,能用bytes32就别用string,除非你真的需要动态长度。

4.3.3 address(地址)

地址类型存储以太坊地址,20字节。它有两个变种:

  • address:普通地址
  • address payable:可转账地址,有.transfer().send()方法
address public owner;
address payable public treasury;

function setOwner(address _newOwner) public {
    owner = _newOwner;
}

我个人习惯把合约的部署者存成address,需要收钱的地址存成address payable。这样逻辑清晰,不会搞混。

类型 占用空间 默认值 典型用途
uint256 32字节 0 余额、数量、ID
string 动态 "" 名称、描述、元数据
address 20字节 0x0 用户地址、合约地址

4.4 函数定义与可见性

函数是合约的“动作”。定义方式如下:

function 函数名(参数列表) 可见性 返回值 {
    // 函数体
}

可见性有四种,但咱们先掌握最常用的两个:

4.4.1 public(公开)

谁都能调用。合约内部、外部账户、其他合约,都可以访问。

function greet() public pure returns (string memory) {
    return "Hello!";
}

注意:public函数会自动生成一个getter,如果你声明了public状态变量,Solidity会帮你自动生成一个同名函数来读取它。

4.4.2 internal(内部)

只有合约内部和子合约能调用。外部不能直接访问。

function _internalLogic() internal {
    // 只有合约内部能调用
}

我习惯把内部逻辑函数用internal,并且函数名前加下划线(_),这是Solidity社区的命名惯例。比如_updateBalance_validateAddress

经验之谈:写合约时,默认用internal,需要暴露给外部的才用public。这样可以减少攻击面。我曾经见过一个合约把所有函数都写成public,结果用户直接调了内部结算函数,导致资金错乱……嗯,教训深刻。

4.4.3 函数修饰符

除了可见性,函数还可以加一些“功能修饰符”:

  • view:只读状态变量,不修改
  • pure:既不读也不写状态变量,纯计算
  • payable:可以接收ETH
function getNumber() public view returns (uint256) {
    return myNumber;
}

function add(uint256 a, uint256 b) public pure returns (uint256) {
    return a + b;
}

function deposit() public payable {
    // 可以接收ETH
}

你想想看,为什么要有viewpure?因为调用它们不需要消耗gas(外部调用时)。这是Solidity的优化机制,能省则省。

4.5 综合示例:一个简单的计数器合约

把今天学的知识串起来,写一个完整的计数器合约:

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

contract Counter {
    // 状态变量
    uint256 public count;
    address public owner;

    // 构造函数 —— 部署时执行一次
    constructor() {
        owner = msg.sender;
        count = 0;
    }

    // 公开函数 —— 任何人都可以增加计数
    function increment() public {
        count += 1;
    }

    // 内部函数 —— 只有合约自己能调用
    function _reset() internal {
        count = 0;
    }

    // 只读函数 —— 不消耗gas
    function getCount() public view returns (uint256) {
        return count;
    }

    // 纯函数 —— 纯计算
    function double(uint256 x) public pure returns (uint256) {
        return x * 2;
    }
}

这个合约虽然简单,但包含了我们今天讲的所有核心概念:合约结构、状态变量、局部变量、基本数据类型、函数定义与可见性。你可以把它部署到测试网上试试看。

课后练习:试着给这个Counter合约加一个decrement函数,并确保计数不会变成负数(提示:用require检查)。

好,这一讲就到这里。下一讲我们会深入mappingstructmodifier,这些都是写NFT合约的必备技能。咱们下节课见。