第十二课 SOLIDITY语法难点解析及故障排查

第十二课 SOLIDITY语法难点解析及故障排查_第1张图片

1.编辑器说明

(1)推荐编辑器
目前尝试 Solidity 编程的最好的方式是使用 Remix (需要时间加载,请耐心等待)。Remix 是一个基于 Web 的 IDE,它可以让你编写 Solidity 智能合约,然后部署并运行该智能合约。
如果外网不能访问,可以访问欧阳哥哥搭建的REMIX编辑器
(2)Visual Studio Extension
Microsoft Visual Studio 的 Solidity 插件,包含 Solidity 编译器。
(3)Visual Studio Code extension
Microsoft Visual Studio Code 插件,包含语法高亮和 Solidity 编译器。

2. REMIN的函数引用

function mint(address receiver, uint amount)

(1) 在REMIX输入时,地址一定要有""表示,否则amount就取不到值。
例如是mint("0xca35b7d915458ef540ade6068dfe2f44e8fa733c",101)
(2) 程序中,如果注释包含中文,单步调试的位置不准确。

3.address相关全局函数

.balance (uint256):
该地址有多少以太坊余额(wei为单位)

.transfer(uint256 amount):
发送特定数量(wei为单位)的以太坊到对应地址,当出现错误时会扔出异常,但不会因异常而停止。固定需要耗费2300个gas。

.send(uint256 amount) returns (bool):
发送特定数量(wei为单位)的以太坊到对应地址,当出现错误时会返回flase。固定需要耗费2300个gas。

【警告】send() 执行有一些风险:如果调用栈的深度超过1024或gas耗光,交易都会失败。因此,为了保证安全,必须检查send的返回值,如果交易失败,会回退以太币。如果用transfer会更好。

.call(...) returns (bool):
CALL的低级调用函数,当失败时返回false。执行需要消耗不固定的gas。
【说明】不鼓励使用call函数,后期将会被移除。调用该函数可能造成安全攻击,详见后期安全相关文章。

.callcode(...) returns (bool):
CALLCODE的低级调用函数,当失败时返回false。执行需要消耗不固定的gas。
不建议使用,后续版本会删除。

.delegatecall(...) returns (bool):
DELEGATECALL的低级调用函数,当失败时返回false。执行需要消耗不固定的gas。
【说明】为了和非ABI协议的合约进行交互,可以使用call() 函数, 它用来向另一个合约发送原始数据,支持任何类型任意数量的参数,每个参数会按规则(ABI协议)打包成32字节并一一拼接到一起。一个例外是:如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名而直接使用。如果仅想发送消息体,需要避免第一个参数是4个字节。如下面的例子:

function callfunc(address addr) returns (bool){
bytes4 methodId = bytes4(keccak256("setScore(uint256)"));
return addr.call(methodId, 100);
}

测试地址和地址调用代码举例

pragma solidity ^0.4.18;

contract AddrTest{
    /*event函数知识把参数信息打印在REMIX等编译器的LOG位置区,不需要函数定义。*/
    event logdata(bytes data);
    event LogContractAddress(address exAccount, address contractAddress);
    
    uint score = 0;
    
    /*回调函数,没有函数名。任何调用不存在的函数,这时被调用的合约的fallback函数会执行。
     payable:如果一个函数需要进行货币操作,必须要带上payable关键字*/
    function() payable {
        logdata(msg.data);
    }
    
    /*智能合约构建函数*/
    function AddrTest(){
    LogContractAddress(msg.sender,this);
    }

    function getBalance() returns (uint) {
        return this.balance;
    }


    function setScore(uint s) public {
        score = s;
    }

    function getScore() returns ( uint){
        return score;
    }
}

contract CallTest{
    /*该函数有函数申明没有实际函数内容,在remix的value区域设置以太坊的个数,调用该函数会把外部账户(ACCOUNT)中的
      以太坊转移到智能合约账户中*/
    function deposit() payable {
    }

    event logSendEvent(address to, uint value);
    event LogContractAddress(address exAccount, address contractAddress);
    
    
    /*转以太坊给目标地址*/
    function transferEther(address towho) payable {
        towho.transfer(10);/*单位为wei*/
        
        logSendEvent(towho, 10);
    }
   
    /*不指定调用函数,则调用无函数名的回调函数*/
    function callNoFunc(address addr) returns (bool){
        return addr.call("tinyxiong", 1234);
    }

    /*制定调用函数的方法*/
    function callfunc(address addr) returns (bool){
        bytes4 methodId = bytes4(keccak256("setScore(uint256)"));
        return addr.call(methodId, 100);
    }  

    /*返回当前合约的以太坊余额*/
    function getBalance() returns (uint) {
        return this.balance;//0
    }  
    
    /*销毁智能合约,把以太坊余额返回给当前外部账户*/
    function ContractSuide() {
        LogContractAddress(this,msg.sender);
        suicide(msg.sender);
    }
}

4.Contract Related

this (current contract’s type):
表示当前合约,可以显式的转换为Address
selfdestruct(address recipient):
destroy the current contract, sending its funds to the given Address
销毁当前合约,发送当前以太坊余额到给定的地址
suicide(address recipient):
selfdestruct的别名函数

5. 区块和交易属性

block.blockhash(uint blockNumber) returns (bytes32): 给定区块的哈希—仅对最近的 256 个区块有效而不包括当前区块
block.coinbase (address): 挖出当前区块的矿工地址
block.difficulty (uint): 当前区块难度
block.gaslimit (uint): 当前区块 gas 限额
block.number (uint): 当前区块号
block.timestamp (uint): 自 unix epoch 起始当前区块以秒计的时间戳
msg.data (bytes): 完整的 calldata
msg.gas (uint): 剩余 gas
msg.sender (address): 消息发送者(当前调用)
msg.sig (bytes4): calldata 的前 4 字节(也就是函数标识符)
msg.value (uint): 随消息发送的 wei 的数量
now (uint): 目前区块时间戳(block.timestamp)
tx.gasprice (uint): 交易的 gas 价格
tx.origin (address): 交易发起者(完全的调用链)

注解
对于每一个外部函数调用,包括 msg.sender 和 msg.value 在内所有 msg 成员的值都会变化。这里包括对库函数的调用。

注解
不要依赖 block.timestamp、 now 和 block.blockhash 产生随机数,除非你知道自己在做什么。
时间戳和区块哈希在一定程度上都可能受到挖矿矿工影响。例如,挖矿社区中的恶意矿工可以用某个给定的哈希来运行赌场合约的 payout 函数,而如果他们没收到钱,还可以用一个不同的哈希重新尝试。
当前区块的时间戳必须严格大于最后一个区块的时间戳,但这里唯一能确保的只是它会是在权威链上的两个连续区块的时间戳之间的数值。

注解
基于可扩展因素,区块哈希不是对所有区块都有效。你仅仅可以访问最近 256 个区块的哈希,其余的哈希均为零。

6. 错误处理

assert(bool condition):
如果条件不满足就抛出—用于内部错误。

require(bool condition):
如果条件不满足就抛掉—用于输入或者外部组件引起的错误。

revert():
终止运行并恢复状态变动。

7 数学和密码学函数

addmod(uint x, uint y, uint k) returns (uint):
计算 (x + y) % k,加法会在任意精度下执行,并且加法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。

mulmod(uint x, uint y, uint k) returns (uint):
计算 (x * y) % k,乘法会在任意精度下执行,并且乘法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。

keccak256(...) returns (bytes32):
计算 (tightly packed) arguments 的 Ethereum-SHA-3 (Keccak-256)哈希。

sha256(...) returns (bytes32):
计算 (tightly packed) arguments 的 SHA-256 哈希。

sha3(...) returns (bytes32):
等价于 keccak256。

ripemd160(...) returns (bytes20):
计算 (tightly packed) arguments 的 RIPEMD-160 哈希。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) :
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。(example usage)

上文中的“tightly packed”是指不会对参数值进行 padding 处理(就是说所有参数值的字节码是连续存放的,译者注),这意味着下边这些调用都是等价的:
keccak256("ab", "c") keccak256("abc") keccak256(0x616263) keccak256(6382179) keccak256(97, 98, 99)
如果需要 padding,可以使用显式类型转换:keccak256("\x00\x12") 和 keccak256(uint16(0x12)) 是一样的。

请注意,常量值会使用存储它们所需要的最少字节数进行打包。例如:keccak256(0) == keccak256(uint8(0)),keccak256(0x12345678) == keccak256(uint32(0x12345678))。

在一个私链上,你很有可能碰到由于 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。这个原因就是他们被当做所谓的预编译合约而执行,并且在第一次收到消息后这些合约才真正存在(尽管合约代码是硬代码)。发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你的合约中实际使用它们之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。

8 Using for 如何使用

using A for B,这里A通常是某个library里面定义的某个方法,B是某种数据类型,这句话是把A方法绑定到B类型上,相当于给B类型附加了一个A方法。(也有翻译为附着库的)
在上面的例子中,将LibContract里定义的方法绑定到所有的数据类型。但是一般我们不会在所有的类型实例上都去调用LibContract的方法,应该是要按需using的,这里偷懒就写*。
在通俗一点的例子就是,
比如 using LibInt for uint,然后LibInt里面有定义一个toString方法。我们有一个uint a;那么可以这样调用a.toString(),toString方法在定义的时候,第一个参数会是一个uint类型的变量,表示调用者。

using A for B,A的函数的第一个参数必须和B的数据类型一致。

还有这个方法是可以重载的,你可以定义好几个同名的方法,但是第一个参数的类型不同,调用的时候自动的根据调用类型选择某一种方法。

9 数组

数组可以在声明时指定长度,也可以动态调整大小。 对于 "存储"的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体)。 对于 "memory"的数组来说,元素类型不能是映射类型,如果作为 public 函数的参数,它只能是 ABI 类型。

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组,应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。 要访问第三个动态数组的第二个元素,你应该使用 x[2][1](数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反,也就是说,x[2] 是从右边减少了一级)。

bytesstring 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 stringbytes 相同,但(暂时)不允许用长度或索引来访问。

注解
如果想要访问以字节表示的字符串 s,请使用 bytes(s).length / bytes(s)[7] = 'x';。 注意这时你访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。

可以将数组标识为 public,从而让 Solidity 创建一个 getter。 之后必须使用数字下标作为参数来访问 getter。

创建内存数组

可使用 new 关键字在内存中创建变长数组。 与"存储"(storage)数组相反的是,你不能通过修改成员变量 .length 改变 "内存"(memory)数组的大小。

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        // 这里我们有 a.length == 7 以及 b.length == len
        a[6] = 8;
    }
}

数组字面常数 / 内联数组

数组字面常数是写作表达式形式的数组,并且不会立即赋值给变量。

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) public pure {
        // ...
    }
}

数组字面常数是一种定长的 "内存"(memory) 数组类型,它的基础类型由其中元素的普通类型决定。 例如,[1, 2, 3] 的类型是 uint8[3] memory,因为其中的每个字面常数的类型都是 uint8。 正因为如此,有必要将上面这个例子中的第一个元素转换成 uint 类型。 目前需要注意的是,定长的 "内存"(memory) 数组并不能赋值给变长的 "内存"(memory) 数组,下面是个反例:

// 这段代码并不能编译。

pragma solidity ^0.4.0;

contract C {
    function f() public {
        // 这一行引发了一个类型错误,因为 unint[3] memory
        // 不能转换成 uint[] memory。
        uint[] x = [uint(1), 3, 4];
    }
}

已经计划在未来移除这样的限制,但目前数组在 ABI 中传递的问题造成了一些麻烦。

成员

length:
数组有 length 成员变量表示当前数组的长度。 动态数组可以在 存储storage (而不是 内存memory )中通过改变成员变量 .length 改变数组大小。 并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度。 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。

push:
变长的 存储storage 数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数,它用来附加新的元素到数组末尾。 这个函数将返回新的数组长度。

警告
在外部函数中目前还不能使用多维数组。

警告
由于 以太坊虚拟机Ethereum Virtual Machine(EVM) 的限制,不能通过外部函数调用返回动态的内容。 例如,如果通过 web3.js 调用 contract C { function f() returns (uint[]) { ... } } 中的 f 函数,它会返回一些内容,但通过 Solidity 不可以。
目前唯一的变通方法是使用大型的静态数组。

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // 注意下面的代码并不是一对动态数组,
    // 而是一个数组元素为一对变量的动态数组(也就是数组元素为长度为 2 的定长数组的动态数组)。
    bool[2][] m_pairsOfFlags;
    // newPairs 存储在 memory 中 —— 函数参数默认的存储位置

    function setAllFlagPairs(bool[2][] newPairs) public {
        // 向一个 storage 的数组赋值会替代整个数组
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 访问一个不存在的数组下标会引发一个异常
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 如果 newSize 更小,那么超出的元素会被清除
        m_pairsOfFlags.length = newSize;
    }

    function clear() public {
        // 这些代码会将数组全部清空
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // 这里也是实现同样的功能
        m_pairsOfFlags.length = 0;
    }

    bytes m_byteData;

    function byteArrays(bytes data) public {
        // 字节的数组(语言意义中的 byte 的复数 ``bytes``)不一样,因为它们不是填充式存储的,
        // 但可以当作和 "uint8[]" 一样对待
        m_byteData = data;
        m_byteData.length += 7;
        m_byteData[3] = byte(8);
        delete m_byteData[2];
    }

    function addFlag(bool[2] flag) public returns (uint) {
        return m_pairsOfFlags.push(flag);
    }

    function createMemoryArray(uint size) public pure returns (bytes) {
        // 使用 `new` 创建动态 memory 数组:
        uint[2][] memory arrayOfPairs = new uint[2][](size);
        // 创建一个动态字节数组:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(i);
        return b;
    }
}

10 关键字 memory 是什么?是用来做什么的?

Ethereum Virtual Machine(EVM) 拥有三类存储区域。

  • 第一类是 存储storage,贮存了合约声明中所有的变量。 虚拟机会为每份合约分别划出一片独立的 存储storage 区域,并在函数相互调用时持久存在,所以其使用开销非常大。
  • 第二类是 内存memory,用于暂存数据。其中存储的内容会在函数被调用(包括外部函数)时擦除,所以其使用开销相对较小。
  • 第三类是栈,用于存放小型的局部变量。使用几乎是免费的,但容量有限。

对绝大部分数据类型来说,由于每次被使用时都会被复制,所以你无法指定将其存储在哪里。

在数据类型中,对所谓存储地点比较重视的是结构和数组。 如果你在函数调用中传递了这类变量,假设它们的数据可以被贮存在 存储storage 或 内存memory 中,那么它们将不会被复制。也就是说,当你在被调用函数中修改了它们的内容,这些修改对调用者也是可见的。

不同数据类型的变量会有各自默认的存储地点:

  • 状态变量总是会贮存在 存储storage 中
  • 函数参数默认存放在 内存memory 中
  • 结构、数组或映射类型的局部变量,默认会放在 存储storage 中
  • 除结构、数组及映射类型之外的局部变量,会储存在栈中

例子:

pragma solidity ^0.4.0;

contract C {
    uint[] data1;
    uint[] data2;

    function appendOne() public {
        append(data1);
    }

    function appendTwo() public {
        append(data2);
    }

    function append(uint[] storage d) internal {
        d.push(1);
    }
}

函数 append 能一起作用于 data1 和 data2,并且修改是永久保存的。如果你移除了 storage 关键字,函数的参数会默认存储于 memory。这带来的影响是,在 append(data1) 或 append(data2) 被调用的时候,一份全新的状态变量的拷贝会在 内存memory 中被创建,append 操作的会是这份拷贝(也不支持 .push ——但这又是另一个话题了)。针对这份全新的拷贝的修改,不会反过来影响 data1 或 data2。

一个常见误区就是声明了一个局部变量,就认为它会创建在 内存memory 中,其实它会被创建在 存储storage 中:

/// 这份合约包含一处错误

pragma solidity ^0.4.0;

contract C {
    uint someVariable;
    uint[] data;

    function f() public {
        uint[] x;
        x.push(2);
        data = x;
    }
}

局部变量 x 的数据类型是 uint[] storage,但由于 存储storage 不是动态分配的,它需要在使用前通过状态变量赋值。所以 x 本身不会被分配 存储storage 的空间,取而代之的是,它只是作为 存储storage 中已有变量的别名。

实际上会发生的是,编译器将 x 解析为一个 存储storage 指针,并默认将指针指向 存储插槽storage slot 0 。这就造成 someVariable (贮存在 存储插槽storage slot 0)会被 x.push(2) 更改。(在本例中,两个合约变量 someVariable 和 data 会被预先分配到两个 存储插槽storage slot 中,即 存储插槽storage slot 0 和 存储插槽storage slot 1 。上面的程序会使局部变量 x 变成指向保存了变量 someVariable 的 存储插槽storage slot 0 的指针。译者注。)

正确的方法如下:

pragma solidity ^0.4.0;

contract C {
    uint someVariable;
    uint[] data;

    function f() public {
        uint[] x = data;
        x.push(2);
    }
}

18. solidity常见错误提示及原因分析

1). 智能合约执行失败

告警描述: " Warning! Error encountered during contract execution [Out of gas] "
发生场景:
执行官网众筹智能合约代码,在给智能合约打代币前,往智能合约地址账户打ETH,交易失败,提示如下:
点击查看信息链接
代码及原因分析:
下面是执行的回调函数,其中“tokenReward.transfer(msg.sender, amount / price);”的意思就是把智能合约的代币打给发送者账号。这个账号还没有代币,所以肯定会执行失败。

    function () payable {
        require(!crowdsaleClosed);
        uint amount = msg.value;
        balanceOf[msg.sender] += amount;
        amountRaised += amount;
        tokenReward.transfer(msg.sender, amount / price);
        FundTransfer(msg.sender, amount, true);
    }

解决方法:
先往智能合约账号打代币,然后打ETH,就不会执行失败了。

2). REMIX+MetaMASK的场景下,调用智能合约函数,提示不可知地址

告警描述:
REMIX输出框输出以下告警内容:
transact to Crowdsale.checkGoalReached errored: Unknown address - unable to sign transaction for this address: "0x3d7dfb80e71096f2c4ee63c42c4d849f2cbbe363"
发生场景:
更换了Meta账号后,调用之前账号创建的智能合约的函数
原因分析:
Remix输出框提示未知地址:

第十二课 SOLIDITY语法难点解析及故障排查_第2张图片
告警描述

查看MetaMask的当前账号,发现是Account 1的账号,所以无法执行合约。
第十二课 SOLIDITY语法难点解析及故障排查_第3张图片
MetaMask的当前账号

解决方法:
更换MetaMask为Account8(地址为0x3D7DfB80E71096F2c4Ee63C42C4D849F2CBBE363)的账号即可正常运行。

3). GETH安装时出现异常

告警描述:
GETH安装时出现以下告警:
E: Failed to fetch http://101.110.118.22/ppa.launchpad.net/ethereum/ethereum/ubuntu/pool/main/e/ethereum/ethereum_1.8.10+build13740+artful_i386.deb Could not connect to 101.110.118.22:80 (101.110.118.22), connection timed out
E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?
发生场景:
GETH安装,执行以下命令出现。

sudo apt-get install ethereum

原因分析:
解决方法:
<1> 参考网上解决方案
sudo vim /etc/resolv.conf ,添加:

nameserver 8.8.8.8

然后执行:

sudo /etc/init.d/networking restart

还是不行。
<2> 从绿地金融中心搬到家里,做相同的操作,就可以安装成功了。
应该是绿地金融中心的路由器做了某些配置,导致下载不成功。

4). 注释导致的错误问题

告警描述:

DocstringParsingError: Documented parameter "owner" not found in the parameter list of the function.

发生场景:
remix编译以下智能合约函数时出现:

    /** 
     * @dev Withdraw the token remained to the constructor address.
     * @param owner Address of owner.
     */
    function withdrawToken() public onlyCreator{
        if( 0 < token.balanceOf(address(this))) {
           token.transfer(creator, token.balanceOf(address(this)));
        }
    }    

原因分析:
remix竟然要检测@param后面跟的参数,看来注释不能随便乱写了。
解决方法:
函数修改为以下的就能编译通过了。

    /** 
     * @dev Withdraw the token remained to the constructor address.
     */
    function withdrawToken() public onlyCreator{
        if( 0 < token.balanceOf(address(this))) {
           token.transfer(creator, token.balanceOf(address(this)));
        }
    } 

5). mapping未初始化,mapping不能作为局部变量

告警描述:

browser/AddrTestCallTest.sol:9:7: TypeError: Uninitialized mapping. Mappings cannot be created dynamically, you have to assign them from a state variable.
      mapping(address => uint) shares;
      ^-----------------------------^

发生场景:
原来代码:

// 不要使用这个合约,其中包含一个 bug。
contract Fund {

    /// 提取你的分成。
    function withdraw() public {
      /// 合约中 |ether| 分成的映射。
      mapping(address => uint) shares;
    
        if (msg.sender.send(shares[msg.sender]))
            shares[msg.sender] = 0;
    }
}

原因分析:
映射类型,一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue)。键类型允许除映射、变长数组、合约、枚举、结构体外的几乎所有类型()。值类型没有任何限制,可以为任何类型包括映射类型。

映射可以被视作为一个哈希表,所有可能的键会被虚拟化的创建,映射到一个类型的默认值(二进制的全零表示)。在映射表中,并不存储键的数据,仅仅存储它的keccak256哈希值,这个哈希值在查找值时需要用到。
正因为此,映射是没有长度的,也没有键集合或值集合的概念。

映射类型,仅能用来作为状态变量,或在内部函数中作为storage类型的引用。

可以通过将映射标记为public,来让Solidity创建一个访问器。通过提供一个键值做为参数来访问它,将返回对应的值。
映射的值类型也可以是映射,使用访问器访问时,要提供这个映射值所对应的键,不断重复这个过程。

解决方法:
代码放到函数外面,就不会产生告警。

pragma solidity ^0.4.0;

// 不要使用这个合约,其中包含一个 bug。
contract Fund {
    
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;    

    /// 提取你的分成。
    function withdraw() public {

    
        if (msg.sender.send(shares[msg.sender]))
            shares[msg.sender] = 0;
    }
}      

6). 局部变量太多,导致栈太深告警

告警描述:

browser/PuzzleBID.sol:319:23: CompilerError: Stack too deep, try removing local variables.
        require(works[_worksID].beginTime != 0); //检查该作品游戏是否存在
                      ^------^

发生场景:
写了一个比较复杂的算法函数,里面含有比较多的局部变量。

原因分析:
stack 保存很小的局部变量,免费使用,但有数量限制(16个变量);
官方的“解决方案”是建议开发者减少变量的使用,并使函数尽量小。当然还有其他几种变通方法,比如把变量封装到struct或数组中,或是采用关键字memory(不知道出于何种原因,无法用于普通变量)。
解决方法:
把函数拆分成更小的函数,或者把变量包装成struct结构,减少变量的数量。

7). 修改数据长度得到编译告警

告警描述:

browser/AddrTestCallTest.sol:11:9: TypeError: Expression has to be an lvalue.
        memArr.length++;             // 非法
        ^-----------^
browser/AddrTestCallTest.sol:14:9: TypeError: Expression has to be an lvalue.
        storageArr.length++;                             // 非法
        ^---------------^

发生场景:
solidity代码如下:

// 这将无法编译通过

pragma solidity ^0.4.18;

contract C {
    int8[] dynamicStorageArray;
    int8[5] fixedStorageArray;

    function f() {
        int8[] memory memArr;        // 第一种情况
        memArr.length++;             // 非法

        int8[5] storage storageArr = fixedStorageArray;   // 第二种情况
        storageArr.length++;                             // 非法

        int8[] storage storageArr2 = dynamicStorageArray;
        storageArr2.length++;                     // 合法


    }
}

原因分析:
你可以使用 arrayname.length = ; 来调整 存储storage 中的动态数组(也就是在合约级别声明的数组)的长度。如果你得到一个 lvalue 错误,那么你有可能做错了以下两件事中的一件或全部。

  • 你在尝试修改长度的数组可能是保存在 内存memory 中的
  • 你可能在尝试修改一个非动态数组的长度。

解决方法:
修改函数。

num). 问题描述模板

告警描述:
发生场景:
原因分析:
解决方法:

9. 常见问题及解答

1).modifer函数是干什么的?

2).如何打币回支付账号?

3).智能合约的定时器和系统函数是什么?

4).当创建一个智能合约时,msg.sender和this的区别?
答复:msg.sender是指外部账户的地址,this是指当前创建的智能合约的地址。

contract AddrTest{
    event LogContractAddress(address exAccount, address contractAddress);
    
    function AddrTest(){
    LogContractAddress(msg.sender,this);
    }    
}

在remix运行,可以证明这个推测
第十二课 SOLIDITY语法难点解析及故障排查_第4张图片
LOG说明

10 View Functions,Pure Functions,Fallback Function的定义?

答案:
参考《【易错概念】Solidity语法constant/view/pure关键字定义》 文章分析,也可参考点击官网链接。

11. 文档参考

1,官方中文网站 http://solidity-cn.readthedocs.io/zh/develop/

2, tiny熊翻译系列
1] Solidity教程序列1 - 类型介绍
2] 智能合约语言Solidity教程系列2 - 地址类型介绍
3] 智能合约语言 Solidity 教程系列3 - 函数类型
4] 智能合约语言 Solidity 教程系列4 - 数据存储位置分析
5] 智能合约语言 Solidity 教程系列5 - 数组介绍
6] 智能合约语言 Solidity 教程系列6 - 结构体与映射
7] 智能合约语言 Solidity 教程系列7 - 以太单位及时间单位
8] 智能合约语言 Solidity 教程系列8 - Solidity API
9] 智能合约语言 Solidity 教程系列9 - 错误处理
10] 智能合约语言 Solidity 教程系列10 - 完全理解函数修改器

你可能感兴趣的:(第十二课 SOLIDITY语法难点解析及故障排查)