Solidity 大神之路之内功修炼第三章

本文系统讲解Solidity核心概念:

  1. 数据类型分为值类型和引用类型,强调值传递与引用传递的区别
  2. 函数修饰符的DRY原则实现,详解onlyOwner等典型用例
  3. 异常处理三机制:require、assert、revert
  4. 类型转换注意事项及浮点数模拟方案

OK,书接上回,欢迎来到第三章,我们继续盘 Solidity ,今天我们讲点啥呢?看完你就知道了😎。这里是 红烧6 的 web3 频道,无论你是初学者还是有一定编程经验的开发者,这篇文章将为你提供清晰、实用的指导。

new.png


1. Solidity 数据类型

Solidity 是一种静态类型语言,变量在声明时必须明确指定类型,类型不匹配将阻止代码编译。这种严格的类型系统确保了以太坊智能合约的安全性和可预测性。 Solidity 的数据类型分为值类型(Value Types)和引用类型(Reference Types),决定了数据是按值传递(复制数据)还是按引用传递(传递内存地址)。Solidity 没有 undefinednull,未初始化的变量会分配默认值,具体值取决于数据类型。

1. 值类型(Value Types)

值类型在传递时复制数据,主要包括:

  • 整数(Integers)

    • 有符号整数(int):可表示正负数。例如,int8(8 位)范围为 -128+127int256 范围为 -(2^255)+(2^255) -1
    • 无符号整数(uint):仅表示正数。例如,uint8 范围为 0255uint256 范围为 02^256 - 1
    • 位长度为 8 的倍数(如 int8int16int256
    • 示例:uint256 a = 100; 若尝试赋值为字符串,编译器报错
  • 地址(Address)

    • 表示以太坊账户(智能合约或外部账户,如 MetaMask 钱包)
    • 默认值:0x0000000000000000000000000000000000000000address(0)
    • 示例:address owner = msg.sender;
  • 布尔值(Boolean)

    • 表示 truefalse,默认值为 false
    • 示例:bool isActive = true;
  • 固定大小字节数组(Fixed-Size Byte Arrays)

    • bytes1bytes2bytes32,存储固定长度的字节序列
    • 示例:bytes32 data = 0x1234567890abcdef;

代码示例:


pragma solidity ^0.8.0;

contract ValueTypeExample {
    // 整数类型
    // 有符号整数 int 支持正负数
    int8 public smallSignedInt = -127; // 8位,范围 -128 到 +127
    int256 public largeSignedInt = -123456789; // 256 位,范围 -(2^255) 到 +(2^255) -1

    // 无符号整数 uint 仅支持正数
    uint8 public smallUnsignedInt = 255; // 8 位 范围 0 到 255
    uint256 public largeUnsignedInt = 123456789; // 256 位 范围 0 到  2^256 - 1

    // 地址类型
    // 表示以太坊账户地址 ,默认值为 0x0
    address public owner = msg.sender; // 当前调用者的地址
    address public zeroAddress = address(0); // 默认地址:0x000...000

    // 布尔类型
    // 表示 true 或者 false 默认值为 false
    bool public isActive = true; // 布尔值,初始化为 true
    bool public isInitialized; // 未初始化,默认为 false

    // 固定大小字节数组
    // 存储固定长度的字节序列
    // bytes1 合法输入方式
    bytes1 public singleByte = 0xFF; // 1字节,最大值 0xFF
    bytes1 public byte1Hex = 0x0A; // 十六进制,十进制 10
    bytes1 public byte1Binary = bytes1(uint8(10)); // 模拟二进制,十进制 10
    bytes1 public byte1Char = "A"; // ASCII 字符,字节值 0x41

    // bytes32 合法输入方式
    bytes32 public byte32String = "Solidity"; // 字符串转为字节
    bytes32 public longBytes =
        0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;

    // 错误示例:直接用十进制整数
    // bytes1 public byte1Invalid = 12345; // 编译错误:Type uint256 is not implicitly convertible to bytes1

    // 示例函数:操作整数
    function updateIntegers(int8 newSmallInt, uint256 newLargeInt) public {
        smallSignedInt = newSmallInt; // 更新 int8
        largeUnsignedInt = newLargeInt; // 更新 uint256
    }

    // 示例函数:检查地址是否有效
    function isValidAddress(address addr) public pure returns (bool) {
        return addr != address(0); // 验证非零地址
    }

    // 示例函数:切换布尔值
    function toggleActive() public {
        isActive = !isActive; // 取反布尔值
    }

    // 示例函数:设置固定大小字节数组
    function setBytes32(bytes32 newBytes) public {
        longBytes = newBytes; // 更新 bytes32
    }

    // 遍历 bytes32,返回每个字节
    function traverseBytes32() public view returns (bytes1[] memory) {
        bytes1[] memory result = new bytes1[](32); // bytes32 固定 32 字节
        for (uint i = 0; i < 32; i++) {
            result[i] = longBytes[i]; // 访问第 i 个字节
        }
        return result;
    }

    // 获取 bytes1 的值(仅 1 字节,无需遍历)
    function getByte1() public view returns (bytes1) {
        return singleByte; // 直接返回
    }
}

2. 引用类型(Reference Types)

引用类型传递内存地址,主要包括:

  • 数组(Arrays)

    • 固定大小数组:大小固定,按值传递
      string[6] fixedArray;
      fixedArray[0] = "a"; // 有效
      fixedArray[6] = "g"; // 错误:超出范围
    • 动态大小数组:大小可变,支持 push()pop(),按引用传递
      uint[] dynamicArray;
      dynamicArray.push(1); // [1]
      dynamicArray.pop();   // []
  • 字节(Bytes)

    • 动态大小字节数组(byte[]),按引用传递,适合高效存储
    • 示例:
      function stringIntoBytes(string memory input) public pure returns (bytes memory) {
      return bytes(input); // "Zubin" -> 0x5a7562696e
      }
  • 字符串(Strings)

    • 动态大小字节数组,存储 UTF-8 编码字节序列,非原语类型
    • 示例:string name = "Solidity";
  • 结构体(Structs)

    • 自定义复合类型,包含多种数据类型的属性
      struct Person {
      string name;
      uint age;
      bool isSolidityDev;
      Job job;
      }
      struct Job {
      string employer;
      string department;
      bool isRemote;
      }
      Person p = Person("Zubin", 41, true, Job("Chainlink Labs", "DevRel", true));
  • 映射(Mappings)

    • 键值对数据结构,键类型有限,值类型可为原语、结构体或映射
    • 默认值由值类型决定,例如 mapping(address => uint256) balances; 默认值为 0
      mapping(address => uint256) public balances;
      function balanceOf(address account) public view returns (uint256) {
      return balances[account]; // 不存在返回 0
      }
    • 限制:无法枚举键,不能作为公共函数参数(除非为 internal

3. 值类型与引用类型的区别

  • 值类型:复制数据,修改不影响原值。
  • 引用类型:传递内存地址,修改影响原数据。
  • 引用类型需指定存储位置(storagememory),例如:
    function inMemArray(string memory firstName) public pure returns (string[] memory) {
      string[] memory arr = new string[](2);
      arr[0] = firstName;
      return arr;
    }

Solidity 的类型系统避免了 JavaScript 动态类型(如 1 + "2" = "12")的错误,提高了代码可靠性。

代码示例

pragma solidity ^0.8.0;

contract ReferenceTypesExample {
    // 动态大小数组:存储 uint 类型,按引用传递
    uint[] public dynamicArray;

    // 字符串:动态大小字节数组,存储 UTF-8 编码,按引用传递
    string public name = "Solidity";

    // 字节:动态大小字节数组,按引用传递
    bytes public data = hex"123456";

    // 结构体:自定义复合类型
    struct Person {
        string username;
        uint age;
        address wallet;
    }

    // 映射:键值对,address 映射到 Person 结构体
    mapping(address => Person) public users;

    // 初始化动态数组
    function initArray(uint value) public {
        dynamicArray.push(value); // 添加元素
        if (dynamicArray.length > 1) {
            dynamicArray[0] = value; // 修改第一个元素
        }
    }

    // 更新字符串
    function setName(string memory newName) public {
        name = newName; // 更新字符串
    }

    // 字节与字符串互转
    function stringToBytes(
        string memory input
    ) public pure returns (bytes memory) {
        return bytes(input); // 转换为字节
    }

    // 添加用户到映射
    function addUser(string memory username, uint age) public {
        users[msg.sender] = Person(username, age, msg.sender);
    }

    // 获取用户信息
    function getUser(
        address userAddress
    ) public view returns (string memory, uint, address) {
        Person memory user = users[userAddress];
        return (user.username, user.age, user.wallet);
    }

    // 在内存中创建固定大小数组
    function createMemoryArray(
        string memory input
    ) public pure returns (string[] memory) {
        string[] memory tempArray = new string[](2); // 固定大小
        tempArray[0] = input;
        tempArray[1] = "default";
        return tempArray;
    }
}

2. 函数修饰符(Function Modifier)

1. 什么是函数修饰符?

函数修饰符是 Solidity 中封装可重用逻辑的代码块,通常在函数执行前后运行验证或检查逻辑。修饰符类似函数,使用 _(下划线)指定主函数执行位置。

优点:

  • 减少代码重复,遵循 DRY 原则
  • 提高代码可读性和可维护性
  • 可从父合约继承

2. 修饰符的写法与下划线的作用

示例:确保只有合约所有者调用函数:

modifier onlyOwner() {
    require(msg.sender == owner, "Not the owner");
    _; // 主函数代码在此执行
}

function changeOwner(address newOwner) public onlyOwner {
    owner = newOwner;
}
  • require_ 前运行,确保调用者是 owner
  • _ 表示主函数(changeOwner)的插入点
  • require_ 后,主函数先执行,可能导致逻辑错误

修饰符可接受参数:

modifier validAddress(address addr) {
    require(addr != address(0), "Address invalid");
    _;
}

function transferTokenTo(address someAddress) public validAddress(someAddress) {
    // 转账逻辑
}

修饰符封装通用验证逻辑(如权限、地址有效性),便于在多个函数中重用。

代码示例:

pragma solidity ^0.8.0;

contract FunctionModifierExample {
    address public owner; // 合约所有者
    uint public value; // 存储数值
    bool public paused; // 合约暂停状态

    // 构造函数:设置部署者为所有者
    constructor() {
        owner = msg.sender;
    }

    // 修饰符:仅允许所有者调用
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _; // 主函数插入点
    }

    // 修饰符:检查合约未暂停
    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    // 修饰符:验证地址非零
    modifier validAddress(address addr) {
        require(addr != address(0), "Invalid address");
        _;
    }

    // 使用 onlyOwner 修饰符更新数值
    function setValue(uint newValue) public onlyOwner whenNotPaused {
        value = newValue;
    }

    // 使用 onlyOwner 切换暂停状态
    function togglePause() public onlyOwner {
        paused = !paused;
    }

    // 使用 validAddress 修饰符转移所有权
    function transferOwnership(
        address newOwner
    ) public onlyOwner validAddress(newOwner) {
        owner = newOwner;
    }

    // 获取当前状态
    function getStatus() public view returns (address, uint, bool) {
        return (owner, value, paused);
    }
}

3. Solidity 中的异常处理:require/assert/revert

Solidity 使用 requireassertrevert 处理错误。错误发生时,EVM 回滚状态更改,返回未使用 gas(delegatecallcall 等除外)。

1. require

require 验证条件,若为假,抛出错误并回滚交易,用于输入验证、返回值检查等。

function requireExample() public payable {
    require(msg.value >= 1 ether, "You must pay at least 1 ether!");
}
  • msg.value < 1 ether,抛出错误并回滚。
  • 错误消息可选,建议提供以便调试。
  • 优点:返回未使用 gas,建议尽早使用。

2. assert

assert 类似 require,抛出 Panic(uint256) 错误,用于检查不变量或内部状态。

contract ThrowMe {
    function assertExample() public pure {
        assert(address(this).balance == 0); // 确保余额为 0
    }
}
  • 用于测试不可能出现的错误。
  • 在 Solidity v0.8 前耗尽 gas,现与 require 类似。
  • 常用于内部函数或关键不变量检查。

3. revert

revert 用于复杂条件,支持自定义错误,gas 消耗低且信息丰富。

contract ThrowMe {
    error ThrowMe_BadInput(string errorMsg, uint inputNum);

    function revertExample(uint input) public pure {
        if (input < 1000) {
            revert ThrowMe_BadInput("Number must be > 999", input);
        }
        if (input < 0) {
            revert("Negative numbers not allowed");
        }
    }
}
  • 自定义错误提高可读性和可追溯性。
  • 适合嵌套条件逻辑。
  • 返回未使用 gas。

4. 注意事项

  • 尽早验证:将 require 放在函数开头,减少 gas 浪费。
  • 自定义错误revert 抛出自定义错误,节省 gas。
  • try-catch:支持捕获外部调用错误,未捕获错误会回滚状态。
  • 底层调用callsend 返回 false,需手动检查。

5. 代码示例:

pragma solidity ^0.8.0;

contract ErrorHandlingExample {
    uint public balance;
    address public owner;

    // 自定义错误:提高 gas 效率和可读性
    error InsufficientBalance(uint requested, uint available);
    error InvalidInput(string message, uint value);

    constructor() {
        owner = msg.sender;
    }

    // 使用 require 验证输入
    function deposit(uint amount) public {
        require(amount > 0, "Amount must be greater than zero");
        balance += amount;
    }

    // 使用 assert 检查不变量
    function checkInvariant() public view {
        assert(balance >= 0); // 余额永远不应为负
    }

    // 使用 revert 和自定义错误处理复杂逻辑
    function withdraw(uint amount) public {
        if (amount > balance) {
            revert InsufficientBalance(amount, balance);
        }
        if (amount == 0) {
            revert InvalidInput("Withdrawal amount cannot be zero", amount);
        }
        balance -= amount;
    }

    // 使用 require 限制仅所有者操作
    function resetBalance() public {
        require(msg.sender == owner, "Only owner can reset balance");
        balance = 0;
    }

    // 获取当前余额
    function getBalance() public view returns (uint) {
        return balance;
    }
}

4. Solidity 中的类型转换

类型转换(Type Casting)是将一种数据类型显式转换为另一种类型。Solidity 是静态类型语言,类型转换需谨慎,以避免数据丢失或意外结果。

1. 显式类型转换

Solidity 允许某些类型之间的显式转换。例如,将 uint256 转换为 bytes32

contract Conversions {
    function uintToBytes() public pure returns (bytes32) {
        uint256 a = 2022;
        bytes32 b = bytes32(a);
        return b; // 0x00000000000000000000000000000000000000000000000000000000000007e6
    }
}
  • uint256(256 位,32 字节)转换为 bytes32(32 字节),无数据丢失
  • b 的值是 2022 的 256 位二进制表示,填充前导零

2. 数据丢失风险

将大位数类型转换为小位数类型可能导致数据丢失。例如:

contract Conversions {
    function explicit256To8() public pure returns (uint8) {
        uint256 a = 2022; // 二进制:11111100110 (11 位)
        uint8 b = uint8(a); // 仅取最后 8 位:11100110
        return b; // 230
    }
}
  • 2022 的二进制表示为 11111100110(11 位),但 uint8 仅保留最后 8 位(11100110),十进制为 230
  • 数据丢失原因是高位被截断,开发者需确保转换合理

3. 有符号与无符号转换

从有符号整数(int)到无符号整数(uint)转换可能导致意外结果:

contract Conversions {
    function unsignedToSigned() public pure returns (int16, uint16) {
        int16 a = -2022;
        uint16 b = uint16(a); // -2022 转换为 63514
        return (a, b);
    }
}
  • int16-2022 转换为 uint16 时,负号信息丢失,二进制被解释为正数 63514。
  • 某些转换(如 int16uint256)会因负号不兼容而编译失败。

4. 注意事项

  • 数据丢失:小位数转换可能截断数据,需验证结果。
  • 符号问题:有符号到无符号转换可能改变数值含义。
  • 显式转换:Solidity 不支持隐式转换,需明确使用类型构造函数(如 uint8(a))。
  • 测试代码以确保转换后值符合预期。

5. 代码示例:

pragma solidity ^0.8.0;

contract TypeCastingExample {
    // 演示不同类型转换
    bytes1 public byte1Hex = 0x0A; // 十六进制,十进制 10
    uint256 public largeNumber = 2022;
    int16 public signedNumber = -2022;

    // 将 uint 转换为 bytes1(需显式转换)
    function setByte1FromUint(uint8 value) public {
        require(value <= 255, "Value exceeds bytes1 range");
        byte1Hex = bytes1(value); // 显式转换为 1 字节
    }

    // 将 uint 转换为 bytes2(适合更大值如 12345)
    function uintToBytes2(uint16 value) public pure returns (bytes2) {
        return bytes2(value); // 转换为 2 字节
    }

    // 示例:将 12345 转换为 bytes2
    function getBytes12345() public pure returns (bytes2) {
        return bytes2(uint16(12345)); // 12345 的字节表示,0x3039
    }

    // uint256 转换为 bytes32
    function uintToBytes() public view returns (bytes32) {
        bytes32 result = bytes32(largeNumber);
        return result; // 0x000...07e6 (2022 的 32 字节表示)
    }

    // uint256 转换为 uint8,可能丢失数据
    function uint256ToUint8() public view returns (uint8) {
        uint8 result = uint8(largeNumber); // 2022 -> 230 (仅保留最后 8 位)
        return result;
    }

    // int16 转换为 uint16,可能导致意外结果
    function signedToUnsigned() public view returns (uint16) {
        uint16 result = uint16(signedNumber); // -2022 -> 63514 (负号丢失)
        return result;
    }

    // 地址转换为 uint256
    function addressToUint(address addr) public pure returns (uint256) {
        return uint256(uint160(addr)); // 地址 (160 位) 转为 uint256
    }

    // 更新 largeNumber 以测试转换
    function setLargeNumber(uint256 newValue) public {
        largeNumber = newValue;
    }

    // 更新 signedNumber 以测试转换
    function setSignedNumber(int16 newValue) public {
        signedNumber = newValue;
    }
}

5. Solidity 中如何使用浮点数

Solidity 当前不支持浮点数(如 93.6),尝试声明会导致编译错误:

int256 floating = 93.6; // 错误:Type rational_const 468 / 5 is not implicitly convertible to int256

1. 浮点数处理方法

为处理浮点数,Solidity 开发者通过将小数转换为整数(乘以 10 的指数)来模拟:

  • 转换步骤

    • 将浮点数乘以 10 的 n 次方(n 为小数位数),得到整数。
    • 记录缩放因子(10^n),用于后续计算。
    • 示例:93.6 乘以 10 得到 936;93.2355 乘以 10^4 得到 932355。
  • 以太坊中的应用

    • 1 Ether = 10^18 wei。金额以 wei(整数)存储,避免浮点数。
    • 示例:1500000000000000000 wei 除以 10^18 得 1.5,但在 Solidity 中会报错:
      function divideBy1e18() public pure returns (int) {
      return 1500000000000000000 / 1e18; // 错误:无法处理 1.5
      }

2. 代码示例:

pragma solidity ^0.8.0;

contract FloatingPointExample {
    // 定义缩放因子,通常为 10^18(类似以太坊的 wei)
    uint256 public constant SCALING_FACTOR = 1e18; // 10^18,18 位小数精度

    // 存储余额(以最小单位 wei 表示,模拟浮点数)
    mapping(address => uint256) public balances;

    // 错误:用于验证输入
    error InvalidAmount(string message, uint256 value);

    // 存款:输入 Ether 单位(浮点数),转换为 wei
    function deposit(uint256 etherAmount) public {
        if (etherAmount == 0) {
            revert InvalidAmount(
                "Deposit amount must be greater than zero",
                etherAmount
            );
        }
        // 模拟浮点数:将 Ether 单位乘以 10^18 转为 wei
        uint256 weiAmount = etherAmount * SCALING_FACTOR;
        balances[msg.sender] += weiAmount;
    }

    // 提取:输入 Ether 单位,转换为 wei 进行操作
    function withdraw(uint256 etherAmount) public {
        if (etherAmount == 0) {
            revert InvalidAmount(
                "Withdrawal amount must be greater than zero",
                etherAmount
            );
        }
        uint256 weiAmount = etherAmount * SCALING_FACTOR;
        require(balances[msg.sender] >= weiAmount, "Insufficient balance");
        balances[msg.sender] -= weiAmount;
    }

    // 获取余额:以 Ether 单位返回(模拟浮点数)
    function getBalanceInEther(address user) public view returns (uint256) {
        return balances[user] / SCALING_FACTOR; // 转换为 Ether 单位
    }

    // 示例:处理更精确的小数(例如 93.2355)
    function addPreciseAmount(uint256 amount, uint8 decimals) public {
        // amount 是整数部分,decimals 是小数位数
        // 例如:93.2355 -> amount = 932355, decimals = 4
        require(decimals <= 18, "Decimals exceed maximum precision");
        uint256 scalingFactor = 10 ** decimals;
        uint256 scaledAmount = (amount * SCALING_FACTOR) / scalingFactor;
        balances[msg.sender] += scaledAmount;
    }

    // 示例:计算利息(模拟浮点数运算)
    function calculateInterest(
        address user,
        uint256 rate
    ) public view returns (uint256) {
        // rate 是百分比的整数表示,例如 5% = 500(5.00%)
        // 转换为浮点数:rate / 10000
        uint256 interest = (balances[user] * rate) / 10000 / SCALING_FACTOR;
        return interest; // 返回 Ether 单位的利息
    }

    // 示例:尝试直接使用浮点数(将导致编译错误)
    // function invalidFloat() public pure returns (int256) {
    //     int256 floating = 93.6; // 错误:Type rational_const 468 / 5 is not implicitly convertible to int256
    //     return floating;
    // }
}

3. 前端处理

Solidity 返回大整数(如 10^18 级别的 wei),需在前端使用库(如 ethers.js)处理:

  • 将大整数除以 10^18,转换为人类可读的浮点数。
  • 示例:ethers.formatEther(1500000000000000000) 返回 "1.5" (v6 版本)

注意事项

  • 精度问题:Solidity 不支持浮点数,需手动管理缩放因子
  • 大整数:使用 uint256 存储大值,避免溢出
  • 前端库:依赖 Ethers.js 或 Web3.js 处理大整数和浮点数转换
  • 未来 Solidity 可能支持固定点类型(fixed/ufixed),但目前未完全实现

代码仓库https://github.com/BraisedSix/Solidity-Learn

总结

Solidity 的静态类型系统、函数修饰符、异常处理、类型转换和浮点数处理方法共同构成了开发可靠智能合约的基础。严格的类型系统避免了动态类型语言的错误;修饰符提高了代码复用性;requireassertrevert 提供了灵活的错误处理;类型转换需注意数据丢失和符号问题;浮点数通过整数模拟实现。掌握这些概念,开发者可以编写更安全、高效的以太坊智能合约。我是红烧6,关注我,实现你的大师梦,我们下期见!

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
BraisedSix
BraisedSix
0x6100...b2d4
一个热爱web3的篮球爱好者