返回 SC 笔记
SC Day 1

Solidity 开发环境 + 数据类型 + 变量

掌握 Remix IDE 使用、Solidity 全部基础数据类型、三种变量类型

2026-04-10
第一阶段:基础构建
solidityremixdata-typesvariablesbasics

日期: 2026-04-10 方向: Solidity 阶段: 第一阶段:基础构建 标签: #solidity #remix #data-types #variables #basics


今日目标

类型内容
学习掌握 Remix IDE 使用、Solidity 全部基础数据类型、三种变量类型
实操在 Remix 中编写和部署合约,在 Sepolia 测试网部署第一个合约
产出一个包含所有数据类型的 DataTypes 合约 + 部署到 Sepolia 的交易记录

一、Remix IDE 完全指南

1.1 什么是 Remix

Remix(https://remix.ethereum.org)是以太坊官方推荐的浏览器端 Solidity 开发环境。对于初学者来说,它是最快上手的工具——无需安装任何东西,打开浏览器即可编写、编译、部署和调试智能合约。

1.2 Remix 界面布局

打开 Remix 后,你会看到四个核心区域:

┌─────────────────────────────────────────────────┐
│  📁 文件浏览器  │  📝 代码编辑器                    │
│  (左侧面板)     │  (中央区域)                       │
│                 │                                   │
│  contracts/     │  // SPDX-License-Identifier: MIT  │
│  scripts/       │  pragma solidity ^0.8.0;          │
│  tests/         │                                   │
│                 │  contract MyFirst { ... }          │
├─────────────────┼───────────────────────────────────┤
│  🔧 插件面板     │  📋 终端/控制台                    │
│  - Compiler     │  (底部区域)                        │
│  - Deploy       │  交易日志、编译输出                 │
│  - Debugger     │                                    │
└─────────────────┴───────────────────────────────────┘

1.3 关键操作流程

Step 1: 创建合约文件

  • 在左侧 File Explorer 中,点击 contracts 文件夹
  • 点击新建文件图标,命名为 DataTypes.sol

Step 2: 编写合约代码

  • 每个 .sol 文件开头必须有 License 标识和 pragma 版本声明

Step 3: 编译合约

  • 左侧点击 "Solidity Compiler" 插件(第二个图标)
  • 选择编译器版本(推荐 0.8.20+)
  • 点击 "Compile" 按钮
  • 绿色勾表示编译成功

Step 4: 部署合约

  • 左侧点击 "Deploy & Run Transactions" 插件(第三个图标)
  • Environment 选择 "Remix VM (Shanghai)" 做本地测试
  • 点击 "Deploy" 按钮

1.4 部署到 Sepolia 测试网

要部署到真正的区块链(测试网),需要:

  1. 安装 MetaMask 浏览器扩展
  2. 切换到 Sepolia 测试网络
  3. 领取测试 ETH:访问 https://sepoliafaucet.comhttps://faucets.chain.link/sepolia
  4. 在 Remix 的 Environment 中选择 "Injected Provider - MetaMask"
  5. MetaMask 会弹出确认框,点击确认即可部署

二、Solidity 数据类型详解

Solidity 是静态类型语言,每个变量在编译时必须明确类型。这与 JavaScript/Python 这类动态类型语言有本质区别。理解数据类型对于智能合约开发至关重要,因为不同类型的 gas 消耗不同,选错类型可能导致严重的安全漏洞。

2.1 布尔类型 (bool)

bool public isActive = true;
bool public isCompleted = false;

// 布尔运算
bool public result1 = !isActive;        // 逻辑非: false
bool public result2 = isActive && isCompleted; // 逻辑与: false
bool public result3 = isActive || isCompleted; // 逻辑或: true

注意事项

  • 默认值为 false
  • gas 优化:Solidity 支持短路求值(short-circuit evaluation),false && expr 不会计算 expr

2.2 整数类型 (uint / int)

这是 Solidity 中最常用的类型。uint 表示无符号整数(≥0),int 表示有符号整数(可以为负)。

// 无符号整数 - 最常用
uint8 public smallNum = 255;          // 0 ~ 255 (2^8 - 1)
uint16 public medNum = 65535;         // 0 ~ 65535
uint32 public timestamp = 1700000000; // 常用于时间戳
uint64 public biggerNum = 18446744073709551615;
uint128 public veryBig = 0;
uint256 public hugeNum = 0;           // 0 ~ 2^256 - 1

// uint 等同于 uint256
uint public defaultUint = 100;

// 有符号整数
int8 public signedSmall = -128;       // -128 ~ 127
int256 public signedBig = -1;
int public defaultInt = -50;          // int 等同于 int256

// 整数运算
uint public sum = 10 + 20;            // 加法: 30
uint public diff = 20 - 10;           // 减法: 10
uint public product = 5 * 6;          // 乘法: 30
uint public quotient = 10 / 3;        // 除法: 3 (向下取整,没有小数!)
uint public remainder = 10 % 3;       // 取模: 1
uint public power = 2 ** 10;          // 幂运算: 1024

关键知识点

  1. 为什么 uint256 最常用? EVM 的原生字长是 256 位,使用 uint256 最高效。使用更小的类型(如 uint8)在单独使用时反而可能消耗更多 gas,因为 EVM 需要额外操作来截断到更小的范围。但在 struct 中紧凑排列小类型可以节省存储。

  2. 溢出保护:Solidity 0.8.0+ 内置溢出检查。uint8(255) + 1 会自动 revert,不再需要 SafeMath 库。

  3. 没有浮点数! Solidity 不支持浮点数。处理小数通常使用放大倍数,例如 ERC20 代币用 decimals = 18,即 1 Token = 1 * 10^18 最小单位。

// 模拟小数:1.5 ETH = 1500000000000000000 wei
uint public onePointFiveEth = 1.5 ether; // Solidity 内置单位
uint public oneGwei = 1 gwei;            // = 10^9 wei
uint public oneEther = 1 ether;          // = 10^18 wei

2.3 地址类型 (address)

地址是 Solidity 特有的类型,表示以太坊上的 20 字节(160位)账户地址。

// 普通地址
address public owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;

// 可支付地址 - 可以接收 ETH
address payable public recipient = payable(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4);

// 地址的属性和方法
// addr.balance - 获取地址余额(单位: wei)
// addr.transfer(amount) - 发送 ETH(失败则 revert)
// addr.send(amount) - 发送 ETH(失败返回 false)
// addr.call{value: amount}("") - 低级调用(推荐方式)

address vs address payable

  • address: 不能直接接收 ETH
  • address payable: 可以调用 .transfer().send() 发送 ETH
  • address 转为 address payable 需要显式转换:payable(addr)
function getBalance(address _addr) public view returns (uint) {
    return _addr.balance;
}

function sendEther(address payable _to) public payable {
    // 推荐使用 call 发送 ETH
    (bool success, ) = _to.call{value: msg.value}("");
    require(success, "Transfer failed");
}

2.4 字节类型 (bytes)

// 固定大小字节数组
bytes1 public a = 0x41;     // 'A' 的 ASCII 码,1字节
bytes2 public b = 0x4142;   // 'AB'
bytes4 public selector = 0xa9059cbb; // transfer 函数选择器
bytes20 public addrBytes = bytes20(owner); // 地址本质上就是 bytes20
bytes32 public hash = keccak256("hello");  // 哈希结果是 bytes32

// 动态大小字节数组
bytes public dynamicData = "hello world";
bytes public emptyBytes = "";

// bytes32 vs string
// bytes32: 固定长度,gas 便宜,适合存哈希/ID
// string: 动态长度,gas 贵,适合存可读文本

2.5 字符串类型 (string)

string public greeting = "Hello, Solidity!";
string public name = unicode"你好世界"; // 支持 Unicode

// 字符串在 Solidity 中功能有限:
// - 不能直接比较:greeting == "Hello" ❌
// - 不能直接获取长度:greeting.length ❌
// - 不能直接拼接(0.8.12+ 可以用 string.concat)

// 字符串比较的正确方式
function compareStrings(string memory _a, string memory _b) public pure returns (bool) {
    return keccak256(abi.encodePacked(_a)) == keccak256(abi.encodePacked(_b));
}

// 字符串拼接 (Solidity 0.8.12+)
function concat(string memory _a, string memory _b) public pure returns (string memory) {
    return string.concat(_a, _b);
}

2.6 枚举类型 (enum)

enum Status { Pending, Active, Completed, Cancelled }
// Pending = 0, Active = 1, Completed = 2, Cancelled = 3

Status public currentStatus = Status.Pending;

function activate() public {
    currentStatus = Status.Active;
}

function getStatusAsUint() public view returns (uint) {
    return uint(currentStatus); // 枚举转整数
}

三、三种变量类型

Solidity 有三种变量类型,它们的存储位置和 gas 成本完全不同。理解这三种变量是写好合约的基础。

3.1 状态变量 (State Variables)

状态变量永久存储在区块链上(storage)。它们是合约的"持久化数据"。

contract StateVariableExample {
    // 状态变量 - 存储在区块链上
    uint public count = 0;           // 任何人可读
    string private secretMsg = "hi"; // 仅合约内部可读(但链上数据仍公开!)
    address internal admin;          // 合约和子合约可读

    // 每次修改状态变量都要消耗 gas(SSTORE 操作码)
    // 首次写入: ~20,000 gas
    // 后续修改: ~5,000 gas
    // 读取: ~200 gas (SLOAD)

    function increment() public {
        count += 1; // 修改状态变量,消耗 gas
    }
}

3.2 局部变量 (Local Variables)

局部变量只存在于函数执行期间,存储在内存(memory)或栈(stack)中。

function calculate(uint _x) public pure returns (uint) {
    // 局部变量 - 函数执行完就消失,不消耗存储 gas
    uint temp = _x * 2;
    uint result = temp + 10;
    return result;
}

3.3 全局变量 (Global Variables)

全局变量是 Solidity 预定义的特殊变量,提供区块链和交易的信息。

contract GlobalVariables {
    function getInfo() public view returns (
        address sender,
        uint value,
        uint gasPrice,
        uint blockNum,
        uint blockTime,
        uint chainId
    ) {
        sender = msg.sender;       // 调用者地址
        value = msg.value;         // 发送的 ETH 数量(wei)
        gasPrice = tx.gasprice;    // 交易 gas 价格
        blockNum = block.number;   // 当前区块号
        blockTime = block.timestamp; // 当前区块时间戳(秒)
        chainId = block.chainid;   // 链 ID (1=主网, 11155111=Sepolia)
    }
}

常用全局变量一览

变量类型说明
msg.senderaddress当前调用者地址
msg.valueuint随交易发送的 ETH(wei)
msg.databytes完整的 calldata
msg.sigbytes4calldata 前4字节(函数选择器)
block.timestampuint当前区块时间戳
block.numberuint当前区块号
block.chainiduint链 ID
block.prevrandaouint前一个区块的 randao 值
tx.gaspriceuint交易 gas 价格
tx.originaddress交易发起者(最初的 EOA)
gasleft()uint剩余 gas

⚠️ 安全警告:永远不要用 tx.origin 做权限校验!用 msg.sender。因为 tx.origin 总是指向最初的外部账户,容易被中间合约利用做钓鱼攻击。


四、代码实战:完整 DataTypes 合约

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

/// @title DataTypes - Solidity 数据类型学习合约
/// @author SC Day1 Learning
/// @notice 这个合约展示了 Solidity 中所有基础数据类型

contract DataTypes {
    // ============ 布尔类型 ============
    bool public isActive = true;

    // ============ 整数类型 ============
    uint8 public maxUint8 = type(uint8).max;       // 255
    uint256 public maxUint256 = type(uint256).max;  // 超大数
    int8 public minInt8 = type(int8).min;           // -128
    int8 public maxInt8 = type(int8).max;           // 127
    uint public tokenDecimals = 18;
    uint public totalSupply = 1_000_000 * 10 ** 18; // 1M tokens, 下划线提高可读性

    // ============ 地址类型 ============
    address public owner;
    address payable public treasury;

    // ============ 字节类型 ============
    bytes1 public singleByte = 0xff;
    bytes4 public transferSelector = bytes4(keccak256("transfer(address,uint256)"));
    bytes32 public dataHash;
    bytes public dynamicBytes = hex"deadbeef";

    // ============ 字符串类型 ============
    string public projectName = "MomoWeb3";
    string public description;

    // ============ 枚举类型 ============
    enum OrderStatus { Created, Paid, Shipped, Completed, Cancelled }
    OrderStatus public currentOrder = OrderStatus.Created;

    // ============ 状态变量: 常量和不可变量 ============
    uint public constant MAX_SUPPLY = 21_000_000;      // 编译时确定,不占存储槽
    address public immutable deployer;                   // 部署时确定,之后不可改
    uint public immutable deployTime;

    // ============ 全局变量演示 ============
    uint public lastCaller;

    constructor(string memory _description) {
        owner = msg.sender;
        treasury = payable(msg.sender);
        deployer = msg.sender;
        deployTime = block.timestamp;
        description = _description;
        dataHash = keccak256(abi.encodePacked(_description));
    }

    /// @notice 展示整数运算
    function mathOperations(uint _a, uint _b) public pure returns (
        uint sum,
        uint diff,
        uint product,
        uint quotient,
        uint remainder
    ) {
        require(_b > 0, "Division by zero");
        require(_a >= _b, "Underflow protection");

        sum = _a + _b;
        diff = _a - _b;
        product = _a * _b;
        quotient = _a / _b;
        remainder = _a % _b;
    }

    /// @notice 展示地址操作
    function getAddressInfo(address _addr) public view returns (
        uint balance,
        bool isContract,
        bytes32 codeHash
    ) {
        balance = _addr.balance;
        // 判断是否是合约地址
        uint size;
        assembly {
            size := extcodesize(_addr)
        }
        isContract = size > 0;
        codeHash = _addr.codehash;
    }

    /// @notice 展示全局变量
    function getGlobalInfo() public view returns (
        address sender,
        uint blockNum,
        uint blockTime,
        uint gasLeft,
        uint chainId
    ) {
        sender = msg.sender;
        blockNum = block.number;
        blockTime = block.timestamp;
        gasLeft = gasleft();
        chainId = block.chainid;
    }

    /// @notice 更新订单状态
    function updateOrderStatus(OrderStatus _newStatus) public {
        require(msg.sender == owner, "Only owner");
        currentOrder = _newStatus;
    }

    /// @notice 字符串比较
    function isProjectName(string memory _name) public view returns (bool) {
        return keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked(projectName));
    }

    /// @notice 接收 ETH
    receive() external payable {}

    /// @notice 查看合约余额
    function getContractBalance() public view returns (uint) {
        return address(this).balance;
    }
}

五、Sepolia 部署实操步骤

5.1 准备工作

  1. 安装 MetaMask:到 https://metamask.io 下载浏览器扩展
  2. 切换网络:MetaMask → 网络下拉 → 显示测试网络 → 选择 Sepolia
  3. 领取测试 ETH

5.2 在 Remix 中部署

  1. 打开 Remix → 粘贴上面的合约代码
  2. Compiler 面板 → 选择 0.8.20 → Compile
  3. Deploy 面板 → Environment 选择 "Injected Provider - MetaMask"
  4. 确认网络显示 "Sepolia (11155111)"
  5. 在 Deploy 旁边的输入框中填写构造函数参数(如 "My first contract"
  6. 点击 Deploy → MetaMask 弹出确认 → 确认交易
  7. 等待 15-30 秒,交易确认后合约地址会出现在 "Deployed Contracts" 下方

5.3 在 Etherscan 上验证

部署成功后,复制合约地址,到 https://sepolia.etherscan.io 搜索,可以看到:

  • 部署交易的详情
  • 合约的字节码
  • 如果验证了源码,可以直接在 Etherscan 上读写合约

六、关键要点总结

要点说明
Solidity 是静态类型变量必须声明类型,编译时检查
uint256 是默认整数EVM 原生字长,gas 最优
没有浮点数用放大倍数模拟小数 (10^18)
address vs address payable只有 payable 能转 ETH
状态变量 = 链上存储最贵的操作,SSTORE ~20000 gas
constant/immutable 省 gas不占存储槽,编译时/部署时确定
msg.sender ≠ tx.origin永远用 msg.sender 做权限检查

七、常见误区

误区 1:以为 private 变量真的"私有"

uint private secret = 42;
// ❌ 链上所有数据都是公开的!
// 任何人都可以通过读取 storage slot 获取 private 变量的值
// private 只是说其他合约不能通过接口读取

误区 2:uint 减法下溢

uint a = 5;
uint b = 10;
// uint c = a - b; // 0.8.0+ 自动 revert!
// 0.7.x 及以前:c 会变成一个极大的数(下溢)

误区 3:string 和 bytes 混淆

// string 适合存储人类可读文本
// bytes32 适合存储哈希、ID 等固定长度数据
// 不要用 string 存哈希——gas 浪费严重

误区 4:以为 block.timestamp 精确

// block.timestamp 由矿工/验证者设置
// 有小范围的操纵空间(约 15 秒)
// 不要用于需要精确到秒的逻辑

八、面试关联

Q: Solidity 中 uint8 和 uint256 哪个更省 gas?

A: 在单独使用时,uint256 更省 gas,因为 EVM 原生字长是 256 位,使用更小的类型需要额外的掩码操作。但在 struct 中,将多个小类型紧凑排列可以共享一个存储槽(32 字节),此时小类型更省 gas。

Q: 为什么 Solidity 没有浮点数?

A: 浮点数有精度问题(0.1 + 0.2 ≠ 0.3),这在金融场景中不可接受。区块链上的资产是精确到 wei 的整数,不存在"约等于"的概念。所有小数都通过放大倍数来处理。

Q: msg.sender 和 tx.origin 有什么区别?

A: msg.sender 是直接调用当前函数的地址(可以是 EOA 或合约),tx.origin 始终是发起这笔交易的 EOA。用 tx.origin 做权限检查有钓鱼攻击风险——恶意合约可以诱导用户调用自己,然后转发调用到目标合约,此时 tx.origin 仍是用户。


九、参考资源

资源链接说明
Solidity 官方文档https://docs.soliditylang.org/最权威的参考
Remix IDEhttps://remix.ethereum.org/在线开发环境
Solidity by Examplehttps://solidity-by-example.org/代码示例大全
Sepolia Etherscanhttps://sepolia.etherscan.io/测试网区块浏览器
OpenZeppelin Docshttps://docs.openzeppelin.com/合约安全库
CryptoZombieshttps://cryptozombies.io/游戏化学习 Solidity