solidity 102
约 6794 字大约 23 分钟
2022-04-26
Data processing and storage
value type & reference type
concept
到目前为止,已经学习了五种变量类型:int
、uint
、bool
、address
和 mapping
。
它们还可以分为两类:值类型 和 引用类型 。
比喻
要想区分值类型和引用类型,引入一个鞋盒例子,值类型表示你在盒子里放一双鞋 - 你将值存储在变量中。
而引用类型表示你在鞋盒中放一个带有地址的便条。
这意味着什么?如果你使用“ b = a ”将一个变量的值分配给另一个变量,如果它们是值类型,则更新 a 不会影响 b 。然而,如果它们是引用类型,则更新 a 也会同时更新 b 。
真实用例
在ERC20
中,_balances
就是一个引用类型,而_totalSupply
是一个值类型。
mapping(address => uint256) private _balances; //引用类型
uint256 private _totalSupply; //值类型
documentation
到目前为止,掌握了四种值类型变量:int
、uint
、bool
、address
。
和唯一引用类型变量:mapping
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Example {
mapping(int256 => address) map;
function types() public {
uint256 a = 1;
uint256 b = a;
a = 2; //a 已更新,但 b 保持不变
b = 4; ///b 已更新,但 a 保持不变
map[1] = address(0x123);
}
}
FAQ
在赋值时,值类型和引用类型分别是怎么运行的?
data location - storage
concept
引用类型是指变量存储的是数据的地址。这个地址在哪里呢?
每种引用类型都有一个数据位置,指明变量值应该存储在哪里。Solidity 提供3种类型的数据位置:storage
、memory
和 calldata
。
而 storage
则是作用于合约的存储结构。
比喻
storage
就像共享文档,任何对storage
的改动,都会被所有人同步更新。
真实用例
在 ERC20 中,_totalSupply
就是一个存储在 storage
的变量,所有函数都可以访问该变量。
同时在 OpenZepplin
的 GovernorUpgradeable
合约中,state
函数也先将获取到的 proposal
定义为 storage
变量,这样对 proposal
的修改将会同步到合约的状态变量中。
uint256 private _totalSupply;
//GovernorUpgradeable.sol L143
function state(uint256 proposalId) public view virtual override returns (ProposalState) {
ProposalCore storage proposal = _proposals[proposalId];
}
documentation
所有的状态变量
都在 storage
中
pragma solidity ^0.8.4;
contract StorageExample {
//这个字符串状态变量存储在 storage 中 可以不显示指定
string name = "hello";
function update() public {
name = "hello~";
}
}
FAQ
究竟什么是 storage
?
这个位置用于存储合约的状态变量。存储在此位置的数据被持久化存储在以太坊区块链上,因此消耗的gas
更大。
何时使用 storage
?
任何想要永久存储在以太坊区块链上的内容都应该存储在 storage 中。
contract MyContract {
//在函数外定义的状态变量默认存储在storage中
mapping (int => bool) b;
}
data location - memory
concept
除了 storage
,还有另一个重要的数据存储位置 - memory
。
memory
在 Solidity 中表示一个临时数据存储区域。与 storage
不同,存储在 memory
中的数据在函数调用结束时会被清空,不具有持久性。
比喻
如果把storage
比作共享文档,那么memory
则更像是本地存档,我们可以查看到共享文档的内容,并把它们复制到本地进行修改或运算,但是这些修改都不会影响到共享文档的状态。
真实用例
在ERC20
中,transfer
函数先调用_msgSender()
函数,随即将该函数的返回值赋值给一个局部变量owner
,这是一个存储在memory
的变量。
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
documentation
要在 memory
中声明变量,需要在函数内部定义它,然后加上关键字 memory
。
pragma solidity ^0.8.4;
contract MemoryExample {
function example() public pure {
// tempStr 是存储在memory中的局部变量
string memory tempStr = "Hello, World!";
}
}
在 Solidity 中,对于函数内*(函数外就是状态变量了)的局部变量:值类型(如 uint
、bool
)默认存储在栈上。对于引用类型(引用类型必须明确指定 memory
或 calldata
),like this:
pragma solidity ^0.8.4;
contract MemoryExample {
// 状态变量默认是 storage
string stateStr = "Hello";
function example() public pure {
// 局部变量必须指定 memory
string memory tempStr = "Hello, World!";
// 函数参数也可以用 calldata
// function example2(string calldata _str) public pure { ... }
}
}
FAQ
为什么使用 memory?
与 storage 相比,memory
在 gas
成本方面更小,在 memory
中读写数据会划算很多。
data location - calldata
在 Solidity 中,calldata
是一种特殊的数据存储位置,主要用于存储函数调用的输入参数。以下是对 calldata
的详细介绍:
1. 定义
calldata
是一个只读的数据位置,用于存储外部函数调用时传递的参数。它在函数执行期间有效,但不会永久存储数据。与其他数据位置(如 memory
和 storage
)相比,calldata
具有一些独特的特性。
2. 特性
- 只读:
calldata
中的数据是不可修改的,不能对其进行写入操作。这意味着在函数内部无法改变calldata
中的值。 - 临时性:
calldata
中的数据仅在函数调用期间存在,函数执行结束后,calldata
中的数据将不再可用。 - 低 Gas 消耗:由于
calldata
是不可变的,编译器可以进行优化,从而减少 Gas 消耗,尤其是在处理函数参数时。 - 支持外部调用:
calldata
主要用于外部函数调用的参数传递,适合接收来自外部账户或其他合约的输入数据。
3. 数据布局
在 calldata
中,数据是以字节形式存储的,布局与内存相似。前四个字节通常对应于函数签名的选择器,其余字节对应于函数的输入参数。每个输入参数通常以 32 字节对齐,即使其实际大小小于 32 字节时也会进行填充。
4. 使用场景
calldata
适用于需要接收外部输入但不需要修改这些输入的场景。例如,当你需要验证用户输入或处理外部合约调用的参数时,可以使用 calldata
来提高效率和降低 Gas
成本。
5. 示例代码
以下是一个使用 calldata
的简单示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Example {
struct TokenDistribution {
address tokenAddress;
uint256 amount;
}
function distributeTokens(TokenDistribution[] calldata distributions) external {
for (uint i = 0; i < distributions.length; i++) {
// 这里可以安全地读取 distributions 中的数据
// 但不能修改它
address token = distributions[i].tokenAddress;
uint256 amount = distributions[i].amount;
// 进行代币分配逻辑
}
}
}
在这个例子中,distributeTokens
函数接收一个 TokenDistribution
结构体数组作为参数,使用 calldata
以确保参数在函数执行期间只读且高效
string
definition
concept
字符串string
是一种表示文本数据的数据类型。字符串是由一系列字符组成,例如数字、字母、标点符号等。
比喻
你可以将字符串想象成一段话、一首歌曲的歌词、一篇文章或者任何包含文字的东西。
真实用例
在 ERC20
标准的实现中,string
数据类型主要用于代币的名称和符号。 名称通常是代币的全名,例如“比特币代币”或“以太币代币”。 符号是代币的缩写,如“ BTC ”或“ ETH ”。
这些属性用于在交易或查看代币时快速识别该代币。 下面是声明两个字符串 _name
和 _symbol
的代码:
string private _name;
string private _symbol;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
documentation
要声明一个字符串,使用关键字 string :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract LearningStrings {
string car = "BMW";
string text;
//使用函数将值赋给字符串变量
function setText () public returns (string memory) {
text = "Hello World";
return text;
}
}
concat
concept
concat
是字符串的连接操作。
比喻
如果把字符串看作是一段文字的话,那么字符串的拼接就是把这些文字给连接起来形成一篇完整的文章。
真实用例
在 NFT 的 ERC721 标准中,每个 NFT 都由代币 ID 唯一标识。 ERC721 标准中的 tokenURI
函数用于通过将两个字符串连接在一起为每个 NFT 创建唯一的 URI:基本 URI 和令牌 ID。
基础 URI 对于合约中的所有 NFT 都是通用的,而代币 ID 对于每个 NFT 都是唯一的。 通过将这两个字符串连接在一起,tokenURI
函数为每个 NFT 创建一个唯一的 URI。 该 URI 用于访问有关 NFT 的信息,例如其名称、图像或其他详细信息。
下面是 ERC721 标准中 tokenURI
函数的代码实现:
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
...
string memory _tokenURI = _tokenURIs[tokenId];
string memory base = _baseURI();
...
return string.concat(base, _tokenURI);
}
documentation
连接字符串,可以使用 string.concat
函数。
//定义两个字符串变量(str_1 和 str_2)
string memory str_1 = "hello ";
string memory str_2 = "world";
//将上面的两个字符串变量传递给 concat 函数,
//该函数将返回这两个字符串的拼接结果"helloworld"
string memory result = string.concat(str_1, str_2);
FAQ
字符串连接的使用场景?
● 构建消息:我们可以使用字符串连接将多个消息片段组合成一条消息。 ● 格式化输出:可以使用字符串连接以可读的方式格式化输出。例如,我们可能会将字符串和数字连接起来以显示余额为“ Your balance is 10 ETH ”。
length
concept
一个 字符串 是由字符集合组成的,而 字符串长度 指的是它所包含的字符数。
比喻
例如,如果一个书信中有100个字符,那么它的长度就是100。同样地,在Solidity中,如果一个字符串中有10个字符,那么它的长度就是10。
真实用例
在OpenZepplin的Strings.sol中,equal
函数使用了bytes(a).length
语法获取了字符串a
的长度。
function equal(string memory a, string memory b) internal pure returns (bool) {
return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));
}
documentation
要确定 Solidity 中字符串的长度,可以使用内置的 bytes
类型,该类型表示动态的 bytes 数组。bytes
类型有一个 length
属性,用来返回数组中的 bytes 数量。通过它可以得到字符串的长度。
pragma solidity ^0.8.0;
contract StringLength {
string name = "the daughter";
string testS;
bytes testB;
function test() public {
testS = string.concat(name, "hello");
uint l = bytes(testS).length; //计算testS字符串长度,将其赋值给uint变量 l
testB = bytes(testS); //将字符串转化为byte数组
}
function getLength(string memory str) public pure returns(uint) {
bytes memory bytesStr = bytes(str); //将字符串转化为byte数组
return bytesStr.length; //返回字符串长度
}
}
FAQ
字符串长度一般何时会被使用?
● 输入验证:有时需要验证输入以确保其符合某些要求。例如,可能要求字符串输入的长度不超过某个特定长度。为此我们需要获取输入的字符串长度。
● 操作字符串:在操作字符串时,经常需要知道它们的长度,以执行连接和比较等操作。
struct
definition
concept
在 Solidity 中,结构体是一种用户自定义的数据类型,其中可以包含多个不同类型的属性。
例如一个学生可以有很多属性,比如姓名、学号、年级等。我们可以将这些属性封装到一个结构体中。
比喻
结构体可以存储很多信息,并且把他们有结构的存储起来。
真实用例
在 OpenZepplin 提供的 AccessControl
合约中使用到了 struct
结构来表示一个管理员角色的信息,其中包括地址→是否有权限的映射和 adminRole
的哈希。
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
documentation
要定义一个结构体,首先你需要使用 struct
关键字,其后是结构体的名字。然后需要用{}
将其属性括起来,{}
里面每个属性用“;”隔开,结构体属性的定义与状态变量的定义相同,只是没有作用域这个概念。
struct Cat {
string name;
address owner;
uint256 age;
}
FAQ
为什么使用结构体?
结构体能更加方便的组织和管理相关的数据,使代码更加清晰和易于理解。
例如我们可以创建多个" Student "结构体,每个结构体对应着一个特定的学生,并包含其所有属性,这能更好的组织和管理学生数据。
initialization
concept
初始化结构体意味着创建一个新的结构体实例,可以通过指定数据来创建一个独特的结构体。
比喻
假设有一个结构体称为"学生",它有几个属性,比如姓名、学号和年级。结构体定义了学生的属性,但是需要具体的学生实例来代表一个具体的学生。
初始化结构体就像是招收一位新学生。当初始化结构体时,为每个属性赋予具体的值,就像是为学生指定姓名、学号和年级。通过为每个属性赋值,创建了一个特定的学生实例。
真实用例
在 GovernorStorage 合约中,通过结构体名( {param name : param}
)的方式初始化 ProposalDetails结构体。
struct ProposalDetails {
address[] targets;
uint256[] values;
bytes[] calldatas;
bytes32 descriptionHash;
}
_proposalDetails[proposalId] = ProposalDetails({
targets: targets,
values: values,
calldatas: calldatas,
descriptionHash: keccak256(bytes(description))
});
documentation
要想初始化一个结构体,需要使用结构体的名字后跟括号的方式,括号里面是结构体的属性值,这需要和结构体定义时的属性一一对应。
Student("zhangsan", 18);
access
concept
访问一个结构体变量。
真实用例
同样在 GovernorStorage 合约当中,execute
函数通过 details.targets
,details.values
等方式获取了 details
结构体的各个属性。
function execute(uint256 proposalId) public payable virtual {
// here, using storage is more efficient than memory
ProposalDetails storage details = _proposalDetails[proposalId];
execute(details.targets, details.values, details.calldatas, details.descriptionHash);
}
documentation
要想访问一个结构体,需要使用 StructName.PropertyName
的形式访问,这个和 Java 中类的访问方式是一样的。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Example {
struct Student {
string name;
uint256 studentId;
uint256 grade;
}
//我们在这个函数中先初始化了一个名为student的实例
//随后使用实例名+"."+属性名的形式访问了name属性
function testUpdate() public pure returns(string memory name) {
Student memory student = Student("Alice", 1, 3);
name = student.name;
}
}
update
concept
结构体通常在创建后需要修改其属性的值。这可能是因为需要更新数据、记录新的状态或者进行其他与结构体相关的操作。
真实用例
在 OpenZepplin 的 AccessControl 合约中,_setRoleAdmin()
函数从 mapping 中检索到 _roles[role]
结构体后,为该结构体的 adminRole
属性赋值。
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
mapping(bytes32 role => RoleData) private _roles;
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
_roles[role].adminRole = adminRole;
}
documentation
想要修改结构体的属性,可以使用如下语法:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Example {
struct Student {
string name;
uint256 studentId;
uint256 grade;
}
//我们在这个函数中先初始化了一个名为student的实例,其原名为Alice,然后我们将其name改为了Bob
//并将旧名字和新名字作为返回值分别返回
function testUpdate() public pure returns(string memory oldName, string memory newName) {
Student memory student = Student("Alice", 1, 3);
oldName = student.name;
student.name = "Bob";
newName = student.name;
}
}
dynamic array
definition
concept
数组是一种用来存储相同类型数据的集合。
比喻
例如一个班级里的学生集合,我们可以将每个学生看作是集合中的一个元素,而班级则是一个数组。
真实用例
在 OpenZepplin 的 GovernorStorage
合约中,定义了一个 uint256 的数组 _proposalIds
,用来存储已经提交提案的 proposald
。
uint256[] private _proposalIds;
documentation
为了定义动态数组,需要使用类型+[]
的方式定义。
uint256[] arr;
在这里我们定义了一个名为 arr 的 uint256 类型的动态数组。
FAQ
动态数组和静态数组有什么区别?
静态数组需要在声明时确定的固定大小,而动态数组的大小可以在运行时进行调整。
例如,我们需要将1,2,3这三个数字存入数组中,使用固定大小为5的静态数组和使用动态数组的内存占用情况如图:
动态数组有什么优点?
1.可以在运行时根据需要调整大小。
2.只占用实际元素所需内存空间,节省内存。
3.可以使用多种函数和操作进行元素操作,更加灵活易用。
push
concept
向数组里添加元素的方式- push
。
当处理动态数组时,使用 push
是一种常见的方式。它允许我们在数组的末尾添加新的元素,而无需事先知道数组的大小或指定索引。
真实用例
同样在 GovernorStorage
合约中,_propose
函数用于提交提案,于是我们使用 _proposalIds.push
的方式将此提案的 proposalId
存储到数组中。
function _propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
address proposer
) internal virtual override returns (uint256) {
uint256 proposalId = super._propose(targets, values, calldatas, description, proposer);
// store
_proposalIds.push(proposalId);
...
}
documentation
为了向动态数组的末尾添加新的元素,需要使用 ArrayName.push()
,括号中为要添加的元素。
pragma solidity ^0.8.0;
contract Example {
uint256[] public nums;
//这里我们像nums数组的末尾依次push了元素1,2,3
//执行完后该数组的结构应该为[1,2,3]
function testPush() public {
nums.push(1);
nums.push(2);
nums.push(3);
}
}
FAQ
无论什么数组都可以使用 push 吗?
只有变长的 storage 数组有 push()
方法。
pop
concept
使用 pop
是在处理动态数组时常见的一种操作,它允许我们从数组的末尾删除最后一个元素。并且在 pop
之后,数组的长度会减小。
真实用例
在 EnumerableSet
中的 _remove
函数,通过 pop
来删除 set
中的一个配置。
function _remove(Set storage set, bytes32 value) private returns (bool) {
set._values.pop();
}
documentation
为了将动态数组的末尾元素删除,需要使用 ArrayName.pop()
。
pragma solidity ^0.8.0;
contract Example {
uint256[] public nums;
function testPush() public {
nums.push(1);
nums.push(2);
nums.push(3);
//执行到这里该数组的结构应该为[1,2,3]
//将数组最后一个元素删除
nums.pop();
//此时数组的结构应该为[1,2]
}
}
如果尝试弹出一个空数组的元素,Solidity 将无法找到任何元素来弹出,并且会引发错误。
FAQ
在什么时候使用 pop?
当有需求删除数组的最后一个元素时,多数情况需要将要删除的元素移到数组末尾,随后使用 pop
将其删除。
length
concept
在 Solidity 中,数组的长度是指数组中元素的数量。
documentation
只需要在数组名字后加上 .length
即可。
uint256 len = arr.length;
数组的长度是用 uint256 类型来存储的。
FAQ
动态数组和静态数组的 Length 各有什么特点?
对于静态数组,其长度在声明时被指定且不可修改;而对于动态数组,其长度可以在运行时进行调整。
access
concept
在此之前有必要了解数组下标这个信息。数组下标从0开始,到数组长度减1结束。下标为 0 的元素是数组中的第一个元素,下标为 1 的元素是数组中的第二个元素,以此类推。
真实用例
同样在 ERC721Enumerable
合约中,可以通过 tokenByIndex()
函数在 _allTokens
数组中找到对应 index
的 token 信息。
uint256[] private _allTokens;
function tokenByIndex(uint256 index) public view virtual returns (uint256) {
...
return _allTokens[index];
}
documentation
为了获取动态数组元素,需要访问数组,使用数组名+[index]
的方式。
uint256 num = arr[10];
在这里获取了一个名为 arr 的数组的10号索引,也就是第十一个元素,并将其赋值给 num 变量。
fixed-size array
concept
介绍一个新的数组定义形式——定长数组。
比喻
当你在 Solidity 中声明一个定长数组,可以将其比喻为一个有固定大小的盒子。这个盒子只能容纳特定数量的项目,无法改变大小。
真实用例
固定长度的数组在 solidity 当中很少使用,因为该方式非常不灵活。一般在定义数组时是不知道这个数组究竟有多少个元素的。
但是当我们想要表示某个集合时,就可以使用固定长度的数组:
string[4] public seasons;
seasons[0] = spring;
seasons[1] = summer;
seasons[2] = autumn;
seasons[3] = winter;
documentation
在定义定长数组时,需要在[]
中指定数组的长度:
contract GradeBook {
uint256[5] public grades; // 存储学生成绩的定长数组,长度为5
function setGrade(uint256 index, uint256 grade) public {
require(index < grades.length, "Invalid index"); // 确保索引不超过数组长度
grades[index] = grade;
}
function getGrade(uint256 index) public view returns (uint256) {
require(index < grades.length, "Invalid index"); // 确保索引不超过数组长度
return grades[index];
}
}
FAQ
为什么要使用定长数组?它有哪些优势?
1.确定长度:定长数组的固定长度使得编译器能够在编译时进行更多的优化。
2.更高的效率:定长数组的存储方式是在插槽中连续存储,这使得对数组的访问速度更快,因为可以通过偏移量直接访问元素,而无需进行额外的计算(例如动态数组中的哈希)。 3.节省存储空间:由于定长数组的长度是确定的,存储每个元素所需的空间也是已知的。这使得在存储定长数组时更加高效,因为不需要为存储数组长度而额外分配空间。
4.避免越界错误:定长数组在编译时会检查数组的访问是否越界,并在必要时引发错误。这可以提供更好的安全性,避免在运行时出现数组越界的问题。
flow control
if
concept
if-else
语句允许对代码片段进行有条件的执行,它允许根据某个条件的真假来执行不同的代码块。
```solidity
if (condition) {
2 // 在条件为真时要执行的代码
3} else {
4 // 在条件为假时要执行的代码
5}
documentation
使用 if
关键字定义 if
语句,后面跟着用括号()
括起来的条件,然后是用{}
括起来的代码块。如果条件为假,将跳过该代码块。
if (value == 10) {
// 当 value 为 10 时执行的代码
} else if (value == 20) {
//else if用于在if条件不被满足时添加多个条件进行检查。
//如果之前的if条件未被满足,则将检查下一个else if条件,并在其中找到第一个满足条件的代码块进行执行。
// 当 value 为 20 时执行的代码
} else if (value == 30) {
// 当 value 为 30 时执行的代码
} else {
//else是if语句的可选部分,用于指定当if条件为false时要执行的代码块。
//如果if条件不被满足,则else语句中的代码块将被执行。
// 当 value 都不满足时执行的代码
}
while
concept
while
循环语句是一种常见的流程控制语句,用于反复执行一段代码块,直到指定的条件表达式不再为真为止。
真实用例
Strings
库提供了一系列用于字符串操作的函数。 它提供了各种实用程序,用于将数值转换为字符串、比较字符串以及执行其他与字符串相关的任务。 该库中的函数之一 toString
使用 while
循环将 uint256 值转换为其 ASCII 字符串十进制表示形式。
让我们仔细看看 while
循环是如何在 toString
中使用的功能:
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), _HEX_DIGITS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
在此函数中,while
循环迭代直至值变为零。 在每次迭代期间,循环提取值的最后一位数字,将其转换为字符,并将其存储在缓冲区中。 然后将该值除以 10 以删除最后一位数字。 此过程一直持续到整个值已转换为字符串。
该库中 while
循环的使用有助于将数值有效转换为字符串。 这个真实的用例演示了如何在 Solidity 中使用 while
循环来创建高效的字符串操作函数。
documentation
使用关键字 while
定义 while
循环,后跟用括号()
括起来的条件。要执行的代码块用{}
括起来。
只要条件仍然为真,代码块就会不断被执行。
uint i = 0;
while (i < 10) {
i++;
// 每次迭代执行的代码
}
do while
concept
do-while
循环语句使得一段代码块可以重复执行,只要某个特定条件保持为真。while
循环和 do-while
循环的关键区别在于后者在执行代码块后再判断条件,因此它保证至少执行一次代码块。
documentation
使用关键字 do
定义 do-while
循环,后跟要执行的代码块并用 {}
括起来,最后是关键字 while
和括在括号 ()
中的条件。
代码块将至少执行一次,并在条件为真的情况下继续执行。
uint i = 0;
do {
i++;
// 每次迭代要执行的代码
} while (i < 10);
FAQ
Do while 和 while 有什么区别?
while
循环先检查条件再执行循环体,而 do-while
循环先执行循环体再检查条件。
for
concept
for
循环语句和 while
和 do-while
一样。它根据特定条件反复执行一个代码块,直到满足某个条件为止。
documentation
使用 for
关键字来定义 for
循环,后面跟着一个初始化语句、一个条件和一个增量语句。所有这些都被括在圆括号()
中。要执行的代码块被括在{}
中。
for (uint i = 0; i < 10; i++) {
// 每次迭代执行的代码
}
FAQ
for与while的区别。
从功能上讲,for
循环和 while
循环是完全相同的,也就是说,while
可以做到的,for
也可以做到,反之亦然。它们之间的关键区别在于代码风格:for
循环更具结构化,它清晰地阐述了初始条件、增量过程和最终条件。