第二课:Solidity语言入门(上)——合约结构、状态变量与局部变量、整型与布尔类型、地址类型与单位

各位同学,欢迎来到Solidity语言的第一课。说实话,每次带新人入门,我都会从这一章开始讲起。因为不管你以后写多复杂的合约,最终都离不开我们今天要聊的这些基础概念。

我个人习惯把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;
    }

    function getNumber() public view returns (uint256) {
        return myNumber;
    }
}

这个结构其实很清晰。从上到下依次是:

  • SPDX许可证声明:这是开源社区的习惯,告诉别人你的代码可以怎么用。我建议你每次都写上MIT,省事。
  • pragma指令:声明编译器版本。这里用的是^0.8.0,意思是兼容0.8.x的所有小版本。
  • contract关键字:定义一个合约,类似于其他语言里的class。
  • 状态变量:存储在链上的数据。
  • 构造函数:部署时自动执行一次,用来初始化。
  • 函数:合约的行为逻辑。
小提示: 我刚开始写合约时,经常忘记写SPDX声明,结果编译时总报错。后来养成了习惯,新建文件第一件事就是敲上那行注释。

二、状态变量 vs 局部变量

这两个概念,说白了就是「永久存储」和「临时存储」的区别。

状态变量是写在合约最外层、函数之外的变量。它们会被永久保存在区块链上,每次调用合约都能读到。你想想看,这就像是一个公开的账本,谁都能查。

局部变量只在函数内部有效,函数执行完就消失了。它们不会消耗存储空间,只存在于内存中。

contract VariableDemo {
    // 状态变量:永久存储在链上
    uint256 public storedData;

    function demo() public {
        // 局部变量:临时存在,函数结束就没了
        uint256 tempValue = 42;
        storedData = tempValue;
    }
}

这里有个坑,我曾经踩过。有一次我写了一个循环,在循环里频繁修改状态变量,结果gas费高得吓人。后来才意识到,应该先用局部变量计算好结果,最后再一次性赋值给状态变量。

注意: 状态变量的读写操作都需要消耗gas。能少写就少写,能合并就合并。这是Solidity优化的第一原则。

三、整型与布尔类型

Solidity的整型分两种:有符号(int)和无符号(uint)。区别就在于能不能表示负数。

类型 范围 常见用途
uint8 0 ~ 255 小数值、状态码
uint256 0 ~ 2^256-1 余额、时间戳、ID
int256 -2^255 ~ 2^255-1 需要负数的场景
bool true / false 开关、权限判断

我个人习惯,只要不是特别需要节省空间,一律用uint256。为什么?因为EVM底层对256位的操作最友好,用更小的类型反而可能多花gas。

布尔类型就简单了,只有true和false。但要注意一点:Solidity里没有「假值」的概念。你不能像在JavaScript里那样写if (someNumber)来隐式转换,必须显式比较。

contract TypeDemo {
    uint256 public balance = 1000;
    bool public isActive = true;

    function check() public view returns (bool) {
        // 必须显式比较
        if (balance > 0) {
            return true;
        }
        return false;
    }
}
核心要点: 整型溢出在Solidity 0.8+版本中会自动检查。如果你用旧版本,一定要用SafeMath库。我曾经在审计一个旧合约时,就发现了一个经典的溢出漏洞——攻击者通过溢出把余额变成了天文数字。

四、地址类型与单位

地址类型是Solidity里最特殊的类型之一。它代表一个以太坊账户地址,长度固定为20字节(160位)。

地址类型有两种:

  • address:普通地址,只能存,不能转账。
  • address payable:可支付地址,可以用.transfer().send()发送以太币。
contract AddressDemo {
    address public owner;
    address payable public beneficiary;

    constructor() {
        owner = msg.sender;
        beneficiary = payable(0x123...); // 需要显式转换
    }

    function sendEther() public payable {
        // 只有payable地址才能接收以太币
        beneficiary.transfer(msg.value);
    }
}

嗯,这里要注意。从Solidity 0.8.0开始,address不能直接转成address payable,必须用payable()函数显式转换。我刚开始升级项目时,就被这个改动坑了一把,编译一直报错。

再来说说单位。Solidity内置了以太币的单位换算:

contract UnitDemo {
    uint256 public weiValue = 1 wei;
    uint256 public gweiValue = 1 gwei;
    uint256 public etherValue = 1 ether;

    // 1 ether = 10^18 wei
    // 1 gwei = 10^9 wei
}

这些单位本质上就是数字的语法糖。1 ether等价于10^18 wei,编译器会自动帮你换算。我个人建议,在合约里统一用wei做单位,避免精度丢失。前端展示时再换算成ether。

避坑指南: 我曾经见过一个合约,开发者在代码里直接写了1 * 10**18来表示1个ether。结果因为运算符优先级问题,算出来的值完全不对。用内置单位1 ether就永远不会出错。

五、实战小练习

光看不练可不行。我建议你动手写一个简单的「存钱罐」合约:

contract PiggyBank {
    address public owner;
    uint256 public balance;

    constructor() {
        owner = msg.sender;
    }

    function deposit() public payable {
        balance += msg.value;
    }

    function withdraw() public {
        require(msg.sender == owner, "你不是主人");
        payable(owner).transfer(address(this).balance);
        balance = 0;
    }
}

这个合约虽然简单,但包含了我们今天讲的所有知识点:状态变量、地址类型、单位换算、权限控制。你试着部署到测试网跑一跑,感受一下链上交互的流程。

好了,这一课的内容就到这里。下一章我们会继续深入,聊聊Solidity的引用类型、映射和数组。到时候你会发现,有了今天的基础,后面的内容学起来会轻松很多。