学习solidity(基本特性)
本文主要是自身学习总结的solidity的一些经验,可以帮助新手小白或已有其他语言基础的开发者快速入门solidity的特性
数组的一些注意事项
返回非固定长度数组使用的修饰符
csharp
function getArr() public view returns (uint256[] memory) {
return arr;
}
- 如果我们在返回时线程的数组我们需要手写一个数组,如果对长度有要求还要设置对应的长度
arr
是一个状态变量,存储在storage
中。- 当函数返回
arr
时,Solidity 需要将arr
的数据复制到一个新的位置,以便将其返回给调用者。 memory
关键字指定了这个新的位置是内存。这意味着函数会在内存中创建一个arr
的副本,并将这个副本返回。
如果不使用 memory
会发生什么?
如果不使用 memory
,编译器会报错,因为它不知道将数组存储在哪里。你需要明确指定返回的数组应该存储在 memory
中。
为什么 memory
是必要的:
- 避免修改原始数据: 通过在
memory
中创建副本,函数可以返回数组的内容,而不会修改存储在storage
中的原始数组。这对于保持合约状态的完整性非常重要。 - 临时存储:
memory
中的数据是临时的,只在函数调用期间存在。这避免了在合约的storage
中创建不必要的永久存储,从而节省了 gas 费用。 - 符合 EVM 规范: 以太坊虚拟机(EVM)要求函数返回的数据必须存储在
memory
中。
总结:
当函数返回动态数组时,必须使用 memory
关键字来指定返回的数组存储在内存中。这确保了函数返回的是原始数组的副本,而不是直接返回对 storage
中数据的引用,从而避免了意外修改合约状态的可能性,并符合EVM规范。
补充说明: 固定大小的数组直接使用unit返回
对于固定大小的数组,例如 uint256[10]
,在函数返回时通常不需要 memory
关键字,因为它们的大小是固定的,可以直接在栈上处理。但是,对于动态数组(例如 uint256[]
),memory
是必不可少的。
数组的pop和push方法
1. push()
方法:
push()
方法用于在数组的末尾添加一个新元素。它有两种用法:
push()
: 如果不带参数调用push()
,则会在数组末尾添加一个零值元素(对于uint
数组,就是 0;对于address
数组,就是address(0)
,以此类推)。此方法返回数组的新长度。push(x)
: 如果带一个参数x
调用push(x)
,则会在数组末尾添加元素x
。此方法返回数组的新长度。
2. pop()
方法:
pop()
方法用于移除数组的最后一个元素。它会修改原数组,并返回被移除的元素。
重要注意事项:
- 只能用于
storage
数组:push()
和pop()
方法只能用于存储在storage
中的动态数组。你不能直接对memory
中的数组使用这些方法。如果你需要在函数内部操作数组,可以使用memory
数组,但你需要预先分配好数组的大小,并且不能使用push()
和pop()
动态调整大小。 - Gas 消耗:
push()
和pop()
操作会消耗 gas。尤其是在storage
中操作数组时,gas 消耗会比较高。因此,在设计智能合约时,需要仔细考虑数组的使用,以优化 gas 消耗。 - 空数组的
pop()
: 尝试对空数组执行pop()
操作会导致 revert,因此在执行pop()
之前,务必检查数组的长度是否大于 0。
总结:
push()
和 pop()
是操作动态数组的有用工具。push()
用于在数组末尾添加元素,pop()
用于移除数组末尾的元素。请记住,这些方法只能用于 storage
数组,并且需要注意 gas 消耗和空数组的情况。
如何在 Solidity 中方法于内存(memory
)中创建一个固定大小的数组
scss
function examples() external pure {
// create array in memory, only fixed size can be created
uint256[] memory a = new uint256[](5);
}
-
function examples() external pure {
:function examples()
: 这是函数声明,函数名为examples
。external
: 这个关键字表示该函数只能从合约外部调用,不能从合约内部调用。pure
: 这个关键字表示该函数既不读取合约的状态变量,也不修改合约的状态变量。它只使用传入的参数进行计算,并返回结果。这意味着该函数执行不会消耗 gas 来读取或写入存储。
-
uint256[] memory a = new uint256[](5);
:uint256[] memory a
: 这部分声明了一个名为a
的变量,其类型是uint256
类型的动态数组,并且存储位置是memory
。memory
关键字至关重要,因为它指定了数组存储在内存中,而不是持久存储在合约的存储(storage
)中。= new uint256[](5);
: 这部分是数组的初始化。new uint256[](5)
创建了一个新的uint256
类型的数组,其大小(长度)为 5。这意味着该数组可以存储 5 个uint256
类型的元素。new
关键字用于在内存中分配空间。
关键点和解释:
- 内存数组的特点: 内存数组是临时的,它们只在函数执行期间存在。当函数执行完毕后,内存中的数据会被释放。
- 只能创建固定大小的内存数组: 在 Solidity 中,你只能 使用
new
关键字创建固定大小的内存数组。你不能使用new
创建动态大小的内存数组,然后通过push()
或其他方法动态调整其大小。如果需要可变大小的数组,你需要使用storage
数组。 pure
函数和内存数组: 由于pure
函数不能访问或修改状态变量,因此在pure
函数中使用memory
数组是很常见的。因为函数本身不与链上存储交互,所以所有操作都在本地的内存中完成。- 与
storage
数组的区别:storage
数组是持久的,它们存储在合约的状态变量中,并在合约的多次调用之间保持不变。memory
数组是临时的,它们只在函数执行期间存在。 - gas 消耗: 在内存中创建数组的 gas 消耗相对较低,因为不需要写入链上存储。
示例说明:
假设你想在函数内部进行一些数组操作,而不想修改合约的永久状态。你可以使用以下模式:
solidity
pragma solidity ^0.8.0;
contract ArrayExample {
function processData(uint256[] memory input) external pure returns (uint256) {
uint256[] memory processed = new uint256[](input.length);
for (uint256 i = 0; i < input.length; i++) {
processed[i] = input[i] * 2; // 对输入数据进行处理
}
uint256 sum = 0;
for (uint256 i = 0; i < processed.length; i++) {
sum += processed[i];
}
return sum;
}
}
在这个例子中,processData
函数接收一个输入数组 input
,在内存中创建一个新的数组 processed
,对输入数据进行处理,然后返回处理后的结果之和。所有这些操作都在内存中完成,不会影响合约的存储状态。
总而言之,uint256[] memory a = new uint256[](5);
这行代码在内存中创建了一个可以容纳 5 个 uint256
元素的固定大小数组,并且该数组只在函数执行期间存在。这在需要临时存储和处理数据的场景中非常有用,尤其是在 pure
或 view
函数中。
new关键字的用法
我们前面已经使用new关键字在合约中定义对应长度的数组
那我们还有哪些常用的方法呢?
new
关键字在 Solidity 中主要用于创建新的合约实例。这意味着它会在区块链上部署一个新的合约,并返回新合约的地址。除了你已经了解的在内存中创建数组之外,new
关键字在合约中最重要的用法就是创建合约实例。
以下是 new
关键字在合约中的用法和作用的详细解释:
1. 创建合约实例:
这是 new
关键字最常见的用法。它允许一个合约(称为"创建者合约")创建另一个合约(称为"被创建合约")的新实例。
语法:
solidity
address newContractAddress = new ContractName{value: amount}(constructorArguments);
ContractName
:被创建合约的名称。{value: amount}
:可选部分,用于在创建合约时发送以太币。amount
是要发送的以太币数量(以 wei 为单位)。(constructorArguments)
:可选部分,用于传递给被创建合约构造函数的参数。
示例:
solidity
pragma solidity ^0.8.0;
contract ContractA {
uint256 public value;
constructor(uint256 _value) {
value = _value;
}
}
contract ContractB {
ContractA public contractA;
function createContractA(uint256 _value) public {
contractA = new ContractA{value: 1 ether}(_value); // 创建 ContractA 的新实例,并发送 1 ether
}
function getContractAValue() public view returns (uint256) {
return contractA.value();
}
}
在这个例子中:
ContractB
的createContractA
函数使用new ContractA{value: 1 ether}(_value)
创建了ContractA
的一个新实例。{value: 1 ether}
表示在创建ContractA
的实例时发送 1 个以太币。(_value)
将_value
参数传递给ContractA
的构造函数。- 创建的新实例的地址被存储在
ContractB
的contractA
状态变量中。
2. 重要注意事项:
- 构造函数: 被创建合约必须有一个构造函数(
constructor
),用于初始化合约的状态变量。传递给new
关键字的参数会传递给被创建合约的构造函数。 - 以太币发送: 可以使用
{value: amount}
在创建合约时发送以太币。这对于需要初始资金才能运行的合约非常有用。 - gas 消耗: 使用
new
关键字创建合约会消耗大量的 gas,因为需要在区块链上部署新的合约代码。 - 错误处理: 如果合约创建失败(例如,由于 gas 不足、堆栈溢出或构造函数抛出异常),整个交易会回滚。
- 合约代码必须已知: 使用
new
创建合约时,被创建合约的完整代码在创建合约的合约编译时必须已知,因此不可能存在递归创建依赖。
3. 与直接赋值的区别:
不要将 new
关键字与直接赋值混淆。例如:
ini
ContractA public contractA = ContractA(someAddress); // 错误!这是类型转换,而不是创建新实例
这行代码不是 创建 ContractA
的新实例。它只是将 contractA
变量指向 someAddress
处的现有合约。如果 someAddress
处没有部署 ContractA
的合约,或者部署的合约代码不兼容,则会导致错误。
总结:
new
关键字是 Solidity 中用于创建新合约实例的关键工具。它允许合约动态地创建其他合约,并与其进行交互。使用 new
关键字时,请务必注意构造函数、以太币发送、gas 消耗和错误处理。
除了创建合约实例,在较新的Solidity版本中,new
关键字也用于在memory
中创建数组,但这个数组的大小必须在创建时就指定,不能动态调整大小(不能使用push或pop)。这是它和storage数组的重要区别。
合约地址的理解
以下是对合约中地址的详细解释,包括合约自身地址、所有者地址和其他地址:
1. 地址类型 (address
):
address
类型可以存储任何以太坊账户或合约的地址。它可以用于:
- 发送以太币。
- 调用其他合约的函数。
- 检查地址的余额。
从 Solidity 0.5.0 版本开始,address
分为两种类型:
address
:普通地址,不能直接发送以太币。address payable
:可支付地址,可以接收和发送以太币。
你需要使用类型转换将 address
转换为 address payable
才能发送以太币,例如:payable(someAddress).transfer(amount)
。
2. 合约自身的地址 (address(this)
):
address(this)
返回当前合约的地址。this
是一个特殊的关键字,指代当前合约实例。
示例:
solidity
pragma solidity ^0.8.0;
contract MyContract {
function getContractAddress() public view returns (address) {
return address(this);
}
function sendEtherToSelf() public payable {
// 将合约自身余额发送到另一个地址
payable(0x...).transfer(address(this).balance);
}
}
在这个例子中,getContractAddress()
函数返回 MyContract
合约的地址。
3. 所有者的地址:
合约通常有一个所有者,拥有特殊的权限,例如升级合约、提取资金等。所有者的地址通常存储在合约的一个状态变量中。
示例:
solidity
pragma solidity ^0.8.0;
contract OwnedContract {
address public owner;
constructor() {
owner = msg.sender; // 将部署合约的人设置为所有者
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function.");
_;
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
}
在这个例子中:
owner
是一个状态变量,存储合约所有者的地址。- 构造函数将部署合约的人(
msg.sender
)设置为所有者。 onlyOwner
修饰器用于限制只有所有者才能调用withdraw()
函数。
4. 其他地址:
合约可以与其他合约或外部账户进行交互。这些交互需要使用其他合约或账户的地址。
示例:
solidity
pragma solidity ^0.8.0;
interface OtherContract {
function getValue() external view returns (uint256);
}
contract MyContract {
OtherContract public otherContract;
constructor(address _otherContractAddress) {
otherContract = OtherContract(_otherContractAddress);
}
function getOtherContractValue() public view returns (uint256) {
return otherContract.getValue();
}
function sendEtherToOther(address payable _to, uint256 _amount) public {
_to.transfer(_amount);
}
}
在这个例子中:
MyContract
与OtherContract
进行交互。OtherContract
的地址在构造函数中传递给MyContract
。sendEtherToOther
函数向指定的地址发送以太币。
5. msg.sender
:
msg.sender
是一个全局变量 ,表示当前消息的发送者。在合约的上下文中,msg.sender
通常是调用合约函数的外部账户或另一个合约的地址。
总结:
address
类型用于表示以太坊地址。address(this)
返回当前合约的地址。- 所有者地址用于控制合约的权限。
- 合约可以与其他合约或外部账户通过地址进行交互。
msg.sender
表示当前消息的发送者。
理解地址的概念对于编写安全的智能合约至关重要。你需要正确处理地址,以避免潜在的安全漏洞,例如未经授权的访问或资金损失。
payable修饰符的使用
payable
关键字在 Solidity 中用于指定函数或地址可以接收以太币(Ether)。这是处理以太币转移的关键概念,理解 payable
的使用对于编写涉及价值转移的智能合约至关重要。
1. payable
修饰符用于函数:
当一个函数被声明为 payable
时,它表示该函数可以接收随交易发送的以太币。如果没有 payable
修饰符,函数就不能接收以太币,任何试图向该函数发送以太币的交易都会被拒绝。
示例:
solidity
pragma solidity ^0.8.0;
contract PayableExample {
address payable public owner; // 可支付的地址
constructor() {
owner = payable(msg.sender); // 将部署者设置为所有者,并转换为可支付地址
}
// 可接收以太币的函数
function deposit() public payable {
// msg.value 包含随交易发送的以太币数量(以 wei 为单位)
// 这里可以将收到的以太币记录下来,例如增加一个余额变量
}
function getBalance() public view returns (uint256) {
return address(this).balance; // 获取合约的以太币余额
}
function withdraw(uint256 _amount) public {
require(msg.sender == owner, "Only owner can withdraw.");
require(address(this).balance >= _amount, "Insufficient balance.");
payable(owner).transfer(_amount); // 将以太币转移给所有者
}
}
在这个例子中:
deposit()
函数使用payable
修饰符声明,因此可以接收以太币。msg.value
是一个全局变量,表示随交易发送的以太币数量(以 wei 为单位)。在deposit()
函数中,你可以使用msg.value
来处理接收到的以太币,例如更新合约的余额。withdraw
函数演示了如何将合约中的以太币转出。注意payable(owner).transfer(_amount)
的用法,只有payable类型的地址才能调用transfer方法。address(this).balance
用于获取合约当前的以太币余额。
2. payable
类型用于地址:
在 Solidity 0.5.0 之后,地址类型被分为 address
和 address payable
两种。
address
:普通地址,不能直接接收或发送以太币。address payable
:可支付地址,可以接收和发送以太币。
你需要使用类型转换将 address
转换为 address payable
才能发送以太币。
我们也可以在声明address
变量时直接加上对应的payable
修饰符
示例:
solidity
pragma solidity ^0.8.0;
contract PayableExample {
address public someAddress;
address payable public payableAddress;
function setAddress(address _addr) public {
someAddress = _addr;
payableAddress = payable(_addr); // 将 address 转换为 address payable
}
function sendEther() public payable{
payableAddress.transfer(1 ether);
}
}
在这个例子中,payable(_addr)
将 address
类型的 _addr
转换为 address payable
类型,然后赋值给 payableAddress
。只有payableAddress
才能调用transfer
方法发送以太币。
3. 使用 transfer()
、send()
和 call()
发送以太币:
只有 address payable
类型的地址才能使用以下方法发送以太币:
transfer(uint256 amount)
: 将指定数量的以太币发送到地址。如果发送失败(例如,由于 gas 不足或接收地址不存在),则会抛出异常(revert)。这是推荐的发送以太币的方式,因为它会进行 gas 安全检查。send(uint256 amount)
: 将指定数量的以太币发送到地址。如果发送失败,则返回false
,而不是抛出异常。你需要手动检查返回值以确保发送成功。不推荐使用,因为它不会进行gas安全检查,容易导致重入攻击。call{value: uint256}(bytes memory)
: 执行低级调用,可以用于发送以太币并执行其他操作。这提供了最大的灵活性,但也需要更谨慎地处理 gas 和错误。
示例:
solidity
pragma solidity ^0.8.0;
contract PayableExample {
address payable public recipient;
function sendEtherWithTransfer() public payable {
recipient.transfer(msg.value); // 使用 transfer 发送以太币
}
function sendEtherWithSend() public payable returns (bool) {
bool success = recipient.send(msg.value); // 使用 send 发送以太币
require(success, "Send failed."); // 手动检查发送是否成功
return success;
}
function sendEtherWithCall() public payable returns (bool) {
(bool success, ) = recipient.call{value: msg.value}(""); // 使用 call 发送以太币
return success;
}
}
总结:
payable
关键字用于指定函数或地址可以接收以太币。msg.value
用于获取随交易发送的以太币数量。- 你需要将
address
转换为address payable
才能发送以太币。 - 推荐使用
transfer()
方法发送以太币,因为它会进行 gas 安全检查。
理解 payable
的使用对于编写安全的智能合约至关重要。你需要正确处理以太币的转移,以避免潜在的安全漏洞,例如重入攻击或资金损失。
自定义结构体类型
声明结构体数组:
接下来,你需要在合约中声明一个结构体数组,用于存储 Person
类型的多个实例。
示例:
solidity
pragma solidity ^0.8.0;
contract StructArrayExample {
struct Person {
string name;
uint256 age;
address payable wallet;
}
Person[] public people; // 声明一个公共的 Person 类型的动态数组
// ...
}
Person[] public people;
声明了一个名为 people
的公共动态数组。public
关键字会自动生成一个 getter 函数,允许外部调用者访问数组中的元素。
结构体数组的应用场景:
- 存储用户信息: 如上面的例子所示,可以使用结构体数组来存储用户的信息,例如姓名、年龄、地址等。
- 管理产品信息: 可以使用结构体数组来存储产品的各种属性,例如名称、价格、描述等。
- 记录事件日志: 可以使用结构体数组来记录合约中发生的各种事件,例如交易记录、状态变更等。
- 实现投票系统: 可以使用结构体数组来存储候选人信息和投票结果。
7. 存储类型:
结构体数组可以存储在 storage
(持久化存储在区块链上)或 memory
(临时存储在内存中)。
storage
: 适合存储需要永久保存的数据。例如Person[] public people;
。memory
: 适合在函数内部临时使用。例如string[] memory names = new string[](people.length);
。
总结:
结构体数组是 Solidity 中一种强大的数据组织方式,可以有效地管理复杂的数据集合。通过结合结构体和数组,你可以创建更灵活、更强大的智能合约。在使用结构体数组时,请注意边界检查、存储位置(storage
或 memory
)以及 gas 消耗等因素。
修饰符的使用
date location
1. Storage(存储)
-
用途: 用于持久化存储合约的状态变量。存储在
storage
中的数据会永久保存在区块链上,合约的每次状态变更都会产生交易并消耗 gas。 -
特点:
- 持久性:数据在合约的整个生命周期内都存在。
- 成本高:读写
storage
的 gas 成本最高。 - 合约状态:用于存储合约的核心数据,例如账户余额、用户数据、合约参数等。
-
使用场景:
- 需要长期保存的数据。
- 合约的核心状态变量。
- 需要在不同函数调用之间共享的数据。
-
声明方式: 在合约内部直接声明的变量,默认存储在
storage
中。例如:uint256 public myNumber;
2. Memory(内存)
-
用途: 用于存储函数执行期间的临时变量。函数执行完毕后,
memory
中分配的内存会被自动释放。 -
特点:
- 临时性:数据只在函数执行期间存在。
- 成本较低:读写
memory
的 gas 成本比storage
低。 - 局部变量:用于存储函数内部的临时数据,例如循环计数器、中间计算结果等。
-
使用场景:
- 函数内部的局部变量。
- 函数参数(引用类型,如
string
、数组、结构体)。 - 在函数内部创建的临时数据结构。
-
声明方式: 在函数内部声明变量时,使用
memory
关键字显式声明。例如:string memory myString;
3. Calldata(调用数据) (只能读取不能修改)
-
用途: 用于存储函数调用的输入数据。它是一个只读的数据区域,用于接收外部调用传递的参数。
-
特点:
- 只读性:只能读取
calldata
中的数据,不能修改。 - 成本最低:读取
calldata
的 gas 成本最低。 - 外部输入:用于存储函数调用时传递的参数。
- 只读性:只能读取
-
使用场景:
- 函数参数(引用类型,如
string
、数组、结构体),特别是外部函数和接口函数的参数。 - 避免不必要的复制,提高 gas 效率。
- 函数参数(引用类型,如
-
声明方式: 在函数参数列表中,使用
calldata
关键字显式声明。例如:function myFunction(string calldata myString) public;
特性 | Storage | Memory | Calldata |
---|---|---|---|
持久性 | 持久 | 临时 | 只读(临时) |
成本 | 高 | 较低 | 最低 |
用途 | 存储合约状态 | 存储临时变量 | 存储函数输入 |
生命周期 | 合约生命周期 | 函数执行期间 | 函数调用期间 |
可修改性 | 可修改 | 可修改 | 不可修改 |
默认位置 | 合约状态变量 | 函数内部局部变量 | 函数参数(外部) |
何时使用 storage
、memory
和 calldata
?
- 需要永久保存的数据: 使用
storage
。例如,存储用户的账户余额、代币数量、合约拥有的数据等。 - 函数内部的临时变量: 使用
memory
。例如,在循环中使用的计数器、函数内部的中间计算结果、需要修改的函数参数副本等。 - 函数参数(特别是外部调用): 优先使用
calldata
。这可以节省 gas 成本,因为calldata
是只读的,不需要复制到memory
中。
重要注意事项:
-
尽量减少Storage的读写次数以节省Gas。
-
在复杂数据操作中,优先考虑Memory。
-
静态数据类型如固定大小的数组或基本类型不需要指定数据位置。
-
在函数内部,如果需要修改引用类型的参数,应该先将其复制到
memory
中,然后再进行修改。例如:solidityfunction myFunction(string calldata _myString) public returns (string memory) { string memory myStringCopy = _myString; // 将 calldata 复制到 memory // 现在可以修改 myStringCopy myStringCopy = string(abi.encodePacked(myStringCopy, " added")); return myStringCopy; }
-
对于数组和结构体等复杂类型,在
storage
和memory
之间复制数据会消耗大量的 gas。因此,应尽量减少不必要的复制。 -
Solidity 0.8.0 版本之后,函数参数的默认数据位置不再是
storage
,而是根据参数类型推断。对于引用类型(如string
、数组、结构体),如果函数是external
的,则默认为calldata
;否则默认为memory
。
通过理解 storage
、memory
和 calldata
之间的区别,你可以编写更高效、更安全的 Solidity 智能合约。合理地选择数据存储位置,可以有效地降低 gas 消耗,并提高合约的性能。
可见修饰符(关于外部调用合约之后在接口提到)
这些修饰符决定了函数可以从哪里被调用。
-
public
:- 任何地方都可以调用该函数,包括合约内部、外部账户以及其他合约。
- 对于状态变量,声明为
public
会自动生成一个 getter 函数,允许外部访问该变量。 - 这是最常用的可见性修饰符。
-
private
:- 只能在当前合约的内部调用该函数。派生合约(继承的合约)无法访问
private
函数。 - 通常用于实现合约内部的辅助函数或逻辑。
- 只能在当前合约的内部调用该函数。派生合约(继承的合约)无法访问
-
internal
:- 可以在当前合约及其派生合约内部调用该函数。外部账户或其他合约无法直接调用
internal
函数。 - 用于实现合约及其子合约共享的逻辑。
- 我们有时候在构造函数中会调用用
internal
修饰的函数,使其在函数部署之前执行
- 可以在当前合约及其派生合约内部调用该函数。外部账户或其他合约无法直接调用
-
external
:- 只能从合约外部调用该函数。合约内部调用
external
函数需要使用this.functionName()
。 external
函数的参数数据存储在calldata
中,而不是memory
中,这可以节省 gas。- 通常用于处理大量数据或需要节省 gas 的场景。
- 我们一些函数只推荐使用该修饰符,如receive函数主要接受来自外部的函数,而内部不需要要调用
- 只能从合约外部调用该函数。合约内部调用
总结表格:
可见性修饰符 | 当前合约内部 | 派生合约内部 | 外部账户/合约 |
---|---|---|---|
public |
✅ | ✅ | ✅ |
private |
✅ | ❌ | ❌ |
internal |
✅ | ✅ | ❌ |
external |
this.f() |
❌ | ✅ |
状态修饰符
这些修饰符描述了函数如何与区块链状态进行交互。
-
view
:- 函数可以读取合约的状态变量,但不能修改它们。
- 调用
view
函数不会消耗 gas(从外部调用时),因为它不会产生状态变更的交易。 - 用于查询合约状态的函数。
-
pure
:- 函数既不能读取也不能修改合约的状态变量。它只能使用函数参数进行计算。
- 调用
pure
函数也不会消耗 gas(从外部调用时)。 - 用于执行与合约状态无关的纯计算。
3. 其他修饰符:
-
payable
:- 允许函数接收以太币。调用
payable
函数时,可以附加msg.value
来发送以太币。 - 用于处理涉及货币转移的函数,例如支付、充值等。
- 允许函数接收以太币。调用
-
自定义修饰符:
- 可以使用
modifier
关键字定义自己的修饰符,用于在函数执行前后添加一些通用的代码逻辑,例如权限检查、输入验证等。
- 可以使用
示例:
Solidity
javascript
pragma solidity ^0.8.0;
contract ModifierExample {
address public owner;
uint256 public counter;
constructor() {
owner = msg.sender;
counter = 0;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function.");
_; // 下划线表示函数体将在此处执行
}
function increment() public {
counter++;
}
function decrement() public view returns (uint256) {
return counter - 1;
}
function reset() public onlyOwner {
counter = 0;
}
function externalFunc(uint256 a) external pure returns(uint256){
return a*2;
}
function internalFunc(uint256 a) internal pure returns(uint256){
return a*2;
}
function testInternal(uint a) public returns(uint){
return internalFunc(a);
}
function payMe() public payable returns (uint256) {
require(msg.value > 0, "Please send some Ether.");
return msg.value;
}
}
在这个例子中:
onlyOwner
是一个自定义修饰符,用于检查调用者是否是合约的所有者。increment
是一个public
函数,可以被任何地方调用。decrement
是一个view
函数,可以读取counter
的值,但不能修改它。reset
是一个public
函数,使用了onlyOwner
修饰符,只有合约所有者才能调用。payMe
是一个payable
函数,可以接收以太币。externalFunc
是一个external
函数,只能从外部调用。internalFunc
是一个internal
函数,只能从合约内部调用。testInternal
是一个public
函数,演示了如何在合约内部调用internalFunc
。
总结:
理解这些 Solidity 的函数修饰符对于编写高质量的智能合约至关重要。通过合理地使用这些修饰符,可以控制函数的访问权限、状态修改能力,并提高合约的效率和安全性。希望以上解释能够帮助你更好地理解各种函数修饰符的区别。