solidity语言语法补充(进阶版)

函数修改器:

在Solidity中,函数修改器(Function Modifiers)是一种用于修改函数行为的特殊类型。它可以在函数执行前、执行后或者在函数执行期间对函数的行为进行修改或增加额外的逻辑。函数修改器通常用于提高代码的重用性、简化代码结构,并确保一致的行为。

1. 定义函数修改器

函数修改器通过 modifier 关键字来定义,其结构类似于函数,但不包含函数体。

modifier modifierName() {
    // 这里可以编写额外的逻辑
    _; // 这个符号表示将控制权转交给函数本体
}

2. 使用函数修改器

在Solidity中,函数修改器可以通过在函数定义时使用 modifier 关键字来应用到函数上。

function functionName() visibilityModifier {
    // 函数逻辑
}

在这里,visibilityModifier 是一个函数修改器,它会在执行 functionName 函数时应用额外的逻辑。

3. 例子

下面是一个简单的示例,演示了如何定义和使用函数修改器:

pragma solidity ^0.8.0;

contract Example {
    address public owner;

    // 定义一个函数修改器,用于验证调用者是否是合约的所有者
    modifier onlyOwner() {
        require(msg.sender == owner, "Only the owner can call this function");
        _; // 控制权转交给函数本体
    }

    // 构造函数,初始化合约所有者
    constructor() {
        owner = msg.sender;
    }

    // 使用函数修改器 onlyOwner
    function changeOwner(address newOwner) public onlyOwner {
        owner = newOwner;
    }
}

在这个示例中,onlyOwner 函数修改器用于限制只有合约的所有者才能调用特定的函数,如 changeOwner 函数。这样可以增加合约的安全性,确保只有合约的所有者才能执行敏感操作。

总之,函数修改器是Solidity中一种强大的工具,可以用于简化代码、提高安全性,并确保合约行为的一致性。

call函数:

基本介绍

call 函数是 Solidity 中用于与其他合约或外部账户进行交互的重要函数之一。它允许智能合约向指定地址发送消息并执行函数调用,同时也可以处理调用结果。以下是 call 函数的语法和详细介绍:

(bool success, bytes memory data) = address.call(bytes memory payload);
  • address:要调用的合约或外部账户地址。
  • payload:要发送的消息数据,通常是函数调用的 ABI 编码。
  • success:布尔值,表示调用是否成功。如果调用成功,则为 true;否则为 false
  • data:调用的返回数据,以 bytes memory 类型返回。如果调用失败或者目标合约没有返回数据,则 data 为空。

需要注意的是,使用 call 函数需要小心处理返回值和异常情况,以避免安全风险。以下是一些关键注意事项:

  1. ABI 编码payload 参数通常是使用 abi.encodeWithSignature 或 abi.encodeWithSelector 等函数生成的 ABI 编码。这样可以确保正确传递函数名和参数,并保持类型安全。
  2. Gas 额度:调用 call 函数会消耗 gas。在调用合约函数时,应该确保提供足够的 gas 额度以完成调用操作。
  3. 返回值处理:调用后需要检查 success 来确定调用是否成功,并根据需要处理返回的 data 数据。
  4. 异常处理:调用可能会抛出异常。如果调用失败或者发生异常,应该采取适当的错误处理措施。

综上所述,call 函数是 Solidity 中实现合约间通信和与外部系统交互的重要手段之一,但在使用时需要谨慎处理,以确保安全性和可靠性。

  • ABI 编码:在 Solidity 中,合约之间进行通信或者与外部系统进行交互时,需要使用 ABI 编码来确保数据的正确传递和类型安全。ABI 是一种二进制接口规范,定义了合约函数调用的参数传递方式、返回值格式等信息。

  •  payload 参数 :在使用 call 函数或者发送交易时,需要构建正确的数据结构作为参数传递给目标合约或外部账户。这个参数通常称为 payload,它包含了要发送的消息数据,包括函数调用的 ABI 编码、参数值等信息。

  • abi.encodeWithSignatureabi.encodeWithSelector:这两个函数是 Solidity 提供的用于生成 ABI 编码的工具函数。

    • abi.encodeWithSignature 用于生成带有函数签名的 ABI 编码,确保正确传递函数名和参数值。例如:abi.encodeWithSignature("transfer(address,uint256)", recipient, amount)
    • abi.encodeWithSelector 用于生成带有函数选择器的 ABI 编码,选择器是函数名和参数类型的哈希值,用于唯一标识函数。例如:abi.encodeWithSelector(bytes4(keccak256("transfer(address,uint256)")), recipient, amount)

下面是使用call函数与另一个函数交互的示例:

pragma solidity ^0.8.0;

contract CallerContract {
    event CallResult(bool success, bytes data);

    function callAnotherContract(address _contractAddress, uint256 _value) external {
        // 构建要发送的消息数据,包括函数选择器和参数
        bytes memory payload = abi.encodeWithSignature("someFunction(uint256)", _value);
        
        // 使用 call 函数调用目标合约的函数
        (bool success, bytes memory data) = _contractAddress.call(payload);
        
        // 触发事件,记录调用结果
        emit CallResult(success, data);
    }
}

contract AnotherContract {
    uint256 public someValue;

    function someFunction(uint256 _newValue) external {
        someValue = _newValue;
    }
}

gas修改器:

在 Solidity 中,call 函数有一个可选的 gas 修改器,用于指定调用目标合约函数时所能消耗的 gas 数量。gas 是以太坊网络中的计价单位,用于支付合约执行所需的计算资源。

call 函数的 gas 修改器有两种形式:

  1. 在调用中直接指定 gas 数量。
  2. 在 Solidity 0.8.0 及更高版本中,通过 gas() 函数动态指定 gas 数量。

下面分别介绍这两种形式:

1. 直接指定 gas 数量

(bool success, bytes memory data) = _contractAddress.call{gas: 20000}(payload);

在这个示例中,call 函数调用了 _contractAddress 地址对应合约的函数,并指定了 20000 个 gas 用于执行。这种方式适用于在编译时已知调用所需 gas 量的情况。

2. 使用 gas() 函数动态指定 gas 数量

在 Solidity 0.8.0 及更高版本中,可以使用 gas() 函数动态指定 gas 数量。例如:

(uint256 myGas) = gasleft();
(bool success, bytes memory data) = _contractAddress.call{gas: myGas}(payload);

在这个示例中,使用了 gasleft() 函数获取当前剩余的 gas 数量,并将其动态指定为 call 函数的 gas 参数。这种方式适用于需要在运行时确定 gas 数量的情况,比如在迭代过程中调用其他合约函数。

需要注意的是,在使用 call 函数时,需要确保提供的 gas 数量足够覆盖合约函数的执行所需。如果 gas 不足以完成合约函数执行,调用会失败并返回 false,同时不会影响调用者合约的状态。因此,在确定 gas 数量时,需要进行充分的测试和评估,以确保合约调用的成功执行。

ps:call还支持value功能(可以理解为以太币修改器)

数组

在 Solidity 中,有两种类型的字节数组:定长字节数组和变长字节数组。

定长字节数组(Fixed-size byte array)

定长字节数组有一个固定的长度,在声明时就已经确定了大小,不能更改。它们存储在内存中,具有固定的大小,因此更加高效。定长字节数组的声明方式如下:

bytes32 public fixedArray; // 声明一个长度为32字节的定长字节数组

变长字节数组(Dynamic byte array)   

变长字节数组的长度可以动态增长或缩小。它们的长度是在运行时动态确定的,因此更加灵活。变长字节数组存储在存储器(storage)中或是作为函数的参数传递。声明方式如下:

bytes public dynamicArray; // 声明一个变长字节数组

需要注意的是,变长字节数组在存储器(storage)中存储时会消耗更多的 gas,因为它们的长度是动态的,可能会导致存储器的碎片化。因此,如果可能的话,应尽量使用定长字节数组,以提高效率和节省 gas 费用。

string数组:

可以使用 string.concat 连接任意数量的 string 字符串。 该函数返回一个 string memory ,包含所有参数的内容,无填充方式拼接在一起。 如果你想使用不能隐式转换为 string 的其他类型作为

参数,你需要先把它们转换为 string

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

contract C {
    string s = "Storage";
    function f(bytes calldata bc, string memory sm, bytes16 b) public view {
        string memory concatString = string.concat(s, string(bc), "Literal", sm);//这里用到了所说的连接
//assert 语句后面的括号中包含一个条件表达式。在这个条件表达式中,合约在断言的位置检查是否满足某种条件。如果条件为真,则合约继续执行,如果条件为假,则合约会失败并触发回滚
        assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);
//下面是byte数组的连接
        bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
        assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);
    }
}

assert 是一个断言语句,用于在合约执行期间检查条件是否满足。如果条件不满足,assert 将导致合约执行失败,并且会回滚所有的状态变化,包括 gas 的消耗。这有助于确保合约在执行期间的正确性。

assert 语句后面的括号中包含一个条件表达式。在这个条件表达式中,合约在断言的位置检查是否满足某种条件。如果条件为真,则合约继续执行,如果条件为假,则合约会失败并触发回滚。

在合约中使用 assert 可以用来进行一些必要的检查,例如检查函数执行过程中的一些关键条件,确保合约的安全性和正确性。

在给定的代码中,assert 语句用于检查拼接后的字符串的长度是否符合预期。如果长度不符合预期,将触发断言失败,并导致合约执行失败。这样可以确保合约在运行过程中正确地处理了字符串拼接的逻辑。

数组成员

length:

数组有 length 成员变量表示当前数组的长度。 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它可以根据运行时的参数创建)。

// 创建一个动态大小的存储数组,并获取其长度
uint[] public dynamicArray;

function getArrayLength() public view returns (uint) {
    return dynamicArray.length;
}
push():

动态的 存储storage 数组以及 bytes 类型( string 类型不可以)都有一个 push() 的成员函数,它用来添加新的零初始化元素到数组末尾,并返回元素引用. 因此可以这样:  x.push().t = 2 或 x.push() = b.

// 在动态存储数组末尾添加新的零初始化元素,并返回元素引用
struct Element {
    uint value;
}

Element[] public dynamicArray;

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

function setElementValue(uint newValue) public {
    dynamicArray.push().value = newValue;
}
push(x):

动态的 存储storage 数组以及 bytes 类型( string 类型不可以)都有一个 push(x) 的成员函数,用来在数组末尾添加一个给定的元素,这个函数没有返回值.

// 在动态存储数组末尾添加给定的元素
uint[] public dynamicArray;

function addElement(uint newValue) public {
    dynamicArray.push(newValue);
}
pop():

变长的 存储storage 数组以及 bytes 类型( string 类型不可以)都有一个 pop() 的成员函数, 它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用 delete ,这个函数没有返回值。

// 从动态存储数组末尾删除元素
uint[] public dynamicArray;

function removeElement() public {
    dynamicArray.pop();
}

ps:通过 push() 增加 存储storage 数组的长度具有固定的 gas 消耗,因为 存储storage 总是被零初始化,而通过 pop() 减少长度则依赖移除与元素的大小(size). 如果元素是数组,则成本是很高的,因为它包括已删除的元素的清理,类似于在这些元素上调用 delete 。

数组切片:

数组切片是数组连续部分的视图,用法如:x[start:end] ,start 和 end 是 uint256 类型(或结果为 uint256 的表达式),x[start:end] 的第一个元素是 x[start] ,最后一个元素是 x[end-1] 。

如果 start 比 end 大或者 end 比数组长度还大,将会抛出异常。

start 和 end 都可以是可选的: start 默认是 0, 而 end 默认是数组长度。

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

contract ArraySlicing {
    uint[] public originalArray;

    constructor() {
        originalArray = [1, 2, 3, 4, 5];
    }

    function getSlice(uint start, uint end) public view returns (uint[] memory) {
        return originalArray[start:end];
    }
}

用户定义的值类型:

一个用户定义的值类型允许在一个基本的值类型上创建一个零成本的抽象。 这类似于一个别名,但有更严格的类型要求。

用户定义值类型使用 type C is V 来定义,其中 C 是新引入的类型的名称, V 必须是内置的值类型(”底层类型”)。 函数 C.wrap 被用来从底层类型转换到自定义类型。同样地,函数函数 C.unwrap 用于从自定义类型转换到底层类型。

// 定义自定义类型
type CustomString is string;

contract Example {
    // wrap 函数将底层类型转换为自定义类型
    function wrapString(string memory value) public pure returns (CustomString memory) {
        return CustomString(value);
    }
    
    // unwrap 函数将自定义类型转换为底层类型
    function unwrapString(CustomString memory custom) public pure returns (string memory) {
        return string(custom);
    }
}

在这个示例中,CustomString 是我们定义的自定义类型,它是 string 类型的一个包装器。wrapString 函数接受一个 string 类型的参数,并将其转换为 CustomString 类型;unwrapString 函数接受一个 CustomString 类型的参数,并将其转换为 string 类型。

memory和storage:

在 Solidity 中,memory 是一种关键字,用于声明临时性的数据存储位置。在 Solidity 中,数据可以存储在不同的位置,包括 storage(持久性存储,如合约状态变量)和 memory(临时性存储,如临时变量和函数参数)。

具体来说,memory 关键字用于指示数据应该存储在临时的内存中,而不是永久性存储在区块链的状态中。在 Solidity 中,函数参数和函数内部声明的临时变量默认会被分配在 memory 中。

使用 memory 的一些常见情况包括:

  1. 函数参数: Solidity 函数的参数默认会被分配到 memory 中。例如:

function foo(uint[] memory _data) public {
    // 函数参数 _data 存储在 memory 中
    // 可以在函数内部使用 _data 数组,但它不会永久存储在区块链上
}

   2.临时变量: 在函数内部声明的临时变量通常会被分配到 memory 中。例如:

function bar() public {
    uint[] memory tempArray = new uint[](10);
    // tempArray 是临时变量,存储在 memory 中
    // 只在函数执行期间存在,并在函数执行完毕后被清除
}

需要注意的是,memory 中存储的数据只在当前函数执行期间存在,函数执行结束后数据将被清除。相比之下,storage 中存储的数据是永久性的,会持久存储在区块链上,例如合约的状态变量。

使用 memory 的好处是它可以提高临时数据的访问效率,并避免占用永久性的存储空间。但需要注意的是,memory 中的数据需要手动分配和管理,并且有一定的 gas 成本用于在区块链上执行这些操作。

结构体:

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

contract MyContract {
    // 定义结构体
    struct Person {
        string name;
        uint age;
        address walletAddress;
    }

    // 声明结构体类型的变量
    Person public myPerson;

    // 修改结构体成员的函数
    function setPerson(string memory _name, uint _age, address _walletAddress) public {
        myPerson.name = _name;
        myPerson.age = _age;
        myPerson.walletAddress = _walletAddress;
    }

    // 获取结构体成员的函数
    function getPerson() public view returns (string memory, uint, address) {
        return (myPerson.name, myPerson.age, myPerson.walletAddress);
    }
}
/*我们定义了一个名为 Person 的结构体,它包含了三个成员变量:name(字符串类型)、age(无符号整数类型)、walletAddress(地址类型)。然后,我们声明了一个类型为 Person 的公共状态变量 myPerson。

setPerson 函数用于修改 myPerson 结构体的成员变量的值,接受三个参数:姓名、年龄和钱包地址。getPerson 函数则用于获取 myPerson 结构体的成员变量的值,并作为元组返回。

结构体在 Solidity 中通常用于组织和管理复杂的数据结构,例如用户信息、交易记录等。它们提供了一种方便的方式来组织和操作相关的数据项。*/

映射:

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

contract MyContract {
    // 定义一个映射,将地址映射到整数
    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);
    }

    // 获取指定地址的余额
    function getBalance(address account) public view returns (uint) {
        return balances[account];
    }
}

结构体和映射的综合使用:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// 定义的新类型包含两个属性。
// 在合约外部声明结构体可以使其被多个合约共享。 在这里,这并不是真正需要的。
struct Funder {
    address addr;
    uint amount;
}

contract CrowdFunding {

    // 也可以在合约内部定义结构体,这使得它们仅在此合约和衍生合约中可见。
    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }

    uint numCampaigns;
    mapping (uint => Campaign) campaigns;

    function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID 作为一个变量返回

        // 不能使用 "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
        // 因为RHS(right hand side)会创建一个包含映射的内存结构体 "Campaign"
        Campaign storage c = campaigns[campaignID];
        c.beneficiary = beneficiary;
        c.fundingGoal = goal;
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 以给定的值初始化,创建一个新的临时 memory 结构体,
        // 并将其拷贝到 storage 中。
        // 注意你也可以使用 Funder(msg.sender, msg.value) 来初始化。
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

上面的合约只是一个简化版的 众筹合约,但它已经足以让我们理解结构体的基础概念。 结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。

尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身。 这个限制是有必要的,因为结构体的大小必须是有限的。

注意在函数中使用结构体时,一个结构体是如何赋值给一个存储位置是 存储storage 的局部变量。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。

可迭代映射:

在 Solidity 中,映射本身是一个无法迭代的数据结构,这意味着你不能直接在合约中对映射进行迭代操作(如循环遍历)。然而,有时候我们可能需要对映射中的所有键或所有值进行迭代操作,这时我们可以通过一些技巧实现类似的功能,这就是可迭代映射。

一种常见的方法是使用一个存储键的数组,然后通过遍历该数组来访问映射中的键或值

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

contract MyContract {
    mapping(address => uint) public balances;
    address[] public accounts;

    // 存款函数,增加指定地址的余额
    function deposit() public payable {
        if (balances[msg.sender] == 0) {
            accounts.push(msg.sender); // 将新地址添加到数组中
        }
        balances[msg.sender] += msg.value;
    }

    // 获取所有账户地址
    function getAllAccounts() public view returns (address[] memory) {
        return accounts;
    }

    // 获取所有账户的余额
    function getAllBalances() public view returns (uint[] memory) {
        uint[] memory result = new uint[](accounts.length);
        for (uint i = 0; i < accounts.length; i++) {
            result[i] = balances[accounts[i]];
        }
        return result;
    }
}

在这个示例中,我们通过一个动态数组 accounts 来存储所有的地址,当有新地址存款时,我们将其添加到数组中。然后,我们可以通过 getAllAccounts 函数获取所有的账户地址,通过 getAllBalances 函数获取所有账户的余额。这种方法虽然不直接对映射进行迭代,但实现了类似的功能。

需要注意的是,当映射中的键值对数量较大时,遍历整个数组可能会导致 gas 消耗过高,因此在设计合约时需要注意 gas 成本和效率。

  • 37
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值