区块链研究实验室 | 手把手教你使用Solidity开发智能合约(四)
3月29日
在上篇文章中,我们可以看到的数据位置是如何工作的(传送门:区块链研究实验室 | 使用Solidity开发智能合约(三)),这一篇我们将继续学习Solidity中的变量的过程。这次,我们将重点放在引用类型上,该引用类型应显式指定数据位置,正如我们在前几篇文章中提到的那样。我们还将看到如何定义映射,枚举和常量。
数组
在Solidity中,我们有两种类型的阵列:存储阵列和内存阵列。
1.存储阵列:
这些数组被声明为状态变量,并且可以具有固定长度或动态长度。
可以调整具有动态长度的存储阵列的大小,这意味着它们可以访问push()和pop()方法。
pragma solidity ^0.7.0;
contract A {
uint256[] public numbers;// dynamic length array
address[10] private users; // fixed length array
uint8 users_count;
function addUser(address _user) external {
require(users_count < 10, "number of users is limited to 10");
users[users_count] = _user;
users_count++;
}
function addNumber(uint256 _number) external {
numbers.push(_number);
}
}
2.内存阵列:
这些数组memory以其数据位置声明。它们也可以具有固定长度或动态长度,但是动态大小的内存阵列无法调整大小(即,不能调用push()和pop()方法)。数组的大小必须预先计算。
使用new关键字声明动态大小的内存数组,如下所示:
Type[] memory a = new Type[](size)
pragma solidity ^0.7.0;
contract B {
function createMemArrays() external view {
uint256[20] memory numbers;
numbers[0] = 1;
numbers[1] = 2;
uint256 users_num = numbers.length;
address[users_num] memory users1; // ERROR : expected integer literal
// or constant expression
address[] memory users2 = new address[](users_num);
users2[0] = msg.sender; // OK
users2.push(msg.sender); // ERROR : member push is not available
}
}
这里要提到的另一点是关于何时使用内存数组并编写如下内容:
uint256[] memory array;array[0] = 1;
您不会收到任何警告,但最终将获得无效的操作码,因为array根据内存中布局的说明,该操作码将指向零插槽,因此切勿将其写入。请记住,在使用数组之前,请务必先对其进行初始化,以便获取有效的地址。
数组切片
数组切片只能与calldata数组一起使用,并写为x[start:end]。切片的第一个元素为x[start],最后一个元素为x[end - 1]。
两个start和end是可选的:start默认为0与end默认为数组的长度。
特殊的动态尺寸阵列
1.byte[] 或者 bytes
这些数组可以保存任意长度的原始字节数据。两者之间的区别在于byte[]遵循数组类型的规则,并且如本部分文档所述, Solidity中的内存数组中的元素始终占据32字节的倍数。这意味着,如果元素的长度小于32字节的倍数,则将对其进行填充,直到其适合所需的大小为止。
在byte数组的情况下,这将浪费每个元素31个字节,而对于bytes或则不然string。我会提醒您,从内存中读取或写入一个字(32字节)会消耗3气,这就是为什么建议使用bytes而不是的原因byte[]。
2.string
string是UTF-8数据的动态数组。与其他语言相反,stringSolidity不提供获取字符串长度或执行两个字符串的串联或比较的功能(需要使用库)。
可以使用将字符串转换为字节数组bytes(<string>)。这将返回字符串的UTF-8表示形式的低级字节。
注意:可以将一个字符编码为一个以上的字节,这意味着字节数组的长度不一定是字符串的长度。
3.字符串与 bytes
该文档的大多数示例都使用bytes32代替string,并且还很明确地使用值类型bytes1来bytes32限制字符串的字节数,因为它便宜得多。
结构
与C和C ++一样,结构允许您定义自己的类型,如下所示:
struct Donation { uint256 value; uint256 date;}
一旦定义了结构,就可以开始将其用作状态变量或在函数中使用。
为了初始化一个结构,我们有两种方法:
1.使用位置参数:
Donation donation = Donation(msg.value,block.timestamp);
2.使用关键字:
Donation donation = Donation(msg.value,block.timestamp);
第二种方法将避免我们必须记住结构成员的顺序,因此它可能比第一种有用。
该结构的成员使用点来访问:
uint256 donationDate = myDonation.date;
“虽然结构本身可以是映射成员的值类型,也可以包含其类型的动态大小的数组,但结构不可能包含其自身类型的成员。这种限制是必要的,因为结构的大小必须是有限的。”
对应关系
您可以将映射视为大量的键/值存储,其中每个可能的键都存在,并且可以使用该键一键设置或检索任何值。
映射声明如下:
mapping( KeyType => ValueType) VariableName
该KeyType可以是任何内置值类型(我们看到的那些部分1),字节或字符串,或任何合约或枚举类型。的ValueType可以是任何类型的,包括映射,数组和结构。
这里要提到的一件事是,映射变量唯一允许的数据位置是storage,这意味着您只能将它们声明为状态变量,存储指针或库函数的参数。
枚举
枚举可让您将自定义类型下的相关值分组,如以下示例所示:
enum Color { green , blue, red }
enum使用以下语法可以访问值:
Color defaultColor = Color.green;
注意:也可以在合同或库定义之外的文件级别上声明枚举。
常量和不可变状态变量
状态变量可以声明为constant或immutable。在这两种情况下,在构造合同之后都无法修改变量。对于constant变量,该值必须在编译时固定,而对于immutable,它仍可以在构造时分配。
编译器不会为这些变量保留存储槽,并且每次出现都会由相应的值替换。
常量使用关键字声明constant:
uint256 constant maxParticipants = 10;
对于不可变状态变量,使用关键字声明它们immutable:
contract C { address immutable owner = msg.sender; uint256 immutable maxBalance; constructor(uint256 _maxBalance){ maxBalance = _maxbalance; }}
您可以在本节的文档中找到有关常量和不可变状态变量的更多详细信息。
注意:也可以constant在文件级别定义变量。
删除关键字
我要补充的最后一件事是delete在Solidity中的使用。
它用于将变量设置为其初始值,这意味着该语句的delete a行为如下:
对于整数,它等效于a = 0。
对于数组,它分配长度为零的动态数组或长度相同的静态数组,并将所有元素设置为其初始值。
delete a[x]删除x数组索引处的项目,并保持所有其他元素和数组长度不变。这尤其意味着它在阵列中留有间隙。
对于结构,它将为所有成员重置分配一个结构。
delete 对映射没有影响(因为映射的键可能是任意的,并且通常是未知的)。
练习时间:简单
在本练习中,我们将创建一个用于管理用户的合同。
以下是说明:
创建一个新文件并添加一个名为Crud的合同。
创建一个名为User的结构,其中包含用户的ID和名称。
添加两个状态变量并将其公开:1)动态的用户数组;2)每次创建新用户时将增加的id。
下一步是创建Crud函数,但是由于我没有向您介绍Solidity函数,因此我将为您提供声明函数的语法。在下一篇文章中,我们将对它们进行详细的讨论:
函数function_name(<param_type> <param_name>)<可见性> <状态可变性> [returns(<return_type>)] {...}
可见性可以是:公开,私有,内部,外部。
状态可变性可以是:查看,纯净,应付。
这是您将创建的功能的描述。
1.添加
可见性:公共
状态可变性:空
该函数将用户名作为参数,创建一个具有新ID的User实例(每次添加新用户时ID都会自动递增),并将新创建的用户添加到数组中。
2.阅读
可见性:公共
状态可变性:视图
此函数获取要查找的用户的ID,如果找到则返回用户名,否则返回用户名(有关稍后的异常处理)。
3.更新
可见性:公共
状态可变性:空
此函数将获取用户的ID和新名称,然后在找到相应用户时对其进行更新,或者在不存在该用户的情况下还原该事务。
4.销毁
可见性:公共
状态可变性:空
此函数将用户的ID删除,如果找到,则将其从数组中删除;如果用户不存在,则还原事务。
提示:由于最后三个函数都需要查找用户,因此您将需要创建一个私有函数,该函数将获取用户的ID并在找到时返回其在数组中的索引,以避免重复相同的代码。
课后联系答案:
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract Crud {
struct User {
uint256 id;
string name;
}
User[] public users;
uint256 public nextId = 1;
function add(string memory name) public {
User memory user = User({id : nextId, name : name});
users.push(user);
nextId++;
}
function read(uint256 id) public view returns(string memory){
uint256 i = find(id);
return users[i].name;
}
function update(uint256 id, string memory newName) public {
uint256 i = find(id);
users[i].name = newName;
}
function destroy(uint256 id) public {
uint256 i = find(id);
delete users[i];
}
function find(uint256 id) private view returns(uint256){
for(uint256 i = 0; i< users.length; i++) {
if(users[i].id == id)
return i;
}
revert("User not found");
}
}
至此,我们对变量的讨论结束了。下次,我们将研究功能以及如何在Solidity中使用它们。