第四讲: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(无符号整数)
说白了就是正整数。有uint8、uint16……一直到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
}
你想想看,为什么要有view和pure?因为调用它们不需要消耗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检查)。
好,这一讲就到这里。下一讲我们会深入mapping、struct和modifier,这些都是写NFT合约的必备技能。咱们下节课见。