第三章:变量、类型与函数
变量:智能合约的记忆
欢迎回来,区块链冒险家!在上一章中,我们搭建了开发环境并创建了我们的第一个智能合约。现在,是时候深入Solidity的核心元素了——变量、数据类型和函数。这些是构建任何智能合约的基础,就像烹饪中的基本原料和技巧。
变量:合约的记忆单元
如果智能合约是一个大脑,那么变量就是它的记忆。变量让合约能够存储、跟踪和操作数据。在Solidity中,每个变量都有一个特定的类型和存储位置。
变量的声明
声明变量的基本语法是:
类型 [可见性] 变量名 [= 初始值];
例如:
uint256 public myNumber = 42;
string private secretMessage = "区块链万岁!";
bool isActive = true;
变量的作用域
Solidity中的变量有三种主要的作用域:
状态变量:声明在合约内但在函数外的变量。它们存储在区块链上,可以被合约中的所有函数访问。
contract MyContract { uint256 stateVariable; // 这是一个状态变量 function doSomething() public { stateVariable = 100; // 可以访问状态变量 } }
局部变量:声明在函数内的变量。它们只在函数执行期间存在,不存储在区块链上。
function calculate() public pure returns (uint) { uint localVar = 5; // 这是一个局部变量 return localVar * 2; }
全局变量:由Solidity提供的特殊变量,可以在任何地方访问。例如
msg.sender
(当前调用者的地址)、block.timestamp
(当前区块的时间戳)等。function whoCalledMe() public view returns (address) { return msg.sender; // 这是一个全局变量 }
小贴士:状态变量存储在区块链上,这意味着每次更新它们都会消耗gas(以太坊网络的交易费用)。所以,尽量减少状态变量的使用和更新可以节省用户的钱包!
数据类型:Solidity的调色板
如果变量是画布上的颜料,那么数据类型就是调色板上的不同颜色。Solidity提供了多种数据类型,每种都有其特定的用途和限制。让我们深入了解这些类型:
值类型
值类型的变量直接包含值,当它们作为函数参数传递或赋值给另一个变量时,会创建一个独立的副本。
布尔型(bool)
布尔型只有两个可能的值:true
和false
。它们通常用于条件判断。
bool isEligible = true;
bool hasVoted = false;
function checkStatus() public view returns (bool) {
return isEligible && !hasVoted; // 可以使用逻辑运算符
}
整型(int/uint)
整型用于存储整数。int
类型可以表示正数和负数,而uint
只能表示非负数(0和正数)。
Solidity提供了不同大小的整型,从8位到256位,以8位为增量:int8
、int16
、…、int256
(或简写为int
)和uint8
、uint16
、…、uint256
(或简写为uint
)。
int8 smallNumber = -128; // 范围:-128 到 127
uint16 mediumNumber = 65535; // 范围:0 到 65535
int256 bigNumber = -57896044618658097711785492504343953926634992332820282019728792003956564819968; // 最小的int256
uint256 reallyBigNumber = 115792089237316195423570985008687907853269984665640564039457584007913129639935; // 最大的uint256
选择合适大小的整型可以优化gas使用,但要注意溢出和下溢的可能性。
溢出和下溢:在Solidity 0.8.0之前,如果一个整数操作的结果超出了该类型的范围,它会"环绕"(就像汽车里程表从999999变回000000)。从0.8.0开始,这些操作会自动回滚以防止意外的溢出和下溢。如果你想要旧的行为,可以使用
unchecked { ... }
块。
地址(address)
地址类型存储一个20字节的值,代表以太坊地址。有两种地址类型:
address
:普通地址address payable
:可以接收以太币的地址
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
address payable recipient = payable(0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB);
function sendEther() public payable {
recipient.transfer(msg.value); // 只有address payable类型才能接收以太币
}
定长字节数组(bytes1, bytes2, …, bytes32)
这些类型存储固定长度的原始字节数据。
bytes1 singleByte = 0x61; // 存储单个字节(相当于字符'a')
bytes32 hash = keccak256(abi.encodePacked("Hello, Solidity!")); // 存储哈希值
枚举(enum)
枚举允许你创建由一组有限的"常量值"组成的自定义类型。
enum Status { Pending, Approved, Rejected }
Status public currentStatus;
function approve() public {
currentStatus = Status.Approved;
}
function reject() public {
currentStatus = Status.Rejected;
}
function isApproved() public view returns (bool) {
return currentStatus == Status.Approved;
}
枚举在内部表示为整数,从0开始递增,但提供了更好的可读性。
引用类型
引用类型不直接存储值,而是存储值的位置(引用)。当它们作为函数参数传递时,可能会修改原始值。
动态长度字节数组(bytes)
bytes
类型是一个动态大小的字节数组,用于存储任意长度的原始字节数据。
bytes public dynamicData = "Hello, Solidity!";
function appendData(bytes memory newData) public {
dynamicData = bytes.concat(dynamicData, newData);
}
字符串(string)
string
类型用于存储UTF-8编码的文本数据。在内部,它实际上是一个特殊的bytes
数组。
string public greeting = "你好,区块链!";
function setGreeting(string memory newGreeting) public {
greeting = newGreeting;
}
注意:Solidity不提供直接操作字符串的内置函数(如获取长度、连接等)。要执行这些操作,通常需要先将字符串转换为
bytes
。
数组
数组是相同类型元素的集合。Solidity支持固定大小和动态大小的数组。
// 固定大小数组
uint[5] public fixedArray = [1, 2, 3, 4, 5];
// 动态大小数组
uint[] public dynamicArray;
function addNumber(uint number) public {
dynamicArray.push(number); // 添加元素到动态数组
}
function getArrayLength() public view returns (uint) {
return dynamicArray.length; // 获取数组长度
}
function removeLastNumber() public {
dynamicArray.pop(); // 移除最后一个元素
}
映射(mapping)
映射是键值对的集合,类似于其他语言中的字典或哈希表。
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
映射的特点:
- 所有可能的键都存在,初始值为类型的默认值(如
uint
的0) - 无法获取映射的大小或所有键的列表
- 键不存储在映射中,只存储键的
keccak256
哈希值和对应的值
结构体(struct)
结构体允许你创建包含多个不同类型字段的自定义类型。
struct Person {
string name;
uint age;
address wallet;
bool isActive;
}
Person public owner;
mapping(address => Person) public members;
function addMember(string memory name, uint age) public {
members[msg.sender] = Person(name, age, msg.sender, true);
}
function deactivateMember() public {
members[msg.sender].isActive = false;
}
函数:智能合约的行为
如果变量是合约的记忆,那么函数就是合约的行为。函数允许合约执行操作、计算值和改变状态。
函数的定义
函数的基本语法是:
function 函数名(参数类型 参数名, ...) 可见性 [状态可变性] [修饰符] [虚拟性] [重载] [返回类型] {
// 函数体
}
例如:
function transfer(address recipient, uint amount) public {
// 函数体
}
函数可见性
Solidity提供了四种函数可见性:
public:可以从合约内部和外部调用。
function getBalance() public view returns (uint) { return address(this).balance; }
private:只能从定义它的合约内部调用。
function calculateFee(uint amount) private pure returns (uint) { return amount * 5 / 100; }
internal:只能从定义它的合约内部和继承该合约的合约中调用。
function _beforeTransfer(address from, address to) internal virtual { // 内部逻辑 }
external:只能从合约外部调用,不能从合约内部调用(除非使用
this
)。function externalFunction() external pure returns (string memory) { return "I can only be called from outside"; }
小贴士:对于外部调用,
external
函数比public
函数更节省gas,因为Solidity不需要复制参数到内存中。
函数状态可变性
状态可变性修饰符指定函数如何与合约状态交互:
view:函数不修改状态,但可以读取状态。
function getTotalSupply() public view returns (uint) { return totalSupply; }
pure:函数既不修改也不读取状态。
function add(uint a, uint b) public pure returns (uint) { return a + b; }
payable:函数可以接收以太币。
function donate() public payable { donations[msg.sender] += msg.value; }
如果函数没有这些修饰符,它可以修改状态。
函数修饰符
修饰符(modifier)是一种特殊的声明,可以在函数执行前后添加代码。它们通常用于访问控制和输入验证。
contract Owned {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_; // 这个符号表示执行被修饰的函数体
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
在上面的例子中,onlyOwner
修饰符确保只有合约的所有者才能调用transferOwnership
函数。
特殊函数
Solidity有一些特殊的函数:
构造函数(constructor):在合约部署时执行一次。
constructor(string memory name, string memory symbol) { tokenName = name; tokenSymbol = symbol; owner = msg.sender; }
回退函数(fallback):当调用合约的函数不存在或没有提供数据时执行。
fallback() external payable { emit FallbackCalled(msg.sender, msg.value); }
接收函数(receive):当向合约发送以太币但不调用任何函数时执行。
receive() external payable { emit EtherReceived(msg.sender, msg.value); }
函数重载
Solidity支持函数重载,这意味着你可以定义多个同名但参数不同的函数。
function sum(uint a, uint b) public pure returns (uint) {
return a + b;
}
function sum(uint a, uint b, uint c) public pure returns (uint) {
return a + b + c;
}
function sum(string memory a, string memory b) public pure returns (string memory) {
return string(abi.encodePacked(a, b));
}
编译器会根据调用时提供的参数类型选择正确的函数。
函数返回值
Solidity函数可以返回多个值:
function divide(uint a, uint b) public pure returns (uint quotient, uint remainder) {
quotient = a / b;
remainder = a % b;
// 返回值可以通过return语句指定,也可以通过命名返回变量自动返回
}
调用这样的函数时,可以接收所有返回值或忽略一些:
function testDivide() public pure {
// 接收所有返回值
(uint q, uint r) = divide(10, 3);
// 忽略某些返回值
(uint quotient, ) = divide(10, 3);
}
实际例子:简单的银行合约
让我们把学到的知识应用到一个实际例子中——一个简单的银行合约,允许用户存款和取款。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleBank {
// 映射存储每个地址的余额
mapping(address => uint) private balances;
// 事件记录存款和取款
event Deposit(address indexed account, uint amount);
event Withdrawal(address indexed account, uint amount);
// 存款函数
function deposit() public payable {
require(msg.value > 0, "Deposit amount must be greater than 0");
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
// 查询余额函数
function getBalance() public view returns (uint) {
return balances[msg.sender];
}
// 取款函数
function withdraw(uint amount) public {
require(amount > 0, "Withdrawal amount must be greater than 0");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdrawal(msg.sender, amount);
}
// 获取合约总余额
function getBankBalance() public view returns (uint) {
return address(this).balance;
}
}
这个合约展示了我们学到的许多概念:
- 使用
mapping
存储用户余额 - 定义
event
来记录重要操作 - 使用
require
进行条件检查 - 使用
payable
函数接收以太币 - 使用
transfer
发送以太币 - 使用
view
函数查询状态
小结:变量、类型和函数的魔力
在本章中,我们深入探讨了Solidity的基础构建块:变量、数据类型和函数。这些元素共同构成了智能合约的"DNA",决定了它们如何存储数据和执行操作。
我们学习了:
- 不同类型的变量及其作用域
- Solidity提供的各种数据类型,从简单的布尔值和整数到复杂的映射和结构体
- 如何定义和使用函数,包括可见性、状态可变性和修饰符
- 特殊函数如构造函数、回退函数和接收函数
- 如何应用这些知识创建一个简单的银行合约
理解这些概念对于成为一名成功的Solidity开发者至关重要。就像学习一门自然语言一样,掌握了词汇(变量和类型)和语法(函数和表达式),你就能开始构建有意义的"句子"(合约)。
在下一章,我们将探索智能合约的生命周期,了解它们如何被创建、部署、调用和销毁。我们还将深入研究gas和交易费用的概念,这是以太坊区块链的重要方面。
练习挑战:尝试扩展我们的SimpleBank合约,添加一个功能,允许用户向其他账户转账。提示:你需要考虑如何从一个用户的余额中扣除资金并添加到另一个用户的余额中。