00:00
我们讲到了有问题的合约,我们在remix里边也已经实现了一下,然后大家发现看起来这个合约是没什么毛病的,我们运行的时候,如果说我们不考虑太多的话,就考虑这个传进来的这个二的话,发现是可以看得到的,但是我们会发现它还会捎带的改变A的值。为什么他会有这样一个很奇特的表现,这里就是确实是扫地里边一个,可以说可以说是一个大坑,也可以说就是所谓的设计上的一个缺陷,呃,我们不管怎么样吧,但是我们至少要知道它是怎么回事儿,然后我们就可以尽量去避免,对吧?我们看一下在这个定义的过程当中,主要问题就出在X上面。因为我们看到了这里,它唯一的一个warning其实就放在这里了,而这个warning告诉我们的是这个X,它是被声明成了一个storage类型的指针,所以我们知道所有的状态变量默认都是在storage里面存储的。而我们的。
01:17
呃,局部变量它的默认存储也是在storage里面,那在这个里边我们直接声明了一个storage类型的。可变长度的数组,因的数组,而且又没有给它赋赋值,所以它在我们的存储空间当中是一个没有被定位的一个,就是没有分配对应存储空间的一个指针,那在造力地里边大家都知道所有的。变量一开始都是要被负一个初值的,对吧,那我们的A一开始负出值给的是零,B初值也是零。贝塔,一开始我们看不到值。那是因为。
02:02
它本身没有长度,所以说我们就看不到值,对吧,同样这里的X一开始的时候不付出值的话,也是没有长度,那它是一个指针,它会指向哪里呢?它就会指向我们整个合约定义的存储空间最开始的地方。也就是整个存储空间的零位置。所以我们可以看到,在这里我们的零位置应该是存储着哪个变量。对,其实就是前面我们看到A的存储位置就是一开始定义,那么它就是在零的位置,所以。我们就会看到在后边如果要是我们调用F的时候。X的变化,它部署了一个二之后,A也会随着变化。那这时候大家可能就又会想了,那也不对啊,A变是变了,但是X不是给他PUSH2吗?那怎么A是变成了一呢?
03:05
而且我们会发现每一次push一次,Push一次二的时候,发现A都会增加一下,对吧,而且每次A还不一样,他每次都增加。这是什么原因?指针在哪里?呃,这个其实是我们前面没有详细给大家讲的一个点啊,就是像我们所说的一个。长度是动态变化的,这样的一个数组,它存储的时候我们说了它所有的数据并不是直接存在给它分配的那个空间里的,对吧?那么它的它的主要的数据都是放在就是直接做了一个哈希散列之后,存到其他地方很很遥远的地方都是散开分布的,那本身一开始定义的从上到下这个顺序存储的位置呢,在我们这里首先是A去存储,然后是B去存储,这是一个顺顺序的存储过程,对吧?那如果我们定义一个。
04:09
长度可变的数组,在这个顺序存储的位置里边,它会存一个东西,它会存一个什么呢?会存它的长度。所以长度可变的数组是这样一种存储模式,就是在我们直接声明的这个地方,再去按照这个顺序存储的地方,它是会把自己的长度存在这个位置。然后我们找这个变量的时候,当然我们那个变量指针是指向这里的,对吧,就会直接找到这个位置,然后可以直接读取到它的长度。那如果要找它里边的某个元素呢?再用它元素的索引值和本身它所处的这个位置共同计算一个哈希。那么哈希出来的那个值就是它存对应的这个元素值的地方。
05:03
这个可能有点绕,大家觉得能听懂啊,可能就是不太好理解啊,但是简单理解就是说在正常存储空间里边放的不是它的值,而是先放了一个整个这个变长数组的长度,就是点LA那个。它的属性。说,嗯。要要画一个图,哎,我们这边好像也没有黑板之类的东西,我在这里电脑上画也画不好啊,呃,可能就是大家可能需要去想象一下,就是大概它的这个是是怎么样去存的,所以在我们这一个例子里边,这个合约里边,首先按照这个顺序存储,第一个位置存着的应该是A,对吧。然后第二个位置啊,现在我们这个位置的话,存着的是B。然后再下面存的是data。啊,当然这个得塔的话,这里也是一个变长的数组,所以存的是data塔的一个长度,对吧,那么大家看,当我们到了一个方式里边的时候,F里边的时候,我们定义的X,它又是一个变长的数组。
06:11
没有定义,所以一开始它是直接指向我们整个这个合约存储空间的最开始的位置的。所以它只到了A那里。是不是这样只到了A那里,然后我们又认为它是一个变长数组,长度可变的数组?我们把它里边push了一个二。这个时候会发生什么事情?对,它的长度就变成了一对吧,所以我们会向它直接指向的那个地方塞进去一个一。然后会把它布置进来的,这个二放到哪里呢。算哈希,算完哈希之后,存在很遥远的一个地方,那个是它的第一个元素。那个地方,那个空间里面存着的数字,数字是二,而本身的这个第一个位置存的是一。
07:06
但是在我们的状态变量里边,本身第一个位置定义的是A,对不对,所以我们一看A就变成了e date,对,那date是指向哪呢?Date本身它指向的是第三个存储位置,对吧?所以它没有被影响到。在我们后来又指定了X贝塔等于X把X付给了贝塔。那这个时候贝塔的值就会从X指向的那个地方拷贝过来一份,把我们的状态变量改变,所以大家可以看到,这个时候贝塔那个位置也就存上了一个一,这是它的长度,然后这个值会指向很遥远的一个地方。存了一个二对,所以大家看的时候,贝塔的值是真正的变成二了,但是呢,A的值对就会变化,就会变成一。呃,对,这这种问题主要就是出在函数里面,我们如果定义了局部变量的时候,它默认是一个story的存储,如果我们定义的时候又没有给它赋值,没有告诉他应该指向哪里,它就会自动的指向我们整个这个存储空间最开始的地方。
08:18
所以大家就可以看到,为什么之后我们每掉一次,它就会又A,就又会增长一个呢。就是因为我们在之后如果我们再调一次F的时候,又定了定义了一个XX又是指向指到A那里了,这个时候A前面我们已经已经是变成一了,对吧?那说明A这里如果如果把它当成一个的话,A的值是一,但是如果我们把它当成一个可变数组的存储的话,那它就是长度是一的一个数组。这个时候我们再去push一个二,那它就长度变成二了。所以是这样的一个状态,所以大家就会发现,看起来好像这个A变成了一个计数器一样,每每次点一次,然后他就他就加一个,每次点一个加一个,事实上它其实是保存了一个可变数组的长度。
09:14
所以这个A的值就会被我们后面这个操作给覆盖掉,好,那这里我就提一个问题了,呃,但这个应该是。是是他不是很清楚是吧,嗯呃,这个跟浮点没有关系。就是跟浮点还是整形没有关系,它是跟它到底是定长数组还是变长数组有关系,我的意思是A不是保存定义一个点的好,那我们直接把它定义成一个浮点看一下啊,比方说我们定一个a fix对吧,1FIX的一个A。
10:09
诶,我这里边好像出了一点问题。哦,大家可以看到这个浮典型似乎是4.25还没有出啊,这应该是更后面版本才才有的一个类型,所以说呃,我不太确定是不是我这里编译器就是没有没有下载完整的问题,大家那里可以自己也试一下,如果要是改一个u fix是不是会有问题。换一下啊,U fix还是会报这个问题啊。大家那里能编译吗?也编译不了是吧,所以应该是编译器的问题,如果大家往好的话,我这里不敢随便换啊,我这里如果随便换一个编译器应该就下不下来了,如果大家网好的话,可以去尝试着换一个高版本,假如说换一个5.0 0.5.0的版本的编译器,它应该是可以把它编译通过的,大家可以简单的想一下,就是假如说它这里A的数据类型不一样,会出什么样的事情。
11:22
嗯。呃,这里我们如果再给A赋值是吧,那就是看我们的调用顺序了。如果说我们先在前面给X push,呃,Push了一个二,然后我们再去给A赋一个值,那肯定A就又重新写了,对吧,后面的负值是有效的,但如果你是前面,比方说我们,我们来一个constructor啊,嗯。啊对,肯定会影响数组,就是如果我们再给A进行了。就是做了一个赋值操作的话,假如说A现在给的给的值给到了三,那之后我们再去定义X的时候,一开始它指向了A,它就会默认为自己是一个长度为三的可变数组。
12:14
对吧。他就会默认这么认为,然后在push进去的时候。他同样还是应该会给。A加一对不对。只不过前面的A表示三的时候,所代表的那三个元素我们不一定能找得到,对吧。是这样。所以我们可以试一下啊,我们来一段代码试一下吧,我们写一个,那就随便定义另外一个方式啊,我们写一个G这样的一个方式,Public类型,然后我们要求去输入一个整形的数吧,Input,然后我们就直接把A变成input,大家看一下好。
13:06
好,现在我们先看一下A的值,B的值都是零,这个没问题对吧。那G我们先给一个值,给一个五,我看一下。A现在就变成了五。B还是没有变的。那现在我如果去调用一下F,会出什么事情?我们试一下吧,大家结果说话啊,大家看一下诶。就变成了六,因为调用F的时候,它默认指向了A那个位置,他认为这就是一个长度是五的一个数组,对吧?那我如果再部署进去,我当然要把它改成六了。只不过就是咱们这里是没有办法去访问,就是假装A是一个一个长度可变的数组的,咱们没有办法用这种方式去访问,如果要能访问的话,咱们就可以看到它其中其中的。
14:09
第六个元素是真正铺进去的,二前面的元素其实都是没有的,对吧。所以大家能能大概能想象得到他的这个处理的机制。呃,之前面是大家还提了一个什么问题,我一下想起来了,就是咱们讲完之后,第一位同学提的问题是什么呢?然后A换了数据,数据类型之后会有什么问题,对吧,大家可以想象我们这里A。他就存了一个一个整数,那如果说我们给他的数据类型是一个,比方说那我们来一个奇怪的数据类型吧,我们给个三二。这个我们就先删了啊,大家觉得这个有影响吗?这个会变成什么样子?
15:04
来,我们实验说话,实际的操作一下就知道了。A,一开始大家可以看到它的初始化是什么样子的,就全是这样对吧?就是32位,32位字节嘛,对吧,所以说它就全是零。这是它的定义啊,D当然也是零了,然后我们F调用一下,大家看一下A会变成什么样子,也就是很简单的加一而已。因为他既然是30,他32位字节这个东西对于我们的。真正的物理意义上的存储而言。他如果要是零的话,它也就全是零而已嘛,没有什么区别对吧,只不过它的数据类型代表什么,就是我们怎么样去解读这个零,这是数据类型代表的意思。那它是32位字节的话,我们认为那32个零这是字符串哦,或者说这是一组字符,那我们会把它解解读成32个零字符,如果说我们认为它是U变U形的整形变量的话,那么我们就就认为它直接就是零这个数。
16:15
所以说这是我们解读的意义不同而已,实际上它里边所有的这整个这个变量的空间存的就是全是零。所以说,如果当我们把它解读成这个空间存的是一个长度可变的数组的时候,它的加一,那就还是后面加一。那大家能想到我再掉一次之后,它会变成什么样?看一下啊。又掉了一次。就是变成二对吧,所以大家看到是一样的,只要你表示的这个方法是一样的,所以它是一样的,好,那这一部分我们就先说到这里,这个例子其实很经典,诶这里给大家再提一个问题吧,就是我们这里是说这个这个程序有问题,那我们要。
17:05
我们现在想改,他怎么改?怎么样把它改的没问题。就是能实现我们现在希望的这个。实现的功能。每一次掉一次F,我们会相当于是把data塔的值push一个二进去,对吧,把贝塔的值改了,那大家说这个能有什么改动的方法吗?我们不要把A给改了,对吧。初嗯,把初始化的时候,A初始化一个值是吗?哦,就是说给给data初始化是吧,但这个就跟我们一开始的状态变量定义就冲突了,我们定义它就是一个可变长度的对吧。对,我们要改的是X对吧,问题就出在这,它是一个storage类型的指针,而且没有指定它指向哪里,它就直接指到我们就是初始的位置,指到A那里去了,那这个东西怎么怎么改呢?其实这里有一个非常简单的更改的渠道啊,就是让他直接指向data就可以了。
18:23
贝塔是一个storage类型的变长数组。那么我们又定义了一个storage类型的变长数组,这样X指向data之后,就相当于变成了data的一个。引用。然后我们之后直接X一个二贝塔是不是就会变。嗯,点呃哦,直接去用data点就就是点二是吧,对当然是可以的,就是我们如果想要去直接操作这个的话,当然也是可以的。
19:05
只不过就是说我们在这里只是演示一下,就是我们的这个状态变量作为一个引用,呃,然后在这个局部变量里边作为引用去更改状态变量的这种操作方式。因为我们知道状态变量,我们随时都可以去去改它嘛,对吧,但是直接你直接上来就是这点push的这种方式一般,呃,不是不是很提倡。就是说。如果说我们同时有很多人去调这个这个东西的话,可能data这里就会有有各种各样的问题出现啊,我们先看一下这个是不是是不是符合我们的预期结果。大家看一下,首先看好这个A还是这个,这不重要,反正看它变不变就行了,对吧,然后我们掉首先看一下data有没有,Data没有这里报错了。F掉一次,先看data塔塔现在有了二给过来了,因为这是一个引用对吧,就是只要X变了,那其实data塔也就变了。
20:10
那么我们看一下BB肯定没没变啊,A也没变。所以这样的一个使用的方式,这是正确的使用方式。所以大家注意一下solidity里边局部变量story类型,一个大客。这个有点违反我们的直觉,主要是对吧,一般大家认为局部变量嘛,我直接这么定义出来,你肯定就是在站里边,或者是在内存里面,它怎么会默认去放到那个就是对吧,全局的那种存储空间里面去,不可变的那种存储空间去,这个是很违反直觉的一个,所以大家一定注意一下。
我来说两句