本文深入探讨了Solidity的类型系统,重点介绍了值类型和引用类型,分析了常见的安全陷阱及防范措施,并详细讲解了数据存储位置(storage、memory、calldata)对Gas成本的影响以及优化策略。掌握这些概念对于在以太坊平台上开发安全、高效、健壮的智能合约至关重要。

在以太坊上构建智能合约需要扎实理解 Solidity 的类型系统。与 JavaScript 或 Python 不同,Solidity 是一种静态类型语言,并且具有一些独特的特性,可能会让来自其他语言的开发人员感到惊讶。
Solidity 的类型系统不仅仅是声明变量——它定义了你的合约如何运行,需要花费多少 gas,以及它的安全性(或脆弱性)。
💡 如果你不理解数据是如何存储和使用的,你将会浪费过多的 gas,或者更糟... 让你的合约存在漏洞。
在本文中,我们将分解 Solidity 中的值类型和引用类型,解释常见的安全陷阱以及如何避免它们,并深入了解数据位置(storage、memory、calldata)——它如何影响 gas 成本以及如何优化它。
Solidity 中的值类型是指直接在定义它们的位置存储数据的变量。当这些变量在赋值中使用或作为函数参数传递时,它们的值会被复制。
bool - true 或 falseint/uint (有符号/无符号,变体从 8 到 256 位 - 例如 uint8, int128, uint256)fixed/ufixed (由于限制,很少使用)address 和 address payable)bytes1, bytes32)例子:
function valueTypeExample() public pure returns (uint) {
    uint a = 5;
    uint b = a; // 值被复制
    b = 10;     // 改变 b 不会影响 a
    return a;   // 返回 5
}引用类型不会直接在变量中存储它们的值。相反,它们存储一个指向数据所在位置的引用(指针)。当引用类型被赋值或作为函数参数传递时,传递的是对相同数据的引用,而不是数据本身的副本。
uint[5], bytes[])string (专门的动态字节数组)mapping(address => uint))例子:
function referenceTypeExample() public pure returns (uint) {
    uint[] memory array = new uint[](1);
    array[0] = 5;
    uint[] memory arrayCopy = array; // 引用被复制
    arrayCopy[0] = 10;              // 也会改变 array[0]
    return array[0];                // 返回 10
}在 Solidity 0.8.0 之前,算术运算可能会溢出或下溢而不会回滚。
易受攻击的代码:
// 在 Solidity <0.8.0 中
function vulnerable(uint8 a, uint8 b) public pure returns (uint8) {
    return a + b; // 可能会溢出
}预防:
// 在 Solidity <0.8.0 中
function safe(uint8 a, uint8 b) public pure returns (uint8) {
    return SafeMath.add(a, b); // 溢出时会回滚
}在 Solidity 中,除以零会导致回滚。
预防:
function safeDivide(uint a, uint b) public pure returns (uint) {
    require(b > 0, "Division by zero");
    return a / b;
}不正确地使用 storage 指针可能导致意外的数据修改。
易受攻击的代码:
function vulnerable() public {
    MyStruct storage localVar = myStructs[0]; // Storage 引用
    // 之后对 localVar 的修改会修改合约 storage
}预防:
memory:function safe() public {
    MyStruct memory localVar = myStructs[0]; // 创建一个 memory 副本
    // 对 localVar 的修改不会影响 storage
}Storage 中的动态数组具有 .length 属性,可以被操作,可能导致越界访问。
易受攻击的代码:
function vulnerable() public {
    uint[] storage myArray = storageArray;
    myArray.length = 0; // 重置数组长度,但不清除 storage
    // 数据仍然可以通过汇编访问
}预防:
function safe() public {
    delete storageArray; // 清除数组的正确方法
}Solidity 为引用类型提供了三种数据位置:storage、memory 和 calldata。选择会影响语义和 gas 成本。
contract StorageExample {
    // 状态变量默认存储在 storage 中
    uint[] public storageArray;
    function manipulateStorage() public {
        // 局部 storage 引用
        uint[] storage localRef = storageArray;
        // 修改合约的 storage
        localRef.push(42);
    }
}function memoryExample(uint[] memory memoryArg) public pure returns (uint[] memory) {
    // 在 memory 中创建新数组
    uint[] memory result = new uint[](memoryArg.length);
    // 操作 memory 数组
    for (uint i = 0; i < memoryArg.length; i++) {
        result[i] = memoryArg[i] * 2;
    }
    return result; // 返回 memory 数组
}function calldataExample(uint[] calldata data) external pure returns (uint) {
    // 来自 calldata 的数据 - 只读
    uint sum = 0;
    for (uint i = 0; i < data.length; i++) {
        sum += data[i];
    }
    return sum;
}按 gas 成本从低到高排列数据位置的优先级:
calldata(外部函数)memorystorage// Gas 效率低:隐式地将 calldata 复制到 memory
function inefficient(string memory s) external pure returns (string memory) {
    return s;
}
// Gas 效率高:将数据保存在 calldata 中
function efficient(string calldata s) external pure returns (string calldata) {
    return s;
}// Gas 效率低:使用 storage 存储临时数据
function inefficient() public {
    uint[] storage tempArray = storageArray;
    // 对 tempArray 的操作会修改 storage
}
// Gas 效率高:使用 memory 存储临时数据
function efficient() public view returns (uint) {
    uint[] memory tempArray = new uint[](storageArray.length);
    for (uint i = 0; i < storageArray.length; i++) {
        tempArray[i] = storageArray[i];
    }
    // 对 tempArray 的操作不会修改 storage
    // 处理 tempArray...
}从 storage 读取比写入便宜得多。在 memory 中缓存 storage 值以供多次读取。
// Gas 效率低:多次 storage 读取
function inefficient() public {
    for (uint i = 0; i < 100; i++) {
        // 每次迭代都从 storage 读取 storageArray.length
        if (i < storageArray.length) {
            // 做一些事情
        }
    }
}
// Gas 效率高:缓存 storage 读取
function efficient() public {
    // 读取一次并存储在 memory 中
    uint length = storageArray.length;
    for (uint i = 0; i < 100; i++) {
        if (i < length) {
            // 做一些事情
        }
    }
}memory vs. storage// Gas 效率低:在循环中重复访问 storage
function inefficient() public {
    for (uint i = 0; i < storageArray.length; i++) {
        storageArray[i] = storageArray[i] * 2;
    }
}
// Gas 效率高:使用 memory 进行中间操作
function efficient() public {
    uint[] memory memArray = new uint[](storageArray.length);
    // 复制到 memory
    for (uint i = 0; i < storageArray.length; i++) {
        memArray[i] = storageArray[i] * 2;
    }
    // 单次写回 storage(仍然很昂贵,但操作较少)
    for (uint i = 0; i < storageArray.length; i++) {
        storageArray[i] = memArray[i];
    }
}// Gas 效率高的修饰符用法:
// external < public < internal < private
// Gas 效率最低
function inefficient(uint[] memory data) public pure returns (uint) {
    return processData(data);
}
// 对于外部调用,Gas 效率最高
function efficient(uint[] calldata data) external pure returns (uint) {
    return processData(data);
}理解 Solidity 的类型系统和数据位置对于编写安全且 gas 效率高的智能合约至关重要:
storage:持久但昂贵 memory:临时且成本适中 calldata:只读,外部函数最便宜的选择通过掌握这些概念,你将在以太坊平台上开发出更安全、高效和健壮的智能合约。
- 原文链接: coinsbench.com/understan...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!