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 = "临时数据";
}
}
3.2 数据类型:Solidity的“基本积木”
Solidity的数据类型,我分成两类讲:值类型和引用类型。这一章我们先搞定值类型。
3.2.1 uint / int:整数类型
uint是无符号整数(只能正数),int是有符号整数(可正可负)。
你可能会问:为什么Solidity要搞这么多位宽?uint8、uint256……
原因很简单:省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:可接收以太币的地址,有transfer和send方法
嗯,这里要注意:如果你想把以太币转给某个地址,必须用address payable。普通address转不了钱。
address public owner = 0x123...;
address payable public recipient = payable(0x456...);
function sendEther() public {
recipient.transfer(1 ether);
}
3.2.3 bool:布尔类型
就是true和false。简单,但容易踩坑。
我记得有一次写合约,判断用户是否已注册:
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。如果长度可变,用string或bytes。但记住:字符串操作非常耗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 |
嗯,这里有个小技巧:如果函数只被外部调用,用external比public省Gas。因为external的参数直接从calldata读取,而public会先复制到内存。
public,结果任何人都能调用。黑客直接把我合约里的钱全提走了……后来我改成onlyOwner修饰符+external可见性。记住:默认用private或internal,需要对外暴露才用public或external。
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里最常用的两种数据结构。到时候你会看到,怎么用它们来存“键值对”和“列表”。