[Web3] 智能合约 02: Solidity 基础语法

一.主要内容

  • 数据类型

  • 循环分支

  • 合约函数

  • 合约事件

  • 抽象合约与接口

  • 异常处理

  • 变量作用域与数据存储

  • Solidty 内存布局

二.合约数据类型

数据类型的概览

【图示 1】原文此处为流程图/白板图示,因平台导出限制未能搬运。

源文档: https://w2z1amg2x5.sg.larksuite.com/docx/OU2OddJLloBYRbxd72glBbn5gFf

引用类型和值类型的区别

值类型:是指直接存储在合约里面的状态变量或者函数栈中数据类型,与引用类型不一样的是值类型赋值和传递的过程通过复制整个数据对象的值进行的,而不是引用或者指针。

引用类型:引用类型是一种特殊的数据类型,用于存储复杂的数据结构或者允许通过引用传递数据类型,与值类型不一样的是,引用类型的赋值和传递是通用引用或者指针进行的,在内存中是通引用地址来操作。

运算符

逻辑运算符
  • 逻辑与 &&

  • 逻辑或 ||

  • 逻辑非!

和其他语言是一样的,&& 和 || 运算遵循短路原则

数学运算

+, -, , /, %,*

位运算

移位与 &

移位或 |

比较运算符(返回布尔值)

<=, <,==, !=, >=, >

值类型

Bool 类型
  • Bool 数据类型的定义

    bool 修饰 变量名
    bool public isActive

  • 初始化变量

    bool public isActive = true/false

  • 布尔值在逻辑运算符里面的使用

    bool a = true
    bool b = false

代码案例
  • ContractsBasic 合约
solidity 复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

contract ContractsBasic {
    bool public aVar;
    bool public bVar;

    // 设置两个布尔状态变量
    function setAB(bool _a, bool _b) public {
        aVar = _a;
        bVar = _b;
    }

    // 逻辑与:两个都为 true 才返回 true
    function compareAA() public view returns (bool) {
        return aVar && bVar;
    }

    // 逻辑或
    function compareOr() public view returns (bool) {
        return aVar || bVar;
    }

    // 逻辑非
    function compareNot() public view returns (bool) {
        return !aVar;
    }

    // 短路原则演示:aVar 为 false 时不会执行右侧表达式
    function shortCircuit() public view returns (bool) {
        return aVar && _alwaysRevert();
    }

    function _alwaysRevert() internal pure returns (bool) {
        revert("should not be reached");
    }
}
  • ContractsBasic 测试合约
solidity 复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {ContractsBasic} from "./ContractsBasic.sol";

import "forge-std/src/Test.sol";
import "forge-std/src/console.sol";

contract CounterTest is Test {
  ContractsBasic cb;

  function setUp() public {
    cb = new ContractsBasic();
  }

  function test_SetAB() public {
    cb.setAB(false, false);

    bool a = cb.aVar();
    bool b = cb.bVar();

    console.log("a is", a);
    console.log("b is", b);
  }

  function test_compareAA() public {
    bool aa = cb.compareAA();
    console.log("aa is", aa);
  }
整形

在 solidity 中,整形用于表示整数,支持有符号和无符号类型, 整形支持数算运算符和移位运算符的计算

无符号类型

无符号表示的是非负整数,关键字 uint, 宽度从 uint8 到 uint256 , 中间加 8 位(例如 uint8, uint16, uint32 ... uint256), 如果定义的 uint, 默认是 uint256。

复制代码
uint + 位数  修饰符  变量名字

uint256 public a
uint32 public b
uint16 public c
uint8 public d
有符号类型

有符号类型表示正负数,关键字 int , 宽度从 int8 到 int256 , 中间加 8 位, 例如 int8, int16, uint32 ... int256), 如果定义的 int, 默认是 int256。

复制代码
int + 位数  修饰符  变量名字

int256 public a
int32 public b
int16 public c
int8 public d
溢出和下溢

在 solidty 早期版本,指的是 0.8.0 之前的版本,整数的溢出是一个常见的问题,需要人为处理这个问题,如果你项目还在使用 0.8.0 之前的版本,需要使用别人封住好的 SafeMath 库; 在 0.8.0 之后的版本里面,智能合约内部把这个问题解决了。

solidity 复制代码
pragma solidity ^0.8.0;

contract OverflowExample {
    uint8 public maxUint8 = 255;

    function increment() public {
        maxUint8 += 1; // 这将导致运行时错误,因为 uint8 的最大值是 255
    }
}
  • Uniswap V2 的 SafeMath 库
solidity 复制代码
library SafeMath {
    function add(uint x, uint y) internal pure returns (uint z) {
        require((z = x + y) >= x, 'ds-math-add-overflow');
    }
常用的整型运算符包括:
  • 比较运算符(返回布尔值): <=, <,==, !=, >=, >

  • 算数运算符: +, -, *, /, %(取余),**(幂)

代码案例
solidity 复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

contract IntTypeContract {
    uint256 public a = 100;
    uint8 public b = 255;
    int256 public c = -100;

    // 加法
    function Add() public view returns (uint256) {
        return a + 1;
    }

    // 减法
    function Sub() public view returns (uint256) {
        return a - 1;
    }

    // 乘法
    function Mul() public view returns (uint256) {
        return a * 2;
    }

    // 除法(整数除法会向下取整)
    function Div() public view returns (uint256) {
        return a / 3;
    }

    // 取余
    function Mod() public view returns (uint256) {
        return a % 3;
    }

    // 幂运算
    function Pow() public view returns (uint256) {
        return a ** 2;
    }

    // 0.8.0 之后溢出会直接 revert
    function overflow() public view returns (uint8) {
        return b + 1;
    }
}
  • Test
solidity 复制代码
function test_Add() public{
    uint256 cc = ibc.Add();
    console.log("cc is", cc);
}
地址类型

在 Solidity 中,地址类型 (address) 是一种特殊的数据类型,用于存储 Ethereum 地址。一个 Ethereum 地址是一个 20 字节(160 位)的值,通常用于表示账户(包括外部账户和合约账户)。地址类型有一些特定的功能和方法,允许开发者与区块链上的账户进行交互。

地址的声明和初始化
  • 地址声明方式

    address 修饰符 变量名字

    address public reciptant

  • 声明并初始化

    address public reciptant = address(0);

    address public reciptant = 0xbdcc079BBb23C1D9a6F36AA31309676C258aBAC7

地址特定的方法
  • 获取 balance
solidity 复制代码
// 获取地址余额
function getBalance(address _addr) public view returns (uint256) {
    return _addr.balance;
}

// 获取当前调用者余额
function getSenderBalance() public view returns (uint256) {
    return msg.sender.balance;
}

// 获取合约自身余额
function getContractBalance() public view returns (uint256) {
    return address(this).balance;
}
  • address 与 address payable, 在 Solidity 中,address 类型的变量不能接收或发送 Ether。如果需要进行这些操作,需要使用 address payable 类型。

    address payable public reciptant = 0x45Bd8ea16cFEB0D937a2D98cBEb0300e3E689Fe7

  • 发送 ETH

    address payable public reciptant = 0x45Bd8ea16cFEB0D937a2D98cBEb0300e3E689Fe7

    payable(reciptant).transfer(1 ether);
    payable(reciptant).send(1 ether);
    payable(recipient).call{value: 1 ether}("");

    bool success = recipient.send(1 ether);
    require(success, "Transfer failed");

    (bool success, ) = recipient.call{value: 1 ether}("");
    require(success, "Transfer failed");

  • 面试重点

代码案例
  • 一个完整的 address 的类型代码示范
solidity 复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

contract AddressTypeContract {
    address public usdtTokenAddress;
    address payable public recipient;

    event Deposit(address indexed from, uint256 amount);
    event SetRecipient(address indexed recipient);

    constructor(address _usdtTokenAddress) {
        usdtTokenAddress = _usdtTokenAddress;
        recipient = payable(msg.sender);
    }

    // 设置收款地址
    function setRecipient(address payable _recipient) public {
        recipient = _recipient;
        emit SetRecipient(_recipient);
    }

    // 查询余额
    function getBalance(address _addr) public view returns (uint256) {
        return _addr.balance;
    }

    // transfer:失败会 revert,固定 2300 gas
    function transferETH(uint256 _amount) public {
        require(address(this).balance >= _amount, "insufficient balance");
        payable(recipient).transfer(_amount);
    }

    // send:返回 bool,失败不 revert,固定 2300 gas
    function sendETH(uint256 _amount) public returns (bool) {
        require(address(this).balance >= _amount, "insufficient balance");
        bool success = payable(recipient).send(_amount);
        require(success, "Transfer failed");
        return success;
    }

    // call:推荐方式,可携带 gas,返回 bool 与数据
    function callETH(uint256 _amount) public returns (bool) {
        require(address(this).balance >= _amount, "insufficient balance");
        (bool success, ) = recipient.call{value: _amount}("");
        require(success, "Transfer failed");
        return success;
    }

    // 接收 ETH
    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    // 存款并校验 msg.value
    function deposit(uint256 _amount) public payable {
        require(msg.value == _amount, "amount mismatch");
        emit Deposit(msg.sender, msg.value);
    }
}
  • Test
solidity 复制代码
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {Test} from "forge-std/src/Test.sol";
import {console} from "forge-std/src/console.sol";
import {AddressTypeContract} from "./AddressTypeContract.sol";

contract AddressTypeTest is Test {
    AddressTypeContract atc;
    address payable recipient;

    function setUp() public {
        atc = new AddressTypeContract(0x55d398326f99059fF775485246999027B3197955);
        recipient = payable(makeAddr("recipient"));
        vm.deal(address(atc), 10 ether);
    }

    function test_ConstructorSetsUsdtTokenAddress() public view {
        assertEq(atc.usdtTokenAddress(), 0x55d398326f99059fF775485246999027B3197955);
    }

    function test_SetRecipientUpdatesRecipientAndEmitsEvent() public {
        vm.expectEmit(true, false, false, false);
        emit AddressTypeContract.SetRecipient(recipient);
        atc.setRecipient(recipient);
        assertEq(atc.recipient(), recipient);
    }

    function test_TransferETHSendsAmountToRecipient() public {
        atc.setRecipient(recipient);
        uint256 before = recipient.balance;
        atc.transferETH(1 ether);
        assertEq(recipient.balance - before, 1 ether);
    }

    function test_SendETHSendsOneEtherToRecipient() public {
        atc.setRecipient(recipient);
        uint256 before = recipient.balance;
        bool ok = atc.sendETH(1 ether);
        assertTrue(ok);
        assertEq(recipient.balance - before, 1 ether);
    }

    function test_CallETHSendsOneEtherToRecipient() public {
        atc.setRecipient(recipient);
        uint256 before = recipient.balance;
        bool ok = atc.callETH(1 ether);
        assertTrue(ok);
        assertEq(recipient.balance - before, 1 ether);
    }

    function test_DepositAcceptsETHAndEmitsEvent() public {
        vm.deal(address(this), 1 ether);
        atc.deposit{value: 1.5 ether}(1.5 ether);
        assertEq(address(atc).balance, 11.5 ether);
    }

    function test_AddressBalanceReturnsRecipientBalance() public {
        vm.deal(recipient, 3 ether);
        assertEq(atc.getBalance(recipient), 3 ether);
    }
}
sql 复制代码
Ran 11 tests for contracts/ContractsBasic.t.sol:AddressTypeTest
[PASS] test_AddressBalanceReturnsRecipientBalance() (gas: 34973)
[PASS] test_CallETHRevertsWhenContractBalanceIsInsufficient() (gas: 69065)
[PASS] test_CallETHSendsOneEtherToRecipient() (gas: 71262)
[PASS] test_ConstructorSetsUsdtTokenAddress() (gas: 15884)
Logs:
  usdtTokenAddress is 0x55d398326f99059fF775485246999027B3197955

[PASS] test_DepositAcceptsETHAndEmitsEvent() (gas: 24456)
Logs:
  deposit eth is 1500000000000000000

[PASS] test_DepositRevertsWhenAmountExceedsMsgValue() (gas: 16380)
[PASS] test_ReceiveAcceptsETHAndEmitsEvent() (gas: 23660)
Logs:
  addressType balance is 1000000000000000000

[PASS] test_SendETHRevertsWhenContractBalanceIsInsufficient() (gas: 68821)
[PASS] test_SendETHSendsOneEtherToRecipient() (gas: 71084)
[PASS] test_SetRecipientUpdatesRecipientAndEmitsEvent() (gas: 36737)
[PASS] test_TransferETHSendsAmountToRecipient() (gas: 71600)
Suite result: ok. 11 passed; 0 failed; 0 skipped; finished in 7.32ms (18.86ms CPU time)
地址类型作业
  • 自己编写一个地址类型,完成地址类型相关的方法测试,理解地址类型 ETH 收发的不同方式,以及测试用例里面 eth 的产生过程。
定长字节数组

定长字节数组是一种特定长度的字节序列,它们的长度在声明时就被固定。Solidity 提供了几种不同长度的定长字节数组类型,例如 bytes1 到 bytes32。

复制代码
bytes1--byte32 修饰符 变量名字

bytes1 public byteArray1
bytes2 public byteArray2
bytes3 public byteArray3

....

bytes32 public byteArray32

这些类型都是用来存储固定长度的字节序列的。它们在声明时会占据固定的存储空间,而不像动态大小的字节数组那样可以动态调整大小。

代码的案例
java 复制代码
contract BytesTypeContracts {
    bytes32 public bytes1Array1 = hex"abcdef";
    bytes32 public bytes2Array2 = hex"abcdee";

    function getBytesLength() public view returns (uint256) {
        return bytes1Array1.length;
    }

    function getBytesByIndex(uint256 index) public view returns (bytes1) {
        require(index < bytes2Array2.length, "index out of bounds");
        return bytes2Array2[index];
    }

    function compareBytes() public view returns (bool) {
      return keccak256(abi.encodePacked(bytes1Array1)) == keccak256(abi.encodePacked(bytes2Array2));
    }
}
  • Test 文件
c++ 复制代码
contract BytesTypeTest is Test {
    BytesTypeContracts public btc;

    function setUp() public {
        btc = new BytesTypeContracts();
    }
  • 注意事项
枚举

枚举是一种用户定义的数据类型,用于定义一组变量,枚举类型允许开发者在代码中以易读的方式引用这些常量,而不必直接使用数值或字符串。

枚举的定义

通过 enum 关键字定义,代码案例

java 复制代码
contract EnumTypeContracts {
    enum BettorType {
        Big,
        Small,
        Single,
        Double
    }

    BettorType public bettorType;

    function setBettorType(BettorType _bettorType) public {
        bettorType = _bettorType;
    }

    function getBettorBigType() public pure returns (uint8)  {
        return uint8(BettorType.Big);
    }

    function getBettorSmallType() public pure returns (uint8)  {
        return uint8(BettorType.Small);
    }
}
  • 测试代码
c++ 复制代码
contract EnumTypeContractsTest is Test {
    EnumTypeContracts public etc;

    function setUp() public {
        etc = new EnumTypeContracts();
    }

    function test_getEnum() public {
        uint8 big = etc.getBettorBigType();
        uint8 small = etc.getBettorSmallType();

        console.log("=======================");
        console.log(big);
        console.log(small);
        console.log("=======================");
    }
}
枚举值和默认值

枚举的每个成员都会自动分配一个整数索引,从 0 开始递增。在上面的例子中,Big 的值为 0,Small 的值为 1,以此类推。如果没有显式地指定枚举成员的值,它们将按顺序从 0 开始递增。

使用枚举的优势

枚举的主要优势在于增强代码的可读性和可维护性。例如,在状态机中,使用枚举可以更清晰地表示不同的状态。另外,枚举类型在合约中的内部使用也有助于避免使用硬编码的数值或字符串。

注意事项
  • 枚举类型在 Solidity 中是值类型,它们在存储和传递时是按值复制的。

  • 枚举成员名称必须是唯一的。

使用枚举类型可以使 Solidity 合约中的代码更加清晰和易于理解,特别是在涉及状态管理或者有限集合的情况下。

引用类型

结构体

结构体是一种用户自定义的复合数据类型,用户存储多个不同类型的数据,它是一组数据的结合;在 solidity 中属于引用类型,赋值和传递是通过引用进行。

结构体的定义

通过关键字 struct 定义,使用一个代码案例说明结构体的基本概念和使用

typescript 复制代码
contract StructType {
    struct Person {
        string name;
        uint256 age;
        bool isMale;
    }

    struct PersonInfo {
        address walletAddress;
        string university;
        string hometown;
        string homeAddress;
    }

    Person public user;
    Person public user2;

    PersonInfo public userInfo1;
    PersonInfo public userInfo2;

    function setPersonOne(string memory name, uint256 age, bool isMale) public {
        user.name = name;
        user.age = age;
        user.isMale = isMale;
        userInfo1.walletAddress = msg.sender;
        userInfo1.homeAddress = unicode"天津市西青区xxx";
typescript 复制代码
        (string memory name1, uint256 age1, bool isMale1)= st.getUser2();
        console.log(name1, age1, isMale1);

        (string memory user2name, uint256 user2age, bool user2isMale) = st.user2();
        console.log(user2name, user2age, user2isMale);

        (address walletAddress,string memory university, string memory hometown, string memory homeAddress) = st.userInfo1();
        console.log(walletAddress, university, hometown, homeAddress);
    }
}
  • 注意事项

结构体是 Solidity 中用于组织和管理数据的重要工具,特别适用于复杂数据结构的描述和操作。在设计合约时,合理使用结构体可以使代码更加清晰、模块化和易于维护。

数组

数组在 Solidity 中也被视为引用类型,特别是动态大小的数组。动态大小的数组允许在运行时动态增加或减少其长度,这意味着它们的操作是基于引用的。

定长数组 (typeN)

定长数组在声明时就需要指定数组的长度 N,且长度不能更改。定长数组通常用于固定大小的数据集合,如固定数量的数据元素存储。

typescript 复制代码
contract FixedArrayExample {
    uint[5] public fixedArray;

    function setFixedArray(uint index, uint value) public {
        require(index < 5, "Index out of bounds");
        fixedArray[index] = value;
    }

    function getFixedArray(uint index) public view returns (uint) {
        require(index < 5, "Index out of bounds");
        return fixedArray[index];
    }
}

在上面的示例中,fixedArray 是一个包含 5 个 uint 元素的定长数组。通过 setFixedArray 函数可以设置数组的元素值,通过 getFixedArray 函数可以获取指定索引处的元素值。

动态数组 (type\[\])

动态数组是在 Solidity 中比较常见和灵活的数组类型,它的长度可以在运行时动态增加或减少。

java 复制代码
contract DynamicArrayExample {
    uint[] public dynamicArray;

    function addElement(uint value) public {
        dynamicArray.push(value);
    }

    function getLength() public view returns (uint) {
        return dynamicArray.length;
    }

    function getElement(uint index) public view returns (uint) {
        require(index < dynamicArray.length, "Index out of bounds");
        return dynamicArray[index];
    }
}

在上面的示例中,dynamicArray 是一个动态大小的 uint 数组。通过 addElement 函数可以向数组末尾添加新的元素,通过 getLength 函数可以获取当前数组的长度,通过 getElement 函数可以获取指定索引处的元素值。

特点和使用注意事项
  • Gas 成本:动态数组在增加元素时会消耗 gas,因此对于大型数组或频繁修改的数组操作,需要注意 gas 成本问题。

  • 内存管理:动态数组在 Solidity 中会自动管理内存,无需手动释放或调整。

  • 返回值:在 Solidity 中,函数不能返回完整的动态数组,如果需要返回数组,则通常需要返回数组的长度和元素的部分内容,或者使用外部函数接口。

  • 多维数组:Solidity 支持多维数组,即数组中包含数组。

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MultiDimensionalArrayExample {
    uint[][] public multiArray;

    function addElement(uint[] memory values) public {
        multiArray.push(values);
    }

    function getLength() public view returns (uint) {
        return multiArray.length;
    }

    function getElement(uint row, uint col) public view returns (uint) {
        require(row < multiArray.length, "Row index out of bounds");
        require(col < multiArray[row].length, "Column index out of bounds");
        return multiArray[row][col];
    }
}

在上面的示例中,multiArray 是一个二维动态数组,可以通过 addElement 添加一维数组到二维数组中,并通过 getLength 和 getElement 函数获取二维数组的长度和指定位置的元素值。

数组是 Solidity 中重要的数据结构之一,用于存储和操作大量相同类型的数据。合理使用数组可以有效管理和操作数据集合,提高合约的灵活性和效率。

定长数组和动态数组使用案例
solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ArrayExample {

    uint256[] public dynamicArray;

    uint256[5] public fixedArray;

    function getDynamicArrayLength() public view returns (uint256) {
        return dynamicArray.length;
    }

    function getFixedArrayLength() public view returns (uint256) {
        return fixedArray.length;
    }
映射

映射是 Solidity 中的一种特殊数据结构,用于存储键值对。映射也被视为引用类型,因为它们在内存中使用引用地址来操作

定义映射

maping(key=>value) public 变量名字

Solidity 中的映射通过 mapping 关键字定义,如下所示:

java 复制代码
pragma solidity ^0.8.0;

contract MappingExample {
    // 定义映射,将地址映射到整数
    mapping(address => uint) public balances;

    // 设置地址对应的余额
    function setBalance(address _address, uint _balance) public {
        balances[_address] = _balance;
    }

    // 获取地址对应的余额
    function getBalance(address _address) public view returns (uint) {
        return balances[_address];
    }
}
映射的特性(面试题目)
  • 键类型:映射的键可以是任何值类型,包括基本类型(如 uint、address)。

  • 值类型:映射的值可以是任何 Solidity 支持的数据类型,包括基本类型、复合类型(如 struct)和数组类型。

  • 默认值:如果映射中不存在某个键对应的值,则返回该值类型的默认值。例如,uint 类型的默认值是 0,address 类型的默认值是 address(0)。

  • Gas 成本:读取映射中的值(查找操作)是 gas 消耗较低的操作,而写入映射中的值(更新操作)可能会消耗更多的 gas,特别是在扩展映射时可能需要大量的 gas。

映射的应用场景

映射在 Solidity 合约中有广泛的应用,特别是用于管理状态数据、存储关联数据和实现查找表。以下是一些常见的应用场景:

  • 存储账户余额:如上面示例所示,映射可以用来存储多个账户的余额信息。

  • ERC20 Token 实现:映射通常用于存储 ERC20 Token 的余额和授权信息。

  • 存储关联数据:映射可以用于存储任何需要通过某种键快速查找和访问的关联数据。

  • 索引和查找:映射可以作为索引来优化查找和检索操作,例如根据地址查找合约用户的信息。

注意事项
  • 映射与数组:映射和数组都是 Solidity 中常用的数据结构,但它们的适用场景不同。映射适合于需要快速查找和更新的情况,而数组适合于顺序访问和处理的情况。

  • 内存管理:映射中的键和值通常存储在存储区域(storage),这意味着它们在区块链上永久保存,并且可以在不同的合约函数之间共享和访问。

  • 映射的限制:Solidity 中的映射不支持遍历操作,即不能直接遍历映射中的所有键或值。如果需要遍历操作,通常需要结合其他数据结构或模式来实现。

映射是 Solidity 中重要且强大的数据结构之一,合理使用映射可以有效提高合约的效率和可维护性。在设计合约时,根据具体需求选择合适的数据结构非常重要。

变量与常量

在 Solidity 中,常量(Constants)和变量(Variables)是两种不同的数据存储方式,它们在合约中的使用和特性有所不同。下面将分别介绍 Solidity 中的常量和变量,并举例说明它们的使用方法和注意事项。

常量 (Constants)

常量在 Solidity 中是指一旦声明后其值无法更改的数据。常量的声明方式是使用 constant 或 immutable 关键字。主要特点包括:

  • 初始化后不可更改:常量在声明时需要立即赋值,并且其值在合约的生命周期内保持不变。

  • 编译时确定:常量的值在编译时确定,并且在部署时被写入合约的字节码中。

  • 不占用存储空间:常量不占用合约的存储空间,而是在代码中直接使用其值。

下面是常量的示例:

solidity 复制代码
contract ConstantsExample {
    // 声明常量
    uint constant public MAX_NUMBER = 100;
    address constant public OWNER = 0x1234567890123456789012345678901234567890;
    string constant public VERSION = "1.0.0";

    // 常量不可更改,以下语句会引发编译错误

在上面的示例中:

  • MAX_NUMBER、OWNER 和 VERSION 都是常量,它们分别代表最大数值、合约拥有者的地址和版本号,它们的值在声明时确定,并且无法在合约内部更改。
变量 (Variables)

变量是 Solidity 中用于存储和管理数据的可变容器。主要特点包括:

  • 可变性:变量在声明后可以根据需要进行修改和更新。

  • 存储空间:变量会占用合约的存储空间,可以存储状态和临时数据。

  • 作用域:变量可以是合约级别的状态变量,也可以是函数内的局部变量。

下面是变量的示例:

solidity 复制代码
pragma solidity ^0.8.0;

contract VariablesExample {
    // 状态变量
    uint public count;

    // 函数示例,包含局部变量
    function updateCount(uint _newCount) public {
        uint localVar = 42;  // 局部变量
        count = _newCount;
    }
}

在上面的示例中:

  • count 是一个公共的状态变量,用于存储合约中的一个整数值。

  • updateCount 函数包含一个局部变量 localVar,用于存储临时数据,_newCount 参数用于更新 count 的值。

使用建议
  • 常量 vs 变量:根据需求选择合适的常量或变量。常量适用于在合约中需要固定且不可变的值,而变量适用于需要存储和修改的动态数据。

  • Gas 成本:读取常量的 gas 成本较低,因为它们的值在编译时已经确定;而更新变量或者读取状态变量可能会涉及更高的 gas 成本,尤其是在合约存储空间的使用方面。

  • 安全性:在编写合约时,合理使用常量和变量可以提高合约的安全性和可读性,避免不必要的状态变化和复杂的逻辑。

通过合理使用常量和变量,Solidity 开发者可以有效管理和操作合约中的数据,确保合约的功能和逻辑符合预期,并且在 gas 使用和执行效率方面进行优化。

Immutable 修饰符

一次初始化之后不可以修改,定义成 immutable 的变量是可以节省 gas

三.循环分支

【图示 2】原文此处为流程图/白板图示,因平台导出限制未能搬运。

源文档: https://w2z1amg2x5.sg.larksuite.com/docx/OU2OddJLloBYRbxd72glBbn5gFf

在 Solidity 中,与大多数编程语言类似,循环和分支控制语句用于控制程序的流程和逻辑。这些结构使得 Solidity 合约能够根据条件执行特定的代码块或者重复执行代码。以下是 Solidity 中常用的循环和分支控制语句:

分支控制

条件语句 (if, else if, else)

条件语句用于根据表达式的结果选择性地执行代码块。

solidity 复制代码
pragma solidity ^0.8.0;

contract ControlStructures {
    uint public number;

在上面的例子中:

  • 如果 _num 大于 10,则将 number 设置为 _num。

  • 如果 _num 等于 5,则将 number 设置为 5。

  • 否则,将 number 设置为 0。

循环结构

for 循环:

for 循环用于按照指定的次数重复执行代码块。

solidity 复制代码
pragma solidity ^0.8.0;

contract LoopExample {
    uint[] public numbers;

    // for 循环示例
    function addNumbers(uint _count) public {
        for (uint i = 0; i < _count; i++) {
            numbers.push(i);
        }
    }
}

在上面的例子中,addNumbers 函数通过 for 循环将从 0 到 _count-1 的整数依次添加到 numbers 数组中。

while 循环

while 循环用于在条件为真时重复执行代码块。

solidity 复制代码

在上面的例子中,incrementUntilTen 函数会持续增加 number 直到它大于或等于 10。

do-while 循环:

do-while 循环首先执行代码块,然后在条件为真时重复执行代码块。

solidity 复制代码
pragma solidity ^0.8.0;

contract DoWhileLoopExample {
    uint public number;

    // do-while 循环示例
    function incrementUntilTen() public {
        do {
            number++;
        } while (number < 10);
    }
}

在上面的例子中,incrementUntilTen 函数首先会增加 number,然后检查条件,如果条件满足则继续执行,直到 number 大于或等于 10。

注意事项
  • Gas 成本:在 Solidity 合约中,循环和分支控制语句的执行会消耗 gas。特别是在循环中使用动态数据结构时要注意 gas 成本,避免不必要的 gas 消耗。

  • 避免复杂逻辑:尽量避免在合约中使用复杂的逻辑或者深层嵌套的控制结构,以确保合约的可读性和维护性。

  • 安全性:合约中的条件和断言语句对于确保合约的状态和操作是安全的非常重要,确保适当地使用断言和条件检查来验证输入和状态。

合理使用循环和分支控制结构可以帮助开发者实现复杂的逻辑和业务需求,同时也需要注意避免陷入不必要的复杂性和高 gas 消耗。

四.总结

  • 如果 solidity 版本低于 0.8.0,数学运算有溢出的风险

  • Address 类型的 ETH 收发的方式,方式区别和特点

  • 常量和变量的 Gas 问题,Immutable 修饰符节省 Gas

  • 在 solidity 里面,虽然动态数组,mapping,结构体和其他语言类似,使用过程过于复杂,消耗的 Gas 会更多

设计的时候,要尽可能简单,考虑 Gas 的消耗问题。

  • 定长字节数组的长度在声明时确定,并且不能更改。

  • 当操作定长字节数组时,需要确保索引不超出数组的有效范围,以避免访问越界错误。

  • 定长字节数组不支持动态大小调整,因此在使用时需要考虑到存储空间和 gas 成本。

  • 成员变量访问:结构体的成员变量可以通过点号 (.) 访问,例如 myPerson.name。

  • 内存与存储:在函数内部使用结构体时,需要注意是否需要在存储区域(storage)或者内存区域(memory)中处理。

  • 嵌套结构体:Solidity 支持结构体的嵌套定义,允许在一个结构体中包含另一个结构体作为成员变量。

  • Gas 成本:对于复杂的结构体或者包含大量数据的结构体,需要注意 gas 成本和执行效率问题。

参考资料 & 致谢

参考资料

致谢

感谢本文所参考的课程讲义、Solidity 社区资料以及帮助校对的同学和朋友。文中的部分代码示例基于 Foundry 测试框架整理,便于读者边读边跑通测试。

相关推荐
Bczheng115 小时前
五十二.网络(2)--节点连接
区块链
Bczheng12 天前
五十一.网络(1)--加载节点地址和连接
区块链
OxYGC2 天前
[Web3] 基础第7篇: 说透比特币的基本框架与原理
web3·区块链·以太坊·比特币
混凝土拌意大利面2 天前
WEB3核心技术的一种实现:时间长河共识算法(TRC, Time-River Consensus)
web3·区块链·共识算法
OxYGC3 天前
[Web3] 基础第5篇: 共识算法与 P2P 通信协议
web3·区块链·以太坊·比特币
Bczheng18 天前
四十六.创建一笔交易(9)--第三个区块 添加tx到钱包
区块链
2301_776045239 天前
理解Layer 1区块链协议
区块链
幻智星科技9 天前
零知识证明三大核心特性解析
区块链·零知识证明
木西10 天前
AI 驱动的去中心化资产管理:QuantixAI 核心金库与智能风控架构解析
web3·智能合约·solidity