## 基于Remix以太坊开发例程及个人感悟 创作来源:
- 一、通过实际操作中对以太坊智能合约编程有个初步认识
- 举一反三:以这个例子来分析一下如何设计一个简单的以太坊程序。
- 视频程序中出现的问题以及解答
- 关于调试的一些自我理解
- 二、尾语
创作来源:
B站视频: Up主:夏夜書.
CSDN文章:基于Remix-Ethereum开发的app打分智能合约(代码调试即测试文档编写详解).
开发环境:Remix.
本文面向刚刚接触以太坊区块链编程,希望能够在自我学习过程中巩固知识,如果有什么问题欢迎各位提出建议。
一、通过实际操作中对以太坊智能合约编程有个初步认识
首先,应该观看上文引用的夏夜书up主的视频。该视频首先介绍了新旧Remix编程差异,其次用一个例程从零到实现的过程教会我们来开发一个简单的程序。
具体实现代码如下:
pragma solidity >=0.4.22 <0.7.0;
contract Billboard {
struct App {
string name;
address owner;
uint8[] stars;
mapping(address => uint) star0f;
uint totalStar;
}
App[] public apps;
// event for EVM logging
event Publish(string indexed name, address indexed owner, uint appId);
event Star(address indexed user, uint indexed appId, uint8 num);
function publish(string memory name) public {
require(bytes(name).length>0, "name empty error");
apps.push(App(name,msg.sender,new uint8[](0),0));
emit Publish(name,msg.sender,apps.length-1);
}
function star(uint appId, uint8 num) public {
require(appId < apps.length,"app id error");
require(num <= 5 && num >= 1, "star num error");
require(apps[appId].star0f[msg.sender] == 0,"user error");
App storage app = apps[appId];
app.stars.push(num);
app.totalStar += num;
app.star0f[msg.sender] = app.stars.length-1;
emit Star(msg.sender,appId,num);
}
function top() public view returns (uint[] memory topIds){
topIds = new uint[](10);
for(uint appId = 1; appId < apps.length ; appId++){
uint topLast = appId < topIds.length? appId:topIds.length-1;
if(appId >= topIds.length&&apps[appId].totalStar <= apps[topIds[topLast]].totalStar){
continue;
}
topIds[topLast] = appId;
for(uint i = topLast; i > 0 ; i--){
if(apps[topIds[i]].totalStar > apps[topIds[i-1]].totalStar){
uint tempAppId = topIds[i];
topIds[i] = topIds[i-1];
topIds[i-1] = tempAppId;
}else{
continue;
}
}
}
}
}
举一反三:以这个例子来分析一下如何设计一个简单的以太坊程序。
1.将实现的项目进行拆解,分为结构体和功能函数。
智能合约由一个数组和具体实现的功能组成。
结构体可以定义名称,所有者,以及具体需要使用的变量等等。
2.文中的代码主要是通过调用Remix几个参考例程的例程代码作为基本模板,从而进行编程。(近期的小目标是理解Remix自带的四个例程,争取以博客的形式来激发自己学习的动力哈哈。)
首先,设置结构体。先调用一个模板
struct Voter {
uint weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint vote; // index of the voted proposal
}
//根据需求定义对应的变量,比如这次商店就需要名称,所有者,评分,评分关系。
//下面是基于模板修改的结构体
struct App {
string name; //定义一个字符串变量name
address owner;//定义一个地址类型
uint8[] stars;//定义一个数组存放评分数据
mapping(address => uint) star0f;//注1,在下文会解释该语法
uint totalStar;//定义一个整型变量分数
}
定义完结构体结构以后,要定义一个结构体数组变量。
App[] public apps;
//其中public表示公共的,可以被调用的数组
根据需求开始定义功能函数
a、发布商品函数
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
//以上是从模板拿来的调用结构体的函数,可以修改为
function publish(string memory name) public {
apps.push(App(name,msg.sender,new uint8[](1),0));
//apps.push()表示结构体数组增加一个变量并对其赋值
//name -> name 临时
//msg.sender(当前调用者地址) -> owner
//new uint8[](1) -> stars 新建一个长度为1的数组赋值给数组
// 0 -> totalStar 使APP评分为0
}
b、打分函数
function star(uint appId, uint8 num) public {
App storage app = apps[appId];
//新建一个结构体指针指向指定序号的apps数组【注2】
app.stars.push(num);
//在原有数组stars末端添加一个num元素进数组
app.totalStar += num;
//记录分数变量totalStar赋值
app.star0f[msg.sender] = app.stars.length-1;
//将star数组的长度-1的值赋值到star0f[msg.sender]映射的值
}
【注2】这行代码运行前,该函数有的状态为:
这段代码运行后,该函数有的状态为:
因此我更加倾向是在函数内部新建了一个类似指针的变量指向对于序号的数组。然后通过这个指针对数组里面的数据进行操作,也就是接下来的代码运行。(这是没有经过阅读代码手册的个人推导解读,因此如果有更好的解释请评论指出,非常感谢。)
c、排序函数
function top() public view returns (uint[] memory topIds){
topIds = new uint[](10);
for(uint appId = 1; appId < apps.length ; appId++){
uint topLast = appId < topIds.length? appId:topIds.length-1;
if(appId >= topIds.length&&apps[appId].totalStar <= apps[topIds[topLast]].totalStar){
continue;
}
topIds[topLast] = appId;
for(uint i = topLast; i > 0 ; i--){
if(apps[topIds[i]].totalStar > apps[topIds[i-1]].totalStar){
uint tempAppId = topIds[i];
topIds[i] = topIds[i-1];
topIds[i-1] = tempAppId;
}else{
continue;
}
}
}
}
以上这个函数的具体排序算法部分在这里就不解释了,让我们分析一下这个函数与其他函数的区别把。
这个函数是一个带返回值的函数,因为它最终要把排序之后的数组输出在
decode output上,如下图:
(uint[] memory topIds) -> 意思是返回这个临时生成的数组结果。memory在Remix中是一个相当于声明一个临时变量,该临时变量会在整个函数结束之后释放内存。
3.用require函数根据需求设计边界条件
a、publish函数的边界条件
考虑到发布商品的命名问题,我们应该杜绝发布无名字的商品。在这里简化成字符串长度必须大于0.因此在publish函数第一行添加require函数,从实现:
function publish(string memory name) public {
require(bytes(name).length>0, "name empty error");
//因为字符串是没办法判断长度,因此用bytes()转换成字节数判断长度
apps.push(App(name,msg.sender,new uint8[](1),0));
}
b、star函数的边界条件
因为star函数的评分最终是存储在函数中的数组,因此我们需要对此做出一系列边界条件:
1.由于发布的商品数量是有限的,因此我们要保证评价商品的数目要小于发布商品的数目,因此在这里用下面的代码进行限制。
require(appId < apps.length,"app id error");
2.由于本次评分系统采用的是一星到五星的分数打分,因此需要对num进行限制。为此引入下面的带面来限制。
require(num <= 5 && num >= 1, "star num error");
3.需要对重复评分的异常操作进行限制。因此在本代码中引入一个star0f数组来进行判断。具体的解析见下面文章。
最终实现的代码如下:
function star(uint appId, uint8 num) public {
require(appId < apps.length,"app id error");
require(num <= 5 && num >= 1, "star num error");
require(apps[appId].star0f[msg.sender] == 0,"user error");
App storage app = apps[appId];
app.stars.push(num);
app.totalStar += num;
app.star0f[msg.sender] = app.stars.length-1;
}
4.把输出结果反映到Remix调试终端
首先在定义结构体变量下方增加event事件函数,event事件要搭配 emit函数才能真正发挥作用。
两个event事件分别对应publish函数和star函数的emit事件。
event相当于声明一个事件,它其中包含那些参数。其中index(索引)表示的是该变量需要从终端对其进行一个赋值。同时address indexed owner会直接从终端抓取本次智能合约调用者的地址填入到指定值。
emit函数主要发挥一个触发功能,并把event事件中获取到的值填入到emit函数中对应的变量。
App[] public apps;
event Publish(string indexed name, address indexed owner, uint appId);
event Star(address indexed user, uint indexed appId, uint8 num);
function publish(string memory name) public {
//代码省略
emit Publish(name,msg.sender,apps.length-1);
}
function star(uint appId, uint8 num) public {
//代码省略
emit Star(msg.sender,appId,num);
}
一般emit函数放在整个函数的最后一行,起
视频程序中出现的问题以及解答
一、mapping(address => uint) star0f的语法解释
它发挥的功能类似python语言的字典。实际上的标准格式可以理解为mapping(key => value)
其中key可选的结构类型分别是字符串,数字,数组(如果按照python的要求来推断,但是实际在remix上运用还需要再多看几个例子,以后阅读的代码多了会回来修改的。)
其中value可以是任意结构类型。
在Remix中,常用的mapping(address => uint)主要是为了把一个复杂的变量:msg.sender(该变量是指智能合约调用者的地址)映射到一个具体的指,在该程序中相当于把(address)msg.sender => (uint)staf0f 这两个值对应起来。
在程序变量中可以看到:
require(apps[appId].star0f[msg.sender] == 0,"user error");
//上面的代码实际上可以翻译成:
require(apps[appId].star0f[1] == 0,"user error");
//通过对数组star0f[1]的值进行判断,如果值为0则表明没有评分过
//(因为数组生成时默认全部都是0)
//apps.push(App(name,msg.sender,new uint8[](1),0));这个商品发布函数时
//默认从在序号为0的数组上置0,因此后续的app.stars.push(num);函数会在序号
//为1的数组上将分数进行赋值,因此stars数组的总长度为2
app.star0f[msg.sender] = app.stars.length-1;
//在这里会把stars总长度-1也就是实现
app.star0f[msg.sender] = 1;
//因此app.star0f[msg.sender]里面的值不再是0而变成1,因此不满足
//require(apps[appId].star0f[1] == 0,"user error");这个条件
//因此就解决了重复评分的问题
关于调试的一些自我理解
一、在本次程序调试过程中主要发挥作用的地方。
函数运行的日志我们可以获取的信息:
先介绍一下详细信息:
这种不带返回值的函数日志中有用主要的信息:
function publish(string memory name) public {
//省略
}
这种带返回值的函数日志中有用主要的信息:
function top() public view returns (uint[] memory topIds){
//代码省略
}
下面的日志主要出现在带event事件声明以及函数中含有emit触发事件,我们可以从logs事件中查看智能合约从Remix虚拟机上获取的数据放在那个数组,以及智能合约是把这些数据存放在具体的那个变量中。
再介绍一下调试中较为常用的功能:
当我们点击Debug可以直接进入调试界面:
其实下面还有好几个调试状态查看器,但是我现在还没用到。等以后具体用到了会回来重新更新这篇文章的!
二、举个具体的例子来演示一下简单的调试用法
问题:视频中作者在第一次调试程序时发现程序可以重复评分,最终作者通过修改代码从而解决了这个重复评分BUG,下面就一步一步地使用调试模式找出具体原因把。
我们可以发现视频UP主做的修改主要是:
apps.push(App(name,msg.sender,new uint8[](0),0));
//替换为
apps.push(App(name,msg.sender,new uint8[](1),0));
两行代码的差别在于原来产生的数组是一个长度为0的数组,而新代码产生的数组是一个长度为1的数组。具体我们可以通过调试状态来看出:
状态0:
运行完代码后的状态如下:
作为对比,我们观察一下状态1时,函数运行的变量图。
从上面两个状态对比图可以知道发布函数产生的变量的区别,因此我们接下来可以逐一分析一下这个重复评分检测代码是如何正常工作的。
require(apps[appId].star0f[msg.sender] == 0,"user error");
这个代码的作用正如前面所说,是判断star0f[msg.sender]位置上的变量是否为0变量,如果不是则返回字符串信息:“user error”
在评分函数执行前,这个star0f[msg.sender]数组没有任何变量,因此默认整个数组上的变量均为0。因此第一次调用star函数符合require的逻辑,从而执行下面的代码。
App storage app = apps[appId];
app.stars.push(num);
app.totalStar += num;
app.star0f[msg.sender] = app.stars.length-1;
咱们接下来用调试模式分别看看在分步执行上述函数的时候,结构体数组app里面的变量是如何变化的。(PS:这里再次提醒,为了减少调试的时间,请务必用断点调试的方法进行调试,且设置断点时需要注意断点位置)
a、函数执行前的状态
b、执行了App storage app = apps[appId];
根据上面的两个图对比,可以清晰的看出各个状态都是一致的。因此我在前面得出了App storage app 中的storage发挥一个类似C语言中指针的作用。
因此在接下来的调试步骤,我就省略全局上的变量,而直接从这个局部函数的变量来分析接下来的调试步骤。
c、执行了app.stars.push(num);
d、执行了:app.totalStar += num;
e、执行了app.star0f[msg.sender] = app.stars.length-1;
我们在分析一下前面UP主一开始无法实现重复检测的原因。是因为stars数组的默认长度,当默认长度为0时,数组push以后,总长度变为1.再执行一遍
app.star0f[msg.sender] = app.stars.length-1;函数,会把0再次赋值到app.star0f[msg.sender] 的位置,因此就会导致require判断函数仍然满足,从而使重复检测函数失效。
综上所述,可以得到得到最终修改后代码:
apps.push(App(name,msg.sender,new uint8[](1),0));
从而实现整个完整逻辑功能。
二、尾语
经过这一次分析程序的运行以及调试,我对于Remix编程的初步理解几乎都呈现在上述文章中。这也是我第一次写博客,估计再未来还会对这篇文章做多次修改。所以,如果这篇文章能对你有所帮助,希望能点个赞。如果有什么建议或者疑问可以尝试在评论下发出,如果我能够解答一定会及时解答。
最后感谢jhw_12138.提供的视频资源、文章的提供。多亏他才发现了这个视频,激发了我想写这篇博客的想法。所以还是要非常感谢一波的哈哈哈。
其次感谢Up主:夏夜書.因为他的一个简简单单半小时视频,却充分运用了一些比较基础的知识点,帮助我们对Remix有个初步的认识,从而能够为接下来以太坊编程的学习有一个较好的理解!
希望接下来还能够继续发博客,噶油噶油!!!