Solidity语言基础(1)
发布日期:2021-06-28 22:10:20 浏览次数:2 分类:技术文章

本文共 13376 字,大约阅读时间需要 44 分钟。

Solidity是一种智能合约高级语言,运行在Ethereum虚拟机(EVM)之上。
Solidity与其它语言相关的特点?
它的语法接近于Javascript,是一种面向对象的语言。但作为一种真正意义上运行在网络上的去中心合约,它又有很多的不同,下面列举一些:
以太坊底层是基于帐户,而非UTXO的,所以有一个特殊的Address的类型。用于定位用户,定位合约,定位合约的代码(合约本身也是一个帐户)
由于语言内嵌框架是支持支付的,所以提供了一些关键字,如payable,可以在语言层面直接支持支付,而且超级简单。
存储是使用网络上的区块链,数据的每一个状态都可以永久存储,所以需要确定变量使用内存,还是区块链。
运行环境是在去中心化的网络上,会比较强调合约或函数执行的调用的方式。因为原来一个简单的函数调用变为了一个网络上的节点中的代码执行,分布式的感觉。
最后一个非常大的不同则是它的异常机制,一旦出现异常,所有的执行都将会被回撤,这主要是为了保证合约执行的原子性,以避免中间状态出现的数据不一致。
Hello Wolrd!
听起来高大上,其实入手玩起来也可以很简单:
pragma solidity ^0.4.0;
contract HelloWorld{
    uint balance;
    function update(uint amount) returns (address, uint){
        balance += amount;
        return (msg.sender, balance);
    }
}
通过读取参数输入的新值,并将之累加至合约的变量中,返回发送人的地址,和最终的累计值。
1.Solidity智能合约文件结构
版本申明
pragma solidity ^0.4.0;
说明:
1 版本要高于0.4才可以编译
2 号表示高于0.5的版本则不可编译,第三位的版本号但可以变,留出来用做bug可以修复(如0.4.1的编译器有bug,可在0.4.2修复,现有合约不用改代码)。
引用其它源文件
全局引入 *
import “filename”;
自定义命名空间引入 *
import * as symbolName from “filename”
分别定义引入
import  {symbol1 as alias, symbol2} from “filename”
非es6兼容的简写语法
import “filename” as symbolName
等同于上述
import * as symbolName from “filename”
关于路径
引入文件路径时要注意,非.打头的路径会被认为是绝对路径,所以要引用同目录下的文件使用
import “./x” as x
也不要使用下述方式,这样会是在一个全局的目录下
import “x” as x;
为什么会有这个区别,是因为这取决于编译器,如果解析路径,通常来说目录层级结构并不与我们本地的文件一一对应,它非常有可能是通过ipfs,http,或git建立的一个网络上的虚拟目录。
编译器解析引用文件机制
各编译器提供了文件前缀映射机制。
1. 可以将一个域名下的文件映射到本地,从而从本地的某个文件中读取
2. 提供对同一实现的不同版本的支持(可能某版本的实现前后不兼容,需要区分)
3. 如果前缀相同,取最长,
4. 有一个”fallback-remapping”机制,空串会映射到“/usr/local/include/solidify”
solc编译器
命令行编译器,通过下述命令命名空间映射提供支持
context:prefix=target
上述的context:和=target是可选的。所有context目录下的以prefix开头的会被替换为target。
举例来说,如果你将github.com/ethereum/dapp-bin拷到本地的/usr/local/dapp-bin,并使用下述方式使用文件
import “github.com/ethereum/dapp-bin/library/iterable_mapping.sol” as it_mapping;
要编译这个文件,使用下述命令:
solc github.com/ethereum/dapp-bin=/usr/local/dapp-bin source.sol
另一个更复杂的例子,如果你使用一个更旧版本的dapp-bin,旧版本在/url/local/dapp-bin_old,那么,你可以使用下述命令编译
solc module1:github.com/ethereum/dapp-bin=/usr/local/dapp-bin  \
        modeule2:github.com/ethereum/dapp-bin=/usr/local/dapp-bin_old \
        source.sol
需要注意的是solc仅仅允许包含实际存在的文件。它必须存在于你重映射后目录里,或其子目录里。如果你想包含直接的绝对路径包含,那么可以将命名空间重映射为=\
备注:如果有多个重映射指向了同一个文件,那么取最长的那个文件。
browser-solidity编译器:
browser-solidity编译器默认会自动映射到github上,然后会自动从网络上检索文件。例如:你可以通过下述方式引入一个迭代包:
import “github.com/ethereum/dapp-bin/library/iterable_mapping.sol” as it_mapping
备注:未来可能会支持其它的源码方式
代码注释
两种方式,单行(//),多行使用(/*…*/)
文档注释
写文档用。三个斜杠///或/** … */,可使用Doxygen语法,以支持生成对文档的说明,参数验证的注解,或者是在用户调用这个函数时,弹出来的确认内容。
2.值类型与引用类型
每个合约中可包含状态变量(State Variables),函数(Functions),函数修饰符(Function Modifiers),事件(Events),结构类型(Structs Types)和枚举类型(Enum Types)。
值类型(Value Type)
值类型包含
布尔(Booleans)
整型(Integer)
地址(Address)
定长字节数组(fixed byte arrays)
有理数和整型(Rational and Integer Literals,String literals)
枚举类型(Enums)
函数(Function Types)
为什么会叫值类型,是因为上述这些类型在传值时,总是值传递。比如在函数传参数时,或进行变量赋值时。
引用类型(Reference Types)
复杂类型,占用空间较大的。在拷贝时占用空间较大。所以考虑通过引用传递。常见的引用类型有:
不定长字节数组(bytes)
字符串(string)
数组(Array)
结构体(Struts)
(1)bool: 可能的取值为常量值true和false.
(2)整型(Integer) int/uint:变长的有符号或无符号整型。变量支持的步长以8递增,支持从uint8到uint256,以及int8到int256。需要注意的是,uint和int默认代表的是uint256和int256。
(3)地址(Address)
以太坊地址的长度,大小20个字节,160位,所以可以用一个uint160编码。地址是所有合约的基础,所有的合约都会继承地址对象,也可以随时将一个地址串,得到对应的代码进行调用。当然地址代表一个普通帐户时,就没有这么多丰富的功能啦。
支持的运算符
<=,<,==,!=,>=和>
地址类型的成员
属性:balance
函数:send(),call(),delegatecall(),callcode()。
地址字面量
十六进制的字符串,凡是能通过地址合法性检查(address checksum test),就会被认为是地址,如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF。需要注意的是39到41位长的没有通过地址合法性检查的,会提示一个警告,但会被视为普通的有理数字面量。
balance
通过它能得到一个地址的余额。
pragma solidity ^0.4.0;
contract addressTest{    
    function getBalance(address addr) returns (uint){
        return addr.balance;
    }
}
this
如果只是想得到当前合约的余额,其实可以这样写:
原因是对于合约来说,地址代表的就是合约本身,合约对象默认继承自地址对象,所以内部有地址的属性。
地址的方法send()
用来向某个地址发送货币(货币单位是wei)。
pragma solidity ^0.4.0;
//请注意这个仅是Demo,请不要用到正式环境
contract PayTest {
    //得到当前合约的余额
    function getBalance() returns (uint) {
        return this.balance;//0
    }     
    //向当前合约存款
    function deposit() payable returns(address addr, uint amount, bool success){
        //msg.sender 全局变量,调用合约的发起方
        //msg.value 全局变量,调用合约的发起方转发的货币量,以wei为单位。
        //send() 执行的结果
        return (msg.sender, msg.value, this.send(msg.value));
    }
}
 
这个合约实现的是充值。this.send(msg.value)意指向合约自身发送msg.value量的以太币。msg.value是合约调用方附带的以太币。
send()方法执行时有一些风险
调用递归深度不能超1024。
如果gas不够,执行会失败。
所以使用这个方法要检查成功与否。或为保险起见,货币操作时要使用一些最佳实践。
如果执行失败,将会回撤所有交易,所以务必留意返回结果。
call(),callcode()和delegatecall()
为了同一些不支持ABI协议的进行直接交互(一般的web3.js,soldity都是支持的)。可以使用call()函数,用来向另一个合约发送原始数据。参数支持任何类型任意数量。每个参数会按规则(规则是按ABI4)打包成32字节并一一拼接到一起。
call()方法支持ABI协议4定义的函数选择器。如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名4。所以如果你只是想发送消息体,需要避免第一个参数是4个字节。
call方法返回一个bool值,以表明执行成功还是失败。正常结束返回true,异常终止返回false。我们无法解析返回结果,因为这样我们得事前知道返回的数据的编码和数据大小(这里的潜在假设是不知道对方使用的协议格式,所以也不会知道返回的结果如何解析,有点祼协议测试的感觉)。
同样我们也可以使用delegatecall(),它与call方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。delegatecall()方法的目的是用来执行另一个合约中的工具库。所以开发者需要保证两个合约中的存储变量能兼容,来保证delegatecall()能顺利执行。
在homestead阶段之前,仅有一个受限的多样的callcode()方法可用,但并未提供对msg.sender,msg.value的访问权限。
上面的这三个方法call(),delegatecall(),callcode()都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了Solidity的类型安全。
上述的函数都是底层的函数,使用时要异常小心。当调用一个未知的,可能是恶意的合约时,当你把控制权交给它,它可能回调回你的合约,所以要准备好在调用返回时,应对你的状态变量可能被恶意篡改的情况。
(4)字节数组(byte arrays)
定长字节数组(Fixed-size byte arrays)
bytes1, ... ,bytes32,允许值以步长1递增。byte默认表示byte1。成员变量
.length表示这个字节数组的长度(只读)。
动态大小的字节数组
bytes: 动态长度的字节数组,参见数组(Arrays)。非值类型1。
string: 动态长度的UTF-8编码的字符类型,参见数组(Arrays)。非值类型1。
一个好的使用原则是:
bytes用来存储任意长度的字节数据,string用来存储任意长度的UTF-8编码的字符串数据。
如果长度可以确定,尽量使用定长的如byte1到byte32中的一个,因为这样更省空间。
(5)十六进制字面量
十六进制字面量,以关键字hex打头,后面紧跟用单或双引号包裹的字符串。如hex"001122ff"。在内部会被表示为二进制流。通过下面的例子来理解下是什么意思:
pragma solidity ^0.4.0;
contract HexLiteral{
    function test() returns (string){
      var a = hex"001122FF";
      //var b = hex"A";
      //Expected primary expression
      return a;
  }
}
由于一个字节是8位,所以一个hex是由两个[0-9a-z]字符组成的。所以var b = hex"A";不是成双的字符串是会报错的。
转换
十六进制的字面量与字符串可以进行同样的类似操作:
pragma solidity ^0.4.0;
contract HexLiteralBytes{
    function test() returns (bytes4, bytes1, bytes1, bytes1, bytes1){
      bytes4 a = hex"001122FF";
      return (a, a[0], a[1], a[2], a[3]);
  }
}
可以发现,它可以隐式的转为bytes,上述代码的执行结果如下:
Result: "0x001122ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000ff00000000000000000000000000000000000000000000000000000000000000"
Transaction cost: 21857 gas. 
Execution cost: 585 gas.
Decoded: 
bytes4: 0x001122ff
bytes1: 0x00
bytes1: 0x11
bytes1: 0x22
bytes1: 0xff
(6)数据位置(Data location)
复杂类型,如数组(arrays)和数据结构(struct)在Solidity中有一个额外的属性,数据的存储位置。可选为memory和storage存储(storage,值类型中的状态变量)。
memory存储位置同我们普通程序的内存一致。即分配,即使用,越过作用域即不可被访问,等待被回收。而在区块链上,由于底层实现了图灵完备,故而会有非常多的状态需要永久记录下来。比如,参与众筹的所有参与者。那么我们就要使用storage这种类型了,一旦使用这个类型,数据将永远存在。
基于程序的上下文,大多数时候这样的选择是默认的,我们可以通过指定关键字storage和memory修改它。
默认的函数参数,包括返回的参数,他们是memory。默认的局部变量是storage的。而默认的状态变量(合约声明的公有变量)是storage。
另外还有第三个存储位置calldata。它存储的是函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多。
数据位置指定非常重要,因为不同数据位置变量赋值产生的结果也不同。在memory和storage之间,以及它们和状态变量(即便从另一个状态变量)中相互赋值,总是会创建一个完全不相关的拷贝。
将一个storage的状态变量,赋值给一个storage的局部变量,是通过引用传递。所以对于局部变量的修改,同时修改关联的状态变量。但另一方面,将一个memory的引用类型赋值给另一个memory的引用,不会创建另一个拷贝。
pragma solidity ^0.4.0;
contract DataLocation{
  uint valueType;
  mapping(uint => uint) public refrenceType;
  function changeMemory(){
    var tmp = valueType;
    tmp = 100;
  }
  function changeStorage(){
    var tmp = refrenceType;
    tmp[1] = 100;
  }
  function getAll() returns (uint, uint){
    return (valueType, refrenceType[1]);
  }
}
下面来看下官方的例子说明:
pragma solidity ^0.4.0;
contract C {
    uint[] x; // the data location of x is storage
    // the data location of memoryArray is memory
    function f(uint[] memoryArray) {
        x = memoryArray; // works, copies the whole array to storage
        var y = x; // works, assigns a pointer, data location of y is storage
        y[7]; // fine, returns the 8th element
        y.length = 2; // fine, modifies x through y
        delete x; // fine, clears the array, also modifies y
        // The following does not work; it would need to create a new temporary /
        // unnamed array in storage, but storage is "statically" allocated:
        // y = memoryArray;
        // This does not work either, since it would "reset" the pointer, but there
        // is no sensible location it could point to.
        // delete y;
        g(x); // calls g, handing over a reference to x
        h(x); // calls h and creates an independent, temporary copy in memory
    }
    function g(uint[] storage storageArray) internal {}
    function h(uint[] memoryArray) {}
}
强制的数据位置(Forced data location)
外部函数(External function)的参数(不包括返回参数)强制为:calldata
状态变量(State variables)强制为: storage
默认数据位置(Default data location)
函数参数(括返回参数:memory
所有其它的局部变量:storage
(7)数组
数组可以声明时指定长度,或者是变长的。对storage的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,数据结构等。但对于memory的数组来说。如果函数是对外可见的,那么函数参数不能是映射类型的数组,只能是支持ABI的类型3。
一个类型为T,长度为k的数组,可以声明为T[k],而一个变长的数组则声明为T[]。
你还可以声明一个多维数据,如一个类型为uint的数组长度为5的变长数组,可以声明为uint[][5] x。需要留心的是,相比非区块链语言,多维数组的长度声明是反的。
要访问第三个动态数据的,第二个元素,使用x[2][1]。数组的序号是从0开始的,序号顺序与定义相反。
bytes和string是一种特殊的数组。bytes类似byte[],但在外部函数作为参数调用中,会进行压缩打包,更省空间,所以应该尽量使用bytes4。string类似bytes,但不提供长度和按序号的访问方式。
由于bytes与string,可以自由转换,你可以将字符串s通过bytes(s)转为一个bytes。但需要注意的是通过这种方式访问到的是UTF-8编码的码流,并不是独立的一个个字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。
类型为数组的状态变量,可以标记为public类型,从而让Solidity创建一个访问器,如果要访问数组的某个元素,指定数字下标就好了。
创建一个数组
可使用new关键字创建一个memory的数组。与stroage数组不同的是,你不能通过.length的长度来修改数组大小属性。我们来看看下面的例子:
pragma solidity ^0.4.0;
contract C {
    function f() {
        //创建一个memory的数组
        uint[] memory a = new uint[](7);
        
        //不能修改长度
        //Error: Expression has to be an lvalue.
        //a.length = 100;
    }  
    //storage
    uint[] b;    
    function g(){
        b = new uint[](7);
        //可以修改storage的数组
        b.length = 10;
        b[9] = 100;
    }
}
在上面的代码中,f()方法尝试调整数组a的长度,编译器报错Error: Expression has to be an lvalue.。但在g()方法中我们看到可以修改5。
字面量及内联数组
数组字面量,是指以表达式方式隐式声明一个数组,并作为一个数组变量使用的方式。下面是一个简单的例子:
pragma solidity ^0.4.0;
contract C {
    function f() {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) {
        // ...
    }
}
通过数组字面量,创建的数组是memory的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如代码里的[1, 2, 3],只需要uint8即可存储。由于g()方法的参数需要的是uint(默认的uint表示的其实是uint256),所以要使用uint(1)来进行类型转换。
还需注意的一点是,定长数组,不能与变长数组相互赋值,我们来看下面的代码:
pragma solidity ^0.4.0;
contract C {
    function f() {
        // The next line creates a type error because uint[3] memory
        // cannot be converted to uint[] memory.
        uint[] x = [uint(1), 3, 4];
}
限制的主要原因是,ABI不能很好的支持数组,已经计划在未来移除这样的限制。(当前的ABI接口,不是已经能支持数组了?)
数组的属性和方法
length属性
数组有一个.length属性,表示当前的数组长度。storage的变长数组,可以通过给.length赋值调整数组长度。memory的变长数组不支持。
不能通过访问超出当前数组的长度的方式,来自动实现上面说的这种情况。memory数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整,对于变长数组,可以通过参数在编译期指定数组大小。
push方法
storage的变长数组和bytes都有一个push(),用于附加新元素到数据末端,返回值为新的长度。
pragma solidity ^0.4.0;
contract C {
    uint[] u;
    bytes b;
    function testArryPush() returns (uint){
        uint[3] memory a = [uint(1), 2, 3];    
        u = a; 
        return u.push(4);
    }
    
    function testBytesPush() returns (uint){
        b = new bytes(3);
        return b.push(4);
    }
}
限制的情况
当前在外部函数中,不能使用多维数组。
另外,基于EVM的限制,不能通过外部函数返回动态的内容。
pragma solidity ^0.4.0;
contract C { 
    function f() returns (uint[]) { 
    }
}
在上面的例子中,通过web.js调用能返回数据,但在Solidity中不能返回数据。一种临时的解决办法,是使用一个非常大的静态数组。
pragma solidity ^0.4.0;
contract ArrayContract {
    //the orginal length of m_aLotOfIntegers is 2**20
    //run it cause a out of gas,so change it to a much smaller 2**2 for test
    uint[2**2] m_aLotOfIntegers;
    // Note that the following is not a pair of arrays but an array of pairs.
    bool[2][] m_pairsOfFlags;
    // newPairs is stored in memory - the default for function arguments
    function setAllFlagPairs(bool[2][] newPairs) {
        // assignment to a storage array replaces the complete array
        m_pairsOfFlags = newPairs;
    }
    function setFlagPair(uint index, bool flagA, bool flagB) {
        // access to a non-existing index will throw an exception
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }
    function changeFlagArraySize(uint newSize) {
        // if the new size is smaller, removed array elements will be cleared
        m_pairsOfFlags.length = newSize;
    }
    function clear() {
        // these clear the arrays completely
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // identical effect here
        m_pairsOfFlags.length = 0;
    }
    function addFlag(bool[2] flag) returns (uint) {
        return m_pairsOfFlags.push(flag);
    }
    function createMemoryArray(uint size)  {
        // Dynamic memory arrays are created using `new`:
        bool[2][] memory arrayOfPairs = new bool[2][](size);
        m_pairsOfFlags = arrayOfPairs;
    }
}
(8)结构体(struct)
Solidity提供struct来定义自定义类型。我们来看看下面的例子:
pragma solidity ^0.4.0;
contract CrowdFunding{
    struct Funder{
        address addr;
        uint amount;
    }
    
    struct Campaign{
        address beneficiary;
        uint goal;
        uint amount;
        uint funderNum;
        mapping(uint => Funder) funders;
    }
    
    uint compaingnID;
    mapping (uint => Campaign) campaigns;
    
    function candidate(address beneficiary, uint goal) returns (uint compaingnID){
        // initialize
        campaigns[compaingnID++] = Campaign(beneficiary, goal, 0, 0);
    }
    
    function vote(uint compaingnID) payable {
        Campaign c = campaigns[compaingnID];
        
        //another way to initialize
        c.funders[c.funderNum++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }
    
    function check(uint comapingnId) returns (bool){
        Campaign c = campaigns[comapingnId];
        
        if(c.amount < c.goal){
            return false;
        }
        
        uint amount = c.amount;
        // incase send much more
        c.amount = 0;
        if(!c.beneficiary.send(amount)){
            throw;
        }
        return true;
    }
}
上面的代码向我们展示的一个简化版的众筹项目,其实包含了一些struct的使用。struct可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。
我们不能声明一个struct同时将这个struct作为这个struct的一个成员。这个限制是基于结构体的大小必须是有限的。
虽然数据结构能作为一个mapping的值,但数据类型不能包含它自身类型的成员,因为数据结构的大小必须是有限的。
需要注意的是在函数中,将一个struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。
当然,你也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如campaigns[comapingnId].amount = 0

转载地址:https://blog.csdn.net/yhc166188/article/details/79654215 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:union介绍
下一篇:javascript基础1

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月08日 01时53分34秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章