3、Solidity基础语法:变量与状态变量、数据类型、函数定义与可见性

好,咱们正式开始写Solidity代码了。

这一章我带你过一遍Solidity最核心的语法。说白了,就是三件事:怎么存数据、存什么类型的数据、以及怎么操作这些数据。你把这章吃透了,后面写合约就会顺手很多。

3.1 变量与状态变量:数据到底存哪儿?

在Solidity里,变量分两种:状态变量局部变量

状态变量是永久存储在区块链上的。你想想看,合约部署之后,这些变量的值会一直存在,直到你主动修改它。每次调用函数,状态变量都“记得”上一次的值。

局部变量就不一样了。它只在函数执行期间存在,函数跑完就没了。有点像你临时写在草稿纸上的数字,用完就扔。

我个人习惯,把状态变量放在合约的最前面,一眼就能看到合约的“全局状态”。

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

contract VariableDemo {
    // 状态变量 - 永久存储在链上
    uint256 public myNumber;
    string public myName;

    function demo() public {
        // 局部变量 - 函数执行完就消失
        uint256 temp = 100;
        string memory tempStr = "临时数据";
    }
}
💡 我的经验:状态变量每次读写都要消耗Gas。所以能不用状态变量就别用。我见过有人把循环计数器也写成状态变量,结果Gas费高得离谱。局部变量用memory或calldata修饰,不花存储费。

3.2 数据类型:Solidity的“基本积木”

Solidity的数据类型,我分成两类讲:值类型引用类型。这一章我们先搞定值类型。

3.2.1 uint / int:整数类型

uint是无符号整数(只能正数),int是有符号整数(可正可负)。

你可能会问:为什么Solidity要搞这么多位宽?uint8uint256……

原因很简单:省Gas。你想想看,如果你只需要存一个0-255之间的数,用uint256就是浪费。Solidity会按256位(32字节)的槽位来存储,但编译器会尽量帮你打包。

类型 范围 占用空间
uint8 0 ~ 2^8-1 1字节
uint256 0 ~ 2^256-1 32字节
int256 -2^255 ~ 2^255-1 32字节
⚠️ 避坑指南:我曾经在项目里用uint8存年龄,结果有个用户年龄超过255……合约直接报溢出错误。后来我改成uint256了。记住:安全第一,省Gas第二。不确定范围就用uint256

3.2.2 address:地址类型

address就是以太坊上的账户地址,20字节。它有两种变体:

  • address:普通地址
  • address payable:可接收以太币的地址,有transfersend方法

嗯,这里要注意:如果你想把以太币转给某个地址,必须用address payable。普通address转不了钱。

address public owner = 0x123...;
address payable public recipient = payable(0x456...);

function sendEther() public {
    recipient.transfer(1 ether);
}

3.2.3 bool:布尔类型

就是truefalse。简单,但容易踩坑。

我记得有一次写合约,判断用户是否已注册:

mapping(address => bool) public isRegistered;

function register() public {
    // 错误写法:直接赋值true
    isRegistered[msg.sender] = true;
    // 正确做法:先检查是否已注册
    require(!isRegistered[msg.sender], "已注册过了");
    isRegistered[msg.sender] = true;
}

你看,bool本身没问题,但逻辑上容易漏掉检查。

3.2.4 string:字符串类型

string在Solidity里比较特殊。它本质上是动态字节数组,但你不能直接修改它。

为什么?因为字符串长度不固定,Solidity处理起来很麻烦。我建议:能用bytes就别用string,除非你非要存文本。

string public greeting = "Hello";

function setGreeting(string memory _newGreeting) public {
    greeting = _newGreeting;
}
💡 我的习惯:如果字符串长度固定(比如哈希值),我用bytes32。如果长度可变,用stringbytes。但记住:字符串操作非常耗Gas,能少用就少用。

3.2.5 bytes:字节数组

bytes是Solidity的“底层数据”。它比string更灵活,也更省Gas。

有两种:

  • bytes1 ~ bytes32:固定长度,存在栈上
  • bytes:动态长度,存在内存或存储中
bytes32 public hash = 0x1234567890abcdef...;
bytes public data;

function setData(bytes memory _data) public {
    data = _data;
}

3.3 函数定义与可见性:谁可以调用?

函数是合约的“行为”。定义函数时,你要告诉Solidity三件事:参数、返回值、可见性

3.3.1 函数的基本结构

function 函数名(参数类型 参数名) 可见性 状态可变性 返回值类型 {
    // 函数体
}

举个例子:

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

3.3.2 可见性:谁能看到这个函数?

Solidity有四种可见性修饰符:

修饰符 谁可以调用 典型用途
public 任何人(内部+外部) 对外接口
private 仅当前合约 内部逻辑
internal 当前合约+子合约 继承时使用
external 仅外部调用 节省Gas

嗯,这里有个小技巧:如果函数只被外部调用,用externalpublic省Gas。因为external的参数直接从calldata读取,而public会先复制到内存。

⚠️ 避坑指南:我曾经把合约的提款函数写成public,结果任何人都能调用。黑客直接把我合约里的钱全提走了……后来我改成onlyOwner修饰符+external可见性。记住:默认用privateinternal,需要对外暴露才用publicexternal

3.3.3 状态可变性:函数会不会改数据?

Solidity还有两个重要的修饰符:

  • view:只读,不修改状态
  • pure:既不读也不写状态

你想想看,如果一个函数只是计算,不碰任何状态变量,用pure。如果只是读取状态变量,用view。这两个修饰符能帮你省Gas,因为调用它们不需要交易。

// 读取状态变量
function getNumber() public view returns (uint256) {
    return myNumber;
}

// 纯计算
function multiply(uint256 a, uint256 b) public pure returns (uint256) {
    return a * b;
}

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

把上面的知识点串起来,写一个完整的计数器合约:

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

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

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

    // 外部函数 - 只允许owner调用
    function increment() external {
        require(msg.sender == owner, "只有owner可以增加");
        count += 1;
    }

    // 只读函数
    function getCount() external view returns (uint256) {
        return count;
    }

    // 纯计算函数
    function double(uint256 _value) external pure returns (uint256) {
        return _value * 2;
    }
}

这个合约虽然简单,但包含了:状态变量、地址类型、可见性控制、view/pure。你把它跑一遍,基本语法就全通了。

📌 本章核心要点:
  • 状态变量存链上,局部变量用完就丢
  • uint/int选对位宽,不确定就用uint256
  • address payable才能收钱
  • string耗Gas,能用bytes32就用bytes32
  • 可见性:private > internal > public > external(按安全程度)
  • view/pure帮你省Gas

下一章,我们讲映射和数组——Solidity里最常用的两种数据结构。到时候你会看到,怎么用它们来存“键值对”和“列表”。