solidity虽然跟js很像,但实际还是有很多不一样的地方,如果不专门学习solidity语法,只能朦朦胧胧,似懂非懂。
官方的资料:https://solidity-cn.readthedocs.io/zh/develop/index.html 但看完没多大用
其他的链接:
https://www.qikegu.com/docs/4922 简单易懂
https://www.tryblockchain.org/ 专业的文档2,值得看
http://www.mamicode.com/info-detail-2286137.html 用于温习回顾,走马观花列一遍语法
https://www.iqiyi.com/v_19rqt6letg.html 千峰视频
1,状态变量storage和局部变量memory
两者区别很容易理解,memory可以理解为临时变量,不会记录在链上,而storage是永久存储的。
一个版本的解读:
Storage
该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。
保存在存储区(Storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。
Memory
内存位置是临时数据,比存储位置便宜。它只能在函数中访问。
通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。
Calldata
Calldata是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,Calldata是外部函数的参数(而不是返回参数)的默认位置。
Stack
堆栈是由EVM (Ethereum虚拟机)维护的非持久性数据。EVM使用堆栈数据位置在执行期间加载变量。堆栈位置最多有1024个级别的限制。
可以看到,要永久性存储,可以保存在存储区(Storage)。
- 变量定义时默认为storage,而作为函数参数时,默认为memory
contract HelloWorld{
//等价于 string storage public a;
string public a;
//参数等价于string memory _a
function changeNum(string _a){
}
}
- 当函数参数为memory类型时,相当于值传递,storage才是指针传递
contract HelloWorld2{
string public a;
function HelloWorld2(){
a = "abc";
}
function f(){
changeNum(a);
}
function changeNum(string _a){
bytes(_a)[0] = "d";
//由于_a默认为memory,所以_a只是值传递,所以此时修改a的值是不成功的,输出还是abc
//需要把函数参数修改为string storage _a,才能输出dbc
}
}
- 将变量赋值给一个新变量时,新变量的类型由赋值给它的类型决定。
function changeNum(string _a){
//_a默认为memory类型,所以b也为memory
string b = _a;
bytes(_a)[0] = "d";
}
数组:
对于存储(storage)数组,元素类型可以是任意的(可以是其他数组、映射或结构)。对于内存(memory)数组,元素类型不能是映射类型,如果它是一个公共函数的参数,那么元素类型必须是ABI类型。
类型为bytes和字符串的变量是特殊数组。bytes类似于byte[],但它在calldata中被紧密地打包。字符串等价于bytes,但(目前)不允许长度或索引访问。
因此,相比于byte[],bytes应该优先使用,因为更便宜。
创建内存数组
可以使用new关键字在内存中创建动态数组。与存储数组相反,不能通过设置.length成员来调整内存动态数组的长度。
length
数组有一个length成员来表示元素数量。动态数组可以通过更改.length成员,在存储器(而不是内存)中调整大小。创建后,内存数组的大小是固定的(但是是动态的,长度可以是函数参数)。
类型转换:
Solidity允许类型之间进行隐式转换和显式转换。
隐式转换时必须符合一定条件,不能导致信息丢失。例如,uint8可以转换为uint16,但是int8不可以转换为uint256,因为int8可以包含uint256中不允许的负值。
(1)函数修饰符:
比如onlyOwner
// 定义修饰符 onlyOwner 不带参数
modifier onlyOwner(){
//如果调用合约的人不是合约创建者则throw
if(msg.sender != sender) throw;
_; //占位符
}
//这样a函数就只能被合约的创建者调用了
function a() onlyOwner{
...
}
修饰符定义中出现特殊符号_
的地方,用于插入函数体。如果在调用此函数时,满足了修饰符的条件,则执行该函数,否则将抛出异常。
// 定义修饰符 costs 带参数
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
contract Register is Owner {
mapping (address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) public { price = initialPrice; }
// 使用修饰符 costs
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
// 使用修饰符 onlyOwner
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
(2)pure、view、constant三种函数定义
当函数有返回值时,可以添加这三种定义,用这三种方式定义的函数都只执行读操作,不会进行编译执行。即用了这三种方式定义的函数,不会执行函数里的逻辑,只会执行一个返回的读操作。所以执行这些函数不需要消耗gas费用。
pure区别是用于返回非变量,如returns 10;
而view和constant用于返回全局变量,两者的区别为新旧版本
uint public a = 1;
//由于被constant声明的函数执行读操作,所以a无法被修改
//执行为f(),a依然为1
function f() constant {
a = 3;
}
View(视图)函数不会修改状态。如果函数中存在以下语句,则被视为修改状态,编译器将抛出警告。
- 修改状态变量。
- 触发事件。
- 创建合约。
- 使用
selfdestruct
。 - 发送以太。
- 调用任何不是视图函数或纯函数的函数
- 使用底层调用
- 使用包含某些操作码的内联程序集。
Getter方法是默认的视图函数。声明视图函数,可以在函数声明里,添加view
关键字。
Pure(纯)函数不读取或修改状态。如果函数中存在以下语句,则被视为读取状态,编译器将抛出警告。
- 读取状态变量。
- 访问
address(this).balance
或<address>.balance
- 访问任何区块、交易、msg等特殊变量(msg.sig 与 msg.data 允许读取)。
- 调用任何不是纯函数的函数。
- 使用包含特定操作码的内联程序集。
如果发生错误,纯函数可以使用revert()
和require()
函数来还原潜在的状态更改。
声明纯函数,可以在函数声明里,添加pure
关键字。
pragma solidity ^0.5.0;
contract Test {
function getResult() public pure returns(uint product, uint sum){
uint a = 1;
uint b = 2;
product = a * b;
sum = a + b;
}
}
fallback(回退) 函数是合约中的特殊函数。它有以下特点
- 当合约中不存在的函数被调用时,将调用fallback函数。
- 被标记为外部函数。
- 它没有名字。
- 它没有参数。
- 它不能返回任何东西。
- 每个合约定义一个fallback函数。
- 如果没有被标记为
payable
,则当合约收到无数据的以太币转账时,将抛出异常。
// 没有名字,没有参数,不返回,标记为external,可以标记为payable
function() external {
// statements
}
pragma solidity ^0.5.0;
contract Test {
uint public x ;
function() external { x = 1; }
}
contract Sink {
function() external payable { }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x 是 1
address payable testPayable = address(uint160(address(test)));
// 发送以太测试合同,
// 转账将失败,也就是说,这里返回false。
return (testPayable.send(2 ether));
}
function callSink(Sink sink) public returns (bool) {
address payable sinkPayable = address(sink);
return (sinkPayable.send(2 ether));
}
}
智能合约 Contract
Solidity中,合约类似于c++中的类。合约包含以下部分:
- 构造函数 – 使用
constructor
关键字声明的特殊函数,每个合约执行一次,在创建合约时调用。 - 状态变量 – 用于存储合约状态的变量。
- 函数 – 智能合约中的函数,可以修改状态变量来改变合约的状态。
(1)可见性:有public、private、internal和external四种访问权限
- 1.函数默认声明为public,即可以以internal方式调用,也可以通过external方式调用。可以理解为能够被内部合约访问和外部合约访问。
- 2.Internal声明的只允许通过internal方式调用,不能被外部合约。而external能够被外部合约访问。
- 3.private和internal类似,都不能被外部合约访问,唯一的不同是private函数不能被子类调用,而internal可以。
构造函数是使用construct
关键字声明的特殊函数,用于初始化合约的状态变量。合约中构造函数是可选的,可以省略。
构造函数有以下重要特性:
- 一个合约只能有一个构造函数。
- 构造函数在创建合约时执行一次,用于初始化合约状态。
- 在执行构造函数之后,合约最终代码被部署到区块链。合约最终代码包括公共函数和可通过公共函数访问的代码。构造函数代码或仅由构造函数使用的任何内部方法不包括在最终代码中。
- 构造函数可以是公共的,也可以是内部的。
- 内部构造函数将合约标记为抽象合约。
- 如果没有定义构造函数,则使用默认构造函数。
- 如果基合约具有带参数的构造函数,则每个派生/继承的合约也都必须包含参数。
- 可以使用下面的方法直接初始化基构造函数
pragma solidity ^0.5.0;
contract Base {
uint data;
constructor(uint _data) public {
data = _data;
}
}
contract Derived is Base (5) {
constructor() public {}
}
- 可以使用以下方法间接初始化基构造函数
pragma solidity ^0.5.0;
contract Base {
uint data;
constructor(uint _data) public {
data = _data;
}
}
contract Derived is Base {
constructor(uint _info) Base(_info * _info) public {}
}
- 不允许直接或间接地初始化基合约构造函数。
- 如果派生合约没有将参数传递给基合约构造函数,则派生合约将成为抽象合约。
就像Java、C++中,类的继承一样,Solidity中,合约继承是扩展合约功能的一种方式。Solidity支持单继承和多继承。Solidity中,合约继承的重要特点:
- 派生合约可以访问父合约的所有非私有成员,包括内部方法和状态变量。但是不允许使用
this
。 - 如果函数签名保持不变,则允许函数重写。如果输出参数不同,编译将失败。
- 可以使用
super
关键字或父合同名称调用父合同的函数。 - 在多重继承的情况下,使用
super
的父合约函数调用,优先选择被最多继承的合约。
(4)抽象合约
类似java中的抽象类,抽象合约至少包含一个没有实现的函数(抽象函数)。通常,抽象合约作为父合约,被用来继承,在继承合约中实现抽象函数,抽象合约也可以包含有实现的函数。
如果派生合约没有实现抽象函数,则该派生合约也将被标记为抽象合约。
pragma solidity ^0.5.0;
contract Calculator {
function getResult() public view returns(uint);
}
contract Test is Calculator {
function getResult() public view returns(uint) {
uint a = 1;
uint b = 2;
uint result = a + b;
return result;
}
}
(5)接口
接口类似于抽象合约,使用interface
关键字创建,接口只能包含抽象函数,不能包含函数实现。以下是接口的关键特性:
- 接口的函数只能是外部类型。
- 接口不能有构造函数。
- 接口不能有状态变量。
- 接口可以包含enum、struct定义,可以使用
interface_name.
访问它们。
(6)库
库类似于合约,但主要作用是代码重用。库中包含了可以被合约调用的函数。
Solidity中,对库的使用有一定的限制。以下是库的主要特征。
pragma solidity ^0.5.0;
library Search {
function indexOf(uint[] storage self, uint value) public view returns (uint) {
for (uint i = 0; i < self.length; i++) if (self[i] == value) return i;
return uint(-1);
}
}
contract Test {
uint[] data;
constructor() public {
data.push(1);
data.push(2);
data.push(3);
data.push(4);
data.push(5);
}
function isValuePresent() external view returns(uint){
uint value = 4;
// 使用库函数搜索数组中是否存在值
uint index = Search.indexOf(data, value);
return index;
}
}
(7)事件
事件是智能合约发出的信号。智能合约的前端UI,例如,DApps、web.js,或者任何与Ethereum JSON-RPC API连接的东西,都可以侦听这些事件。事件可以被索引,以便以后可以搜索事件记录。
事件在区块链中的存储
区块链是一个区块链表,这些块的内容基本上是交易记录。每个交易都有一个附加的交易日志,事件结果存放在交易日志里。合约发出的事件,可以使用合约地址访问。
// 声明一个事件
event Deposit(address indexed _from, bytes32 indexed _id, uint _value);
// 触发事件
emit Deposit(msg.sender, _id, msg.value);
示例:
pragma solidity ^0.5.0;
contract Counter {
uint256 public count = 0;
event Increment(address who); // 声明事件
function increment() public {
emit Increment(msg.sender); // 触发事件
count += 1;
}
}
上面的代码中,
event Increment(address who)
声明一个合约级事件,该事件接受一个address类型的参数,该参数是执行increment
操作的账户地址。emit Increment(msg.sender)
触发事件,事件会记入区块链中。
按照惯例,事件名称以大写字母开头,以区别于函数。
用JavaScript监听事件
下面的JavaScript代码侦听Increment
事件,并更新UI。
counter = web3.eth.contract(abi).at(address);
counter.Increment(function (err, result) {
if (err) {
return error(err);
}
log("Count was incremented by address: " + result.args.who);
getCount();
});
getCount();
索引(indexed)参数
一个事件最多有3个参数可以标记为索引。可以使用索引参数有效地过滤事件。下面的代码增强了前面的示例,来跟踪多个计数器,每个计数器由一个数字ID标识:
pragma solidity ^0.4.21;
contract Multicounter {
mapping (uint256 => uint256) public counts;
event Increment(uint256 indexed which, address who);
function increment(uint256 which) public {
emit Increment(which, msg.sender);
counts[which] += 1;
}
}
counts
替换count
,counts
是一个map。event Increment(uint256 indexed which, address who)
添加一个索引参数,该参数表示哪个计数器。emit Increment(which, msg.sender)
用2个参数记录事件。
在Javascript中,可以使用索引访问计数器:
...
counter.Increment({ which: counterId }, function (err, result) {
if (err) {
return error(err);
}
log("Counter " + result.args.which + " was incremented by address: "
+ result.args.who);
getCount();
});
...
事件的局限
事件构建在Ethereum中,底层的日志接口之上。虽然您通常不会直接处理日志消息,但是了解它们的限制非常重要。
日志结构最多有4个“主题”和一个“数据”字段。第一个主题用于存储事件签名的哈希值,这样就只剩下三个主题用于索引参数。主题需要32字节长,因此,如果使用数组作为索引参数(包括类型string和bytes),那么首先将哈希值转换为32字节。非索引参数存储在数据字段中,没有大小限制。
日志,包括记录在日志中的事件,不能从Ethereum虚拟机(EVM)中访问。这意味着合约不能读取自己的或其他合约的日志及事件。
总结
- Solidity 提供了一种记录交易期间事件的方法。
- 智能合约前端(DApp)可以监听这些事件。
- 索引(indexed)参数为过滤事件提供了一种高效的方法。
- 事件受其构建基础日志机制的限制。
(8)错误处理
Solidity 提供了很多错误检查和错误处理的方法。通常,检查是为了防止未经授权的代码访问,当发生错误时,状态会恢复到初始状态。
下面是错误处理中,使用的一些重要方法:
assert(bool condition)
− 如果不满足条件,此方法调用将导致一个无效的操作码,对状态所做的任何更改将被还原。这个方法是用来处理内部错误的。-
require(bool condition)
− 如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。 -
require(bool condition, string memory message)
− 如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。它提供了一个提供自定义消息的选项。 -
revert()
− 此方法将中止执行并将所做的更改还原为执行前状态。 -
revert(string memory reason)
− 此方法将中止执行并将所做的更改还原为执行前状态。它提供了一个提供自定义消息的选项。
异常处理:
Solidity使用状态恢复来处理异常,就是说当抛出异常时将恢复到调用(包括自调用)前的状态。
抛出异常的方式有assert,require,revert,throw。
- assert函数,用于条件检查,只能测试内部错误和检查常量。
function add(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
- require函数,也是用于条件检查,用于测试调用的输入或者合约状态变量。
function sendHalf(address addr) payable returns (uint balance) {
require(msg.value % 2 == 0); // 只允许偶数
.....
}
solidity常用模式
1,提款模式
当在智能合约中,直接向一个地址转账时,如该地址是一个合约地址,合约中可以编写代码,拒绝接受付款,导致交易失败。为避免这种情况,通常会使用提款模式。
提款模式是让收款方主动来提取款项,而不是直接转账给收款方。
提款模式,让收款方(前首富)主动来提取款项,交易不会失败,游戏可以继续。
2,限制访问
编程风格:
结构体名称
驼峰式命名,例如: SmartCoin
事件名称
驼峰式命名,例如:AfterTransfer
函数名
驼峰式命名,首字母小写,比如:initiateSupply
局部变量和状态变量
驼峰式命名,首字母小写,比如creatorAddress、supply
常量
大写字母单词用下划线分隔,例如:MAX_BLOCKS
修饰符的名字
驼峰式命名,首字母小写,例如:onlyAfter
枚举的名字
驼峰式命名,例如:TokenGroup