Solidity学习笔记

  • 一、基本介绍

    Solidity是一门静态类型语言,支持继承、库和复杂的用户自定义类型等特性。

    二、基础语法

    1. 地址(address)

    • 以太坊中的地址为160位,即20个字节大小,所以可以用一个uint160表示。
    • eip-55:有关验证地址的合法性
    • 在合约内部,this表示当前合约本身,由于合约也是一个address类型(contract是从address继承的),所以this其实也是address类型的值。

    2. 类型转换

    (1) 隐式转换

    (1)无符号整数(uint)可以转换为相同或更大尺寸字节类型(bytes),但是反过来不可以转换。
    (2)任何可以转换为uint160类型的变量都可以转换为地址类型(address)

    pragma solidity ^0.4.17;
    
    contract convert{
        int8  a = 1;
        int16  b = 2;
        // 返回的类型必须是int16,如果返回int8类型会报错,这边运算符已经帮我们隐式转换了类型
        function test() public view returns(int16)
        {
        // a+b 的结果会隐式的转换为int16
            return a + b;
        }
    }
    

    (2)显式转换

    int8  public  c = -2;
    uint8  public  d = uint8(c);
    // 此时的d为254
    uint  public e = uint(c);
    // e = 115792089237316195423570985008687907853269984665640564039457584007913129639934

    3. 发送以太币的两种方法

    (1)transfer

    transfer从合约发起方向某个地址转入以太币,当地址无效或者发起方余额不足时,transfer将抛出异常

    // 向addressA转入一个以太币
    addressA.transfer(1 ether);
    
    // or // 附带 gas
    addressA.transfer.gas(120000)(1 ether);
    

    (2)send

    send是transfer的低级版本。当合约执行失败时,send会返回false。当转账成功,则返回true。

    owner.send(SOME_BALANCE); // 失败时返回false
    
    if(owner.send(SOME_BALANCE)){
        ...
    }
    

    使用send时需注意以下三点:

    • (1)send方法需包裹在if中,因为在调用send函数时,合约可能会有函数执行,这些函数可能会执行失败
    • (2)在用send方法发送以太币之前,请先执行减少账户余额的操作,因为可能会有递归调用消耗完合约余额的风险。
    • (3)用户可以重载send方法。

    总结:
    x.transfer(y)等价于if(!x.send(y)) throw; ,send是transfer的底层实现,建议尽可能使用transfer。

    4. 字节数组

    (1)固定长字节数组

    固定长度字节数组是以bytes加上数字后缀的方式定义的。

    byte a;  // byte 等同于bytes1 a
    bytes2 b;
     ...
    bytes32 c;
    

    索引访问:bytes1~bytes32 支持索引访问,但是这种索引访问是只读的不能使用进行赋值

    bytes10  b
    b[0]   // 获取第1个字节
    b[1]   // 获取第2个字节
    ...
    b[9]   // 获取第9个字节
    
    b[0] = x  // 不能使用索引的形式进行赋值,因为这种索引访问是只读的。
    

    可以将 byte[] 当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时每个元素会浪费 31 字节。 更好地做法是使用 bytes

    (2)动态长度字节数组

    bytes:动态长度字节数组

    一种特殊的数组。bytes类似于byte[],在外部函数作为参数时,会进行压缩打包以便节省空间,所以尽量用bytes。

    bytes m;
    

    string:动态长度字符数串

    • (1)字符串以UTF-8的形式编码
    • (2)类似bytes,但不提供长度和按索引的访问方式。
    // 字符串是双引号
    string n = "hello";
    
    // 不支持长度访问
    n.length
    
    // 不支持按索引访问
    n[0]
    
    • (1) string不支持通过索引访问,但可以通过string类型的值转换为bytes类型的值,然后就可以使用索引访问字符串的特定字节。
    • (2) 由于bytes类型的值是可读写的,所以要修改string类型的值,可以先将string类型的值转换为bytes类型的值,修改完后,再将bytes类型的值转换为string类型的值。
    value2 = typename ( value1 );  //类型转换
    

    其中typename表示类型名,如string、bytes等。

    string类型有一些缺陷:如,不能直接使用加号(+)进行连接,但可以通过bytes类型间接将两个或多个字符串连接起来。

    字符串连接的基本原理

    • (1)创建一个尺寸与所有参与连接的字符串尺寸之和相同的大字符串;
    • (2)将该大字符串转换为bytes类型;
    • (3)依次迭代参与连接的字符串,将字符串中的字符逐一添加到bytes类型的值中;
    • (4)将这个bytes类型的值转换为string类型的值。
        // internal表示函数只能被合约内部调用,函数在合约外不可见
        function strConcat(string memory _str1,string memory _str2) internal pure returns(string memory) {
            // 先将string转化为bytes类型的值
            bytes memory _bytesValue1 = bytes(_str1);
            bytes memory _bytesValue2 = bytes(_str2);
            // 创建一个能容纳_str1和_str2的string对象
            string memory resultStr = new string(_bytesValue1.length + _bytesValue2.length);
            // 创建与_str1和_str2总和同样尺寸的bytes对象
            bytes memory resultBytes = bytes(resultStr);
    
            uint index = 0;
    
            for(uint i=0;i<_bytesValue1.length;i++){
                resultBytes[index++] = _bytesValue1[i];
            }
    
            for(uint i=0;i<_bytesValue2.length;i++){
                resultBytes[index++] = _bytesValue2[i];
            }
    
            return string(resultBytes);
        }
    

    5. 枚举类型(enum)

    枚举是Solidity中的自定义数据类型。枚举可以显式转为整型,但是不能与整型隐式转换,枚举在一般程序中可以当作状态机使用。

    // 定义枚举类型(类型名为enumName)
    enum enumName{ value1, value2, ... , valueN}
    

    例如:

     // 定义一个枚举类型名为Country的枚举类型,每一个枚举值都对应一个整数索引,China表示0,America表示1,以此类推。(不用添加分号)
        enum Country {China,America,Japan,Australia,Canada,South_Korea}
        
        // 定义枚举Country类型的变量
        Country country;
        
        // 赋值
        country = Country.China;   //使用枚举值进行赋值
        // or
        country = Country(0);   //使用整数值进行赋值(必须显式类型转换)
        
        // 枚举可以显式转化为整型
        uint currentCountry = uint(Country.China);  // 0
    

    注意:

    • (1)在合约中可以使用枚举值设置枚举变量,也可以使用整数值设置枚举变量,后者必须显式类型转换
    • (2)在remix环境中测试智能合约时,再输入测试数据时不能直接输入Country.China或其他枚举值,而需要输入整数,如0,1,2,3等。

    6. 函数类型

    • (1)函数声明的类型称为函数类型。(注意:强调的是类型,类似我们常见的uint,string类型)
    • (2)函数类型可以作为函数参数类型和函数返回值类型。
    • (3)只要函数的参数个数参数类型函数返回值函数类型一样,该函数就可以赋给一个函数类型变量
    //func是一个函数类型变量
    function (uint,uint) returns(uint) func; 
    function add(uint x, uint y) public returns(uint){
    return x+y;
    }
    function test() public{
    // 将add函数赋给函数类型变量func
    func = add;
    }
    

    7. 函数和状态变量的4种可见性(访问权限)

    (1)访问权限

    (1) 如果不为函数指定访问权限,默认为public
    (2) 合约的状态变量不能用external修饰,否则无法编译通过。
    (3) 如果不为状态变量指定访问权限,默认为internal

    4类可见性(访问权限) 指定:

    • public:智能合约外部和内部都可使用的方法;

    用public声明的状态变量,会自动产生一个getter函数

    • internal智能合约(包括派生合约,子合约)内部才可调用的方法;internal修饰的状态变量只能通过在内部的方式访问;
    • external:可通过其他合约和交易进行调用的方法;

    (1)external函数不能直接调用,前面需要加this, 如this.func( )。
    (2)在接收大量数据时,external函数有时更有效率。

    • private:只有在定义的合约中才可以调用,即使派生的合约也无法调用;用private修饰的状态变量也是如此,只在当前合约中可以访问(子合约也不能访问)。

    (2)getter函数

    如果合约的状态变量使用public修饰,Solidity编译器会自动为状态变量生成一个与状态变量同名getter函数用于获取状态变量的值

    • (1)如果状态变量的数据类型比较复杂,如mapping,生成的getter函数会带一些参数。
    • (2)尽管自动生成的getter函数使用external修饰,但是不能在合约内使用this调用。(尽管不会产生编译错误,但是无法成功部署合约)
    • (3)在合约内部,getter函数会解析为状态变量。

    例如,如果状态变量persons是mapping类型,在合约内部应该使用persons[key], 而不是this.persons(key)形式。
    不过在合约外部必须使用getter函数形式引用persons。(如:my.persons(key), my为创建的合约对象名,persons(key)即为状态变量persons的getter函数形式)

    pragma solidity >=0.4.20 <=0.7.0;
    
    contract MyContract{
        uint public data = 115;
        string public personName;
        uint public personAge;
        struct Person{
            string name;
            uint age;
        }
        mapping(uint=>Person) public persons;
    
        constructor() public{
            data = 200;
            // 创建Person结构体的实例
            Person memory person = Person({
                name:"Lebron James",
                age:34
            });
            // 将person添加到persons映射中
            persons[10] = person;
            // 在合约内部不能使用persons的getter函数形式引用persons映射,
            // 所以尽管下面的代码编译不会出错,但无法成功部署在以太坊上。
            // (string memory name,uint age) = this.persons(10);
            
            string memory name = persons[10].name;
            uint age = persons[10].age;
            personName = name;
            personAge = age;
        }
    }
    
    contract GetterContract{
        MyContract my = new MyContract();
        function getData() public view returns(uint){
            // 调用MyContract合约中的data状态变量对应的getter函数(data函数)
            return my.data();
        }
    
        function getPerson(uint id) public view returns(string memory,uint){
            // 调用MyContract合约中persons状态变量对应的getter函数(persons函数)
            // 该函数返回了多个值,这些值都是Person结构体的成员,
            // 如果这个结构体的某个成员的数据类型无法通过函数返回(如mapping),那么系统就会忽略这个结构体成员。
            (string memory name,uint age) = my.persons(id);
            return (name,age);
        }
    }
    

    8. 数组(稍有不同)

    (1)不同之处

    solidity中数组与大多数语言稍有不同。具体如下:

    // 定义一个j行i列的二维数组(注意:定义的时候列在前面,行在后面)
    int[i][j]  arrayName;
    // 为数组arrayName的第m行第n列元素赋值,赋值为20.( 注意:赋值的时候,行在前面,列在后面)
    arrayName[m][n] = 20;
    
    • (1)定义的时候列在前面,行在后面
    • (2)赋值的时候,行在前面,列在后面

    (2)注意事项(一)

    • 对于storage数组,可以保存任意类型的数据,包括另一个数组、映射或结构体。
    • 但对于memory数组,不能存储映射类型的数据
    • 如果作为 public 函数的参数,它只能是 ABI 类型

    (3)注意事项(二)

    • (1) 如果你在一个空数组中使用.length,这将会造成向下溢出(不减小反而增大),导致长度变为2^256-1。
    • (2) 增加一个storage数组的长度花费的gas成本是一个常量值,因为storage变量被当作是zero-initialised(领初始化)的;
      而减少storage数组的长度花费的gas成本至少是线性增长的(但事实上,大多数都要高于线性增长),因为其包含了显式清除要被删除的元素(类似于调用delete方法)。
    • (3) 外部函数中暂时还不支持使用多维数组(但在public函数是支持的)。

    (4)数组成员

    length

    数组的成员变量length表示当前数组的长度。

    • (1)动态数组可以在storage中通过改变成员变量 .length 改变数组大小(在memory中是不可以的)。
    • (2)并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度
    • (3)一经创建,memory数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。
    • (4)如果你尝试改变一个不在storage中的非动态数组的大小,你将会收到一个“Value must be an Ivalue”的错误。

    push

    storage的动态数组以及 bytes类型(字节数组)都有一个叫做 push 的成员函数,它用来添加新的元素到数组末尾。 这个函数将返回新的数组长度

    注意:string即字节数组是没有push方法的

    pop

    storage的动态数组和bytes数组(字节数组)都有一个叫做pop的成员函数,用于从数组的末尾删除元素。
    其在删除元素的时候隐式地调用了delete方法。

    注意:string即字节数组是没有pop方法的

    (5)实例

    pragma solidity >=0.4.16 <0.7.0;
    
    contract ArrayContract {
        uint[2**20] m_aLotOfIntegers; // 数组大小为2的20次方
        // m_pairsOfFlags不是一对动态数组,而是一个数组元素为两个变量的动态数组(说白了就是其每个元素是一个长度为2的数组)
        bool[2][] m_pairsOfFlags; // 列数为2,行数为动态的
    
        // newPairs是一个数组元素为两个bool类型变量的动态数组(其每个元素是一个包含两个bool变量的数组)
        function setAllFlagPairs(bool[2][] memory newPairs) public {
            // 将newPairs数组赋值给storage数组的m_pairsOfFlags,m_pairsOfFlags的值将会被newPairs中的值替换。
            m_pairsOfFlags = newPairs;
        }
    
        struct StructType {
            uint[] contents;
            uint moreInfo;
        }
        StructType s;
    
        function f(uint[] memory c) public {
            // 将类型为StructType结构体变量s的指针(引用)赋值给g
            StructType storage g = s;
            // 改变结构体变量g中的成员属性值,其实也在改变s中的成员属性值(因为s和g指向同一块数据区域)
            g.moreInfo = 2;
            // 将c的值赋值给g.contents(虽然g.contents不是一个局部变量,但它是某个局部变量的一个成员)
            g.contents = c;
        }
    
        function setFlagPair(uint index, bool flagA, bool flagB) public {
            // 访问一个不存在数组下标会抛异常
            m_pairsOfFlags[index][0] = flagA; // 将flagA赋值给第index行第0列的元素
            m_pairsOfFlags[index][1] = flagB; // 将flagB赋值给第index行第1列的元素
        }
    
        function changeFlagArraySize(uint newSize) public {
            // 如果所赋给的新长度值小于原数组长度值,则会把原数组在新长度之外的元素删除。
            m_pairsOfFlags.length = newSize;
        }
    
        function clear() public {
            // 将数组清空
            delete m_pairsOfFlags;
            delete m_aLotOfIntegers;
            // 与上面效果相同(清空数组)
            m_pairsOfFlags.length = 0;
        }
    
        bytes m_byteData;
    
        function byteArrays(bytes memory data) public {
            // 字节数组(bytes)是不一样的,因为它们不是填充式存储,但是它们可以被当作和uint8[]一样对待。
            m_byteData = data;
            m_byteData.length += 7;
            m_byteData[3] = 0x08;
            delete m_byteData[2];
        }
    
        function addFlag(bool[2] memory flag) public returns (uint) {
            return m_pairsOfFlags.push(flag); // 向二维动态数组添加新元素(这里添加的元素是一个长度为2的数组),给二维数组增加一行
        }
    
        function createMemoryArray(uint size) public pure returns (bytes memory) {
            // 使用new关键字进行动态数组的创建
            uint[2][] memory arrayOfPairs = new uint[2][](size);
    
            // 内联数组总是静态大小的,如果你只是使用字面量,则你必须提供至少一种类型。
            arrayOfPairs[0] = [uint(1), 2];
    
            // 创建一个动态数组
            bytes memory b = new bytes(200);
            for (uint i = 0; i < b.length; i++)
                b[i] = byte(uint8(i));
            return b;
        }
    }
    

    参考:Array

    9. 结构体(struct)

    (1)定义

    结构体用于自定义数据类型,结构体成员可以是任何数据类型,甚至可以是结构体本身。

    • (1)结构体可用于函数返回值,但是要在智能合约内部调用,否则会抛出异常。
    • (2)如果要返回结构体中成员的值,可以使用返回多个值的函数

    (2)实例

    pragma solidity >=0.4.16 <= 0.7.0;
    
    contract StructContract_1{
        // 定义结构体类型
        struct Job{
            uint id;
            string name;
            string company;
        }
    
        struct Person{
            uint id;
            string name;
            uint age;
            Job job; // 结构体类型中引用结构体变量(结构体变量作为结构体类型的成员)
        }
        // Job public job;
        // 声明一个Person类型的变量
        Person  person;
        
        // 初始化结构体 
        // 方法一:按照结构体中命名参数进行初始化
        Person personA = Person({
            id:10002,
            name:"Kobe Bryant",
            age:39,
            job:Job({   //结构体中包含结构体
                id:102,
                name:"Basketball Player",
                company:"NBA"
            })
        });
        // 方法二:按照结构体中定义的顺序初始化
        Job jobA = Job(103,"NBA Retired Players","Home");
        Person personB = Person(10003,"Dwyane Wade",36,Job(104,"LiNing Spokeman","LiNing"));
        Person personC = Person(10004,"Chris Bosh",35,jobA);
        //通过构造函数初始化结构体类型变量
        constructor (uint personId,string memory name,uint age) public{
            // 初始化结构体变量
            Job memory job = Job({
                id:101,
                name:"Software Engineer",
                company:"Google"
            });
    
           person = Person({
                id:personId,
                name:name,
                age:age,
                job:job
            });
        }
        // 修改工作属性(修改结构体变量的值)
        function setJob(string memory jobName,string memory company) public{
            // job.name = jobName;
            // job.company = company;
            person.job.name = jobName;
            person.job.company = company;
            // 重置为初始值,把struct中的所有变量的值设置为0,除了mapping类型
            // delete person; //也须写在函数内部
        }
    
        // 要用结构体当作返回值,必须将函数定义为internal,即合约内部可见(函数仅在合约内部可调用)
        // 必须在内部调用(需要使用internal声明函数),否则会抛出异常
        function getPerson() internal view returns(Person memory){
            return person; // 返回构造体类型的值
        }
        
        // 获取人员的姓名、年龄、工作等信息(获取结构体的成员值)
        function callGetPerson() public  returns(string memory,uint,string memory,string memory){
            person = getPerson();
            return (person.name,person.age,person.job.name,person.job.company);
        }
    }
    // 1,"Lebron James",34    "BasketBall Player","NBA"
    

    10. 映射(mapping)

    (1)定义

    映射与字典类似,通过key获取对应的value值

    • key:可以是除了映射外的任何数据类型;
    • value:任何数据类型;
    mapping(keyType=>valueType) varName;
    

    (2)实例

    pragma solidity >=0.4.16 <=0.7.0;
    
    contract MappingContract{
        //声明映射类型的变量names
        mapping(uint=>string) public names;
        // 定义Person结构体类型
        struct Person{
            string name;
            uint age;
            string job;
        }
        //声明映射类型的变量persons
        mapping(uint=>Person) public persons;
        // 通过合约的构造函数向映射变量names添加值
        constructor (uint id,string memory name) public{
            names[id] = name; //映射变量的赋值
        }
        // 根据key值从映射类型变量中获取相应的value值
        function getValue(uint id) public view returns(string memory){
            return names[id];
        }
        // 向映射类型变量中添加值
        function addPerson(uint id,string memory name,uint age,string memory job) public{
            // 先初始化结构体
            Person memory person = Person({
                name:name,
                age:age,
                job:job
            });
    
            persons[id] = person; //增加一个person(向映射类型变量中添加值)
        }
        // 根据id(key)从persons映射获取Person对象,并通过返回多值函数返回Person结构体的成员
        
        function getPerson(uint id) public view returns(string memory name,uint age,string memory job){
            // 返回多个值
            // 方法一:多返回值函数可以通过定义具体的函数返回值接收多个返回值,而不使用return关键字
            name = persons[id].name;
            age = persons[id].age;
            job = persons[id].job;
            // 方法二:使用return关键字(多个返回值,需用括号括起来)
            // return (persons[id].name,persons[id].age,persons[id].job);
        }
    }
    // 测试数据
    // 1001,"Lebron James"
    // 1002,"Dwyane Wade",36,"NBA Player"
    // 1003,"Kobe Bryant",39,"World Cup Spokeman"
    

    实例中有提到两种不同的方式返回多个值

    11.函数参数和函数返回值

    (1)函数参数

    在函数中,如果某个参数未使用只需保留参数类型,参数名可以省略。

    (2)函数返回值

    函数返回值可以直接指定返回值类型,也可以为返回值指定变量名,声明返回值类型的方式与声明函数参数的方式相同,所以也可以将函数返回值称为函数输出和参数

    • 返回值类型要使用returns指定,多个返回值类型中间用逗号( , )分隔;
    • 如果为函数返回值指定变量名可以不使用return返回,直接为函数输出参数变量赋值即可。

    返回多个值的两种方法

    • 方法一: 函数可以通过设置多个具体的函数返回值变量接收多个返回值,而不使用return关键字,就可实现多个值的返回。
     function getPerson(uint id) public view returns(string memory name,uint age,string memory job){
            name = persons[id].name;
            age = persons[id].age;
            job = persons[id].job;
        }
    }
    
    • 方法二: 使用return关键字(多个返回值,需用括号括起来)
     function getPerson(uint id) public view returns(string memory, uint ,string memory){
            return (persons[id].name,persons[id].age,persons[id].job);
        }
    }
    

    12. 调用其他合约中的函数

    (1)定义

    当前合约中的函数调用其他合约中的函数的两个前提条件:

    • (1)被调用函数所在的合约必须已经成功部署在以太坊网络上(或在本地的测试环境)。
    • (2)需要知道被调用函数所在的合约的地址

    (2)实例

    // CallOtherContract.sol
    pragma solidity >=0.4.16 <=0.7.0;
    
    /**
    注意:
     (1)在部署FunCallContract之前,必须先部署FactorialContract合约,否则就无法获得FactorialContract的地址。
     (2)部署完FactorialContract合约之后,将FactorialContract合约的地址作为FunCallContract合约的构造参数
           传入FunCallContract合约,然后部署FunCallContract合约。
     */
    
    // 用于计算阶乘的合约
    contract FactorialContract{
        // 计算阶乘的函数
        function getFactorial(uint n) public returns(uint){
            if(n==0 || n==1){
                return 1;
            }
            else{
                return getFactorial(n-1)*n;
            }
        }
    }
    // 调用FactorialContract.getFactorial函数计算阶乘
    contract FunCallContract{
        FactorialContract factorial;
        //在构造函数中创建FactorialContract合约的实例,
        // 必须通过FunCallContract构造函数的参数指定FactorialContract合约的地址。
        constructor(address addr) public{
            factorial = FactorialContract(addr);//实例化合约实例的时候需要传入其合约的地址
        }
        // 计算阶乘
        function jiecheng(uint n) public returns(uint){
            return factorial.getFactorial(n);
        }
    }
    

    12. 通过new关键字创建合约对象

    通过new关键字创建合约对象最大的优势
    不需要先部署被调用函数所在的合约,并先获取被调用函数所在合约的地址,然后才能部署调用函数的合约。

    换句话说就是,合约A调用合约B中的函数还需要先部署合约B是比较麻烦的。但是通过new关键字创建合约对象,则不需要部署合约B就可以调用B中的函数。

    相对于上面CallOtherContract.sol的代码,只需将FunCallContract的构造函数

    constructor(address addr) public{
            factorial = FactorialContract(addr);//实例化合约实例的时候需要传入其合约的地址
        }
    

    修改为

    // CallOtherContract_1.sol
    constructor() public{
             // 通过new关键字创建合约对象(此时不需要传入该合约对象的合约地址)
            factorial = new FactorialContract();
        }
    

    其他不用变化。

    这样使用new关键字创建合约对象,就不需要先部署FactorialContract合约,并获取其合约的地址后,然后才能部署FunCallContract合约,在其合约内部调用其FactorialContract合约中的函数。
    这里可以直接部署FunCallContract合约。

    13. 函数的命名参数

    在solidity语言中调用函数时可以指定命名参数,通过命名参数,可以不按被调用函数的参数的定义的顺序传入参数值

    pragma solidity >=0.4.16 <=0.7.0;
    
    // 命名参数的使用
    contract NamedParameter{
        function sub(int n1,int n2) public pure returns(int) {
            return n1-n2;
        }
        function fun() public pure returns(int){
            // 通过函数的命名参数,可以不按被调用函数中的参数的定义顺序进行赋值
            // 命名参数要通过{...}传递,有点类似于javascript中的对象
            return sub({n2:66,n1:32});
        }
    }
    

    15. 函数多返回值解构和元组赋值

    • (1)多返回值解构:如果函数返回多个值,可以支持将多个返回值分别赋给相应数目的变量
    • (2)元组赋值:指赋值运算符(=)左侧和右侧都有n个变量。
    pragma solidity >=0.4.24 <=0.7.0; //注意:只有0.4.24及以上版本才支持多返回值解构和元组赋值
    
    contract AssignmentContract{
        uint[] data;
        function mulValueFun() public pure returns(uint,bool,uint){
            return (2018,true,2019);
        }
    
        function assignment() public returns(uint xx,uint yy,bool bb,uint length){
            // 多返回值解构赋值,x、b和y分别等于mulValueFun函数的3个返回值
            (uint x,bool b,uint y) = mulValueFun();
            // 交换x和y的值
            (x,y)=(y,x);  //元组赋值
            // 这里只指定了一个变量(data.length),所以mulValueFun函数的其他返回值会被忽略
            (data.length,,) = mulValueFun(); //未指定的变量,通过逗号(,)将位置留着
            // 重新设置y变量的值
            y = 123;
            // 设置返回值
            xx = x;
            yy = y;
            bb = b;
            length = data.length;
        }
    }
    

    16. 变量声明和作用域

    (1)0.5.0版本之前

    在Solidity 0.5.0之前,Solidity语言的作用域规则继承自JavaScript。
    在if、while、for循环中定义的变量仍然作用于{...}外面,也就是说 {...}中声明的变量,在 {...}外仍然可以使用
    换句话说,就是无论{..}内还是{...}外,都不能有同名的变量。

    (2)0.5.0版本之后

    在Solidity 0.5.0之后, 开始支持声明块({...})变量,也就是在 {...}中声明的变量只在{...}中有效,这就意味着在多个{...}中可以声明多个同名的变量。

    17. 错误处理

    Solidity语言有3种与错误处理相关的函数:

    • (1)require:用于校检外部输入,如函数的参数、调用外部函数的返回值等。
    • (2)assert:用于校检合约的内部错误
    • (3)revert抛出错误

    Solidity语言的错误处理与数据库中的事务回滚类似,一旦发生错误以前做的所有操作都将回滚,因为合约很可能涉及到转账等敏感操作,所以一旦有任何异常,必须全部恢复到最初的状态,以避免数据不一致的情况发生。

    18. 全局变量

    (1)block变量

    pragma solidity >=0.4.20 <=0.7.0;
    
    contract BlockContract{
        function getBlockInfo() public view returns(address coinbase,uint difficulty,
        uint gaslimit,uint number,uint timestamp){
            coinbase = block.coinbase; //获取挖出当前区块的矿工的地址;
            difficulty = block.difficulty; //获取当前区块的挖矿难度;
            gaslimit = block.gaslimit; //获取当前区块的gas限制;
            number = block.number; //获取当前区块的编号
            timestamp = block.timestamp; //获取当前区块的时间戳(从Unix epoch即Unix纪元,从1970年1月1日开始)
        }
    }
    

    (2)msg变量

    • (1)执行函数包含参数:
    pragma solidity >=0.4.20 <=0.7.0;
    
    contract MsgContract{
        // 获取相关的系统信息
        function getMsgInfo(uint x) public payable returns(bytes memory data,uint gas,address sender,bytes4 sig,uint value){
            data = msg.data; //获取当前执行函数的调用数据(包含函数标识,即sha3散列值的前8位,若执行函数有参数,则还包含参数值)
            // gas = msg.gas; // msg.gas已经被gasleft()函数取代
            gas = gasleft(); // 获取剩余的gas
            sender = msg.sender; // 获取当前执行函数的调用地址
            sig = msg.sig; // 获取当前执行函数的标识(sha3散列值的前8位)
            value = msg.value; // 当前被发送的wei的数量(使用该属性的函数要使用payable关键字修饰)
        }
    }
    

    Solidity学习笔记_第1张图片

    结果:
    Solidity学习笔记_第2张图片

    • (2)执行函数不包含参数:

    把上述合约函数中的getMsgInfo(uint x)修改为getMsgInfo( ), 即去掉函数的参数。
    结果:
    Solidity学习笔记_第3张图片

    • msg.data表示当前执行函数的调用数据,包含函数标识(即sha3散列值的前8位)。如果执行函数包含参数,则其还包含参数值
    • msg.sig表示当前执行函数的标识(即sha3散列值的前8位)。
    • 换句话说,如果执行函数不包含参数,则msg.data(只包含函数标识)与msg.sig(函数标识)是一样的

    例如,若当前执行的函数是getMsgInfo( ),那么可以使用下面的Node.js代码获取该函数sha3散列值的前8位。该值与msg.data属性返回的值相同(即都是只包含函数标识)。

    var Web3 = require('Web3');
    web3 = new Web3( );
    // 由于sha3函数返回的值前两位是表示十六进制的0x,所以从第3个字符开始截取,截取的长度为8位
    sign = web3.sha3("getMsgInfo( )").substr(2,8);
    console.log(sign); //输出 4c668374

    (3)其他全局变量

    pragma solidity >=0.4.22 <=0.7.0;
    
    // 其他全局变量
    contract OtherGlobalContract{
        // 获取其他全局变量的值
        function getOtherGlobal() public view returns(bytes32 hash,uint nowTime,uint gasPrice,address origin){
            // 获取指定区块的哈希值(要传入区块号)
            hash = blockhash(1001);
            // 获取当前区块的时间戳(与block.timestamp属性返回的值相同)
            nowTime = now;
            // 获取交易的gas价格
            gasPrice = tx.gasprice;
            // 获取发送交易的地址
            origin = tx.origin; 
        }
    }
    

    19. 自定义修饰符(modifier)

    modifier常用于在函数执行前检查某种前置条件是否满足,modifier是一种合约属性,可以被继承(子合约可以使用父合约中定义的modifier),同时还可被派生的合约重写(override)。

    modifier  modiferName{
        //校检代码
        _;
    }
    

    校检代码用于校检使用自定义修饰符的函数,后面必须跟一个下划线(_),而且下划线后面跟分号( ; )。如果通过校检,将使用该定义修饰符的函数的函数体插入到下划线的位置。也可以认为自定义修饰符其实就是多个函数相同代码的抽象,除了校检代码。

    pragma solidity >=0.4.20 <0.7.0;
    
    contract OwnerContract{
        address owner;
        // 保存部署合约的账号
        constructor() public{
            owner = msg.sender;
        }
        // 定义用于检测msg.sender是否为部署合约的账号,如果不是,终止执行函数
        modifier onlyOwner{
            require(msg.sender == owner,"Only owner can call this function.");
            _;   // 如果校检通过,会将使用onlyOwner函数的函数体插到这个位置。
        }
        // 校检地址是否可以为空
        // 当输入的_address为0x0000000000000000000000000000000000000000(0x后40个0),会抛出“_address can not be 0!”
        modifier notNull(address _address){
            require(_address != address(0),"_address can not be 0!");
            _;
        }
        // 一个函数可以有多个修饰符,多个修饰符之间用空格或回车分隔,修饰符的生效顺序与定义顺序是一样的
        // 修改合约所有者
        function changeOwner(address newOwner) notNull(newOwner) onlyOwner() public{
            owner = newOwner;
        }
    }
    
    //从OwnerContract继承 
    contract AddContract is OwnerContract{
        // 使用onlyOwner修饰函数
        function add(uint m,uint n)  public view onlyOwner() returns(uint){
            return m+n;
        }
    }
    
    contract RestrictContract{
        uint public mm;
        uint public nn;
        // 用于校检 m是否大于或等于n,如果不满足条件,相当于将使用restrict1函数的函数体删除
        modifier restrict1(uint m,uint n){
            if(m>=n){ //如果不满足条件,相当于将使用restrict1函数的函数体删除
                _;
            }
        }
        // 除了校检m是否大于n外,还将m和n分别保存在mm和nn变量中
        modifier restrict2(uint m,uint n){
            require(m>=n,"m can not less than n");
            mm = m;
            nn = n;
            _;
        }
    }
    
    // 从RestrictContract合约继承
    contract SubContract is RestrictContract{
        // 使用restrict1修饰sub1函数
        function sub1(uint m,uint n) public pure restrict1(m,n) returns(uint){
            return m-n;
        }
        // 使用restrict2修饰sub2函数
        function sub2(uint m,uint n) public restrict2(m,n) returns(uint){
            return m-n;
        }
    }
    

    20. pure和view

    (1)pure

    使用pure关键字修饰的函数不允许读写 状态变量,否则会编译出错。
    下面几种情况会被认为是读写状态变量,在这些情况下,用pure关键字修饰函数就会编译错误:

    • (1)直接读取状态变量;
    • (2)访问 this.balance
      .balance
    • (3)访问任何block、tx、msg变量中的成员,但msg.sig和msg.data除外
    • (4)调用任何没有使用pure修饰的函数,哪怕是这个函数中确实没有读写任何状态变量。
    • (5)内嵌用于操作状态变量的汇编代码的函数。

    (2)view

    使用view关键字修饰函数时,表示该函数不会修改状态变量
    下面几种情况表明函数会修改合约的状态变:

    • (1)只写修改状态变量;
    • (2)触发事件
    • (3)创建其他合约的实例
    • (4)调用selfdestruct函数销毁合约;
    • (5)通过call函数方发送以太币
    • (6)调用任何未标记view或pure函数
    • (7)使用底层的call函数;
    • (8)内嵌用于操作状态变量的汇编代码的函数;

    需要注意的是:用view修饰的函数并不会阻止函数中修改状态变量只是在用view修饰的函数中修改状态变量会出现警告。(不报错,只出现警告)

    21. fallback函数(回调函数)*

    fallback函数:一个没有函数名、参数和返回值的函数。必须用external进行修饰。
    在下面两种情况下会调用fallback函数:

    • (1) 合约中没有匹配的函数标识

    换句话说,就是

    1. 该合约没有其他函数;
    2. 调用合约时,如果没有匹配上该合约中的任何一个函数,就会调用回调函数。
    • (2) 合约接收到以太币(交易中没有附带任何其他数据),也会调用回调函数。

    注意:

    1. 这种情况下,fallback函数要使用payable关键字修饰,否则给包含fallback函数的合约发送以太币时出现编译错误
    2. 即使 fallback 函数不能有参数仍然可以使用 msg.data 来获取随调用提供的任何有效数据

    另外,还需注意以下几点:

    • (1) 如果调用者想调用一个不存在的函数,fallback函数将会被执行。
    • (2) 如果你只想为了接收以太币而实现fallback函数,你需要增加一些校检(如 require(msg.data.length == 0 ) )去避免一些无效的调用。
    • (3) 一个没有定义fallback函数(回调函数)的合约直接接收以太币(没有函数调用,如使用send或transfer),则会抛出一个异常,并返还以太币(有些行为在Solidity V0.4.0之前有些不同)。因此如果你要使你的合约接收以太币,你就必须实现一个被payable修饰的fallback函数。

    一个没有 payable fallback 函数的合约,可以作为 coinbase transaction (又名 miner block reward )的接收者或者作为 selfdestruct 的目标来接收以太币。

    pragma solidity >=0.5.0 <=0.7.0;
    
    contract Test{
        uint x;
        // (1)给这个合约发送任何消息都会调用这个函数(因为合约没有其他函数)
        // 定义一个fallback函数,在该函数中设置了状态变量x。
        // (2)向这个合约发送以太币将会抛出一个异常,因为这个回调函数没有用“payable”修饰符修饰。
        function() external{ x=101; }
        
    }
    
    contract Sink{
        // 定义了一个fallback函数,该函数使用payable修饰,表明可以接受其他地址发过来的以太币。
        function() external payable{ }
    }
    
    contract Caller{
        function callTest(Test test) public returns(bool){
            // 这里调用一个不存在的函数,由于匹配不到函数,所以将调用Test合约中的回调函数。
            (bool success,) = address(test).call(abi.encodeWithSignature("nonExitingFunction()"));
            require(success);
            // address(test)不允许直接调用“send”方法,因为“test”没有被“payable”修饰的回调函数。
            // 其必须通过“uint160”进行一个中间转换,然后再转换为“address payable”类型才能调用“send”方法。
            address payable testPayable = address(uint160(address(test)));
            // 如果某人发送以太币给那个合约,这笔交易将会失败(例如,这里将会返回false)
            return testPayable.send(2 ether);
        }
    
        function callSink(address payable sinkAddress) public returns(bool){
            Sink sink = Sink(sinkAddress);
            // 如果向Sink合约发送以太币时发送成功,Sink中的fallback函数会被调用
            return address(sink).send(5 ether);
        }
    }
    

    参考:Fallback Function

    22. 函数重载

    (1)定义

    函数重载是指一个合约中定义了多个函数名相同,但参数个数和类型不同的函数。(不考虑返回值)

    需要注意的是
    如果函数参数类型是可以转换的,例如合约和address,Solidity编译器就会认为它们是同一个数据类型,因此会产生编译错误

    (2)实例

    pragma solidity >=0.4.20 <=0.7.0;
    
    // 拥有4个同名的重载函数
    contract OverloadContract1{
        // 拥有2个uint类型的参数
        function add(uint m,uint n) public pure returns(uint){
            return m+n;
        }
        // 没有参数
        function add() public pure returns(uint){
            return 11+22;
        }
        // 有一个bool类型参数
        function add(bool b) public pure returns(bool){
            return b;
        }
        // 有3个uint类型的参数
        function add(uint l,uint m,uint n) public pure returns(uint){
            return l+m+n;
        }
    }
    
    contract A{
    
    }
    
    // 从表面上看第一个和第二个test函数的参数不一样,其实是一样的。因为合约A本身就是一个address类型
    // 所以OverloadContract2合约编译会失败,因为前两个test函数无法实现函数重载
    contract OverloadContract2{
        // 函数重载失败
        function test(address addr) public view returns(uint){
            return addr.balance;
        }
        // 函数重载失败,具体报错:Function overload clash during conversion to external types for arguments.
        // function test(A a) public view returns(uint){
        //     return address(a).balance;
        // }
    
        // 函数重载成功
        function test(A a,uint b) public view returns(uint,uint){
            return (address(a).balance,b);
        }
       
    }
    

    23. 事件(event)

    (1)定义

    如果将合约部署在TestRPC环境或者以太坊网络上,在执行以太坊函数时无法直接获得函数的返回值的,但是可以通过事件将计算结果返回给客户端

    event EventName( typeName  parameter,... );
    

    (2)实例

    pragma solidity >=0.4.20 <=0.7.0;
    
    contract EventContract{
        // 定义MyEvent事件
        event MyEvent(
            uint m,
            uint n,
            uint results
        );
    
        function add(uint m,uint n) public returns(uint){
            uint results = m+n;
            // 使用emit指令触发MyEvent事件,并通过事件参数传递m、n和m+n的计算结果(传递到客户端)
            emit MyEvent(m,n,results);
            return results;
        }
    }
    

    24. 合约继承

    合约继承,使用is关键字指定父合约。

    • (1) Solidity合约支持多继承,如果要指定多个合约,合约之间用逗号( , )分隔
    • (2) 尽管可以指定多个父合约,但是只会创建一个合约实例,将其他父合约中的代码复制到这个合约实例中。
    • (3) 如果多个父合约实现了同样的函数,那么以最后一个父合约的函数为准

    25. 合约构造函数

    • (1)老版本的solidity语言中,合约的构造函数与普通函数类似,只是函数名与合约名相同
    • (2)新版本的solidity语言中,使用constructor作为构造函数的名字

    这样做的好处是,一旦改变了合约的名字,也不用修改其构造函数的名字。

    • (3)合约构造函数允许使用publicinternal修饰。
    pragma solidity >=0.4.20 <=0.7.0;
    
    contract Contract1{
        uint public a;
        // 带参数的构造函数,假设用internal修饰
        constructor(uint _a) internal{
            a = _a; //用来初始化状态变量
        }
    }
    
    // 从Contract1继承,并将构造函数重新用public修饰,变成外部可访问的构造函数。
    // 由于Contract1合约的构造函数有一个参数,所以在继承时需要指定Contract1合约构造函数的参数值。
    contract Contract2 is Contract1(100){
        constructor() public{
    
        }
    }
    
    contract Contract3 is Contract1{
        uint aa;
        uint bb;
        // 如果构造参数的参数需要用某些变量设置,如构造函数的参数,可以在构造函数后面指定父合约构造函数的参数值
        constructor(uint _a,uint _b) Contract1(_a*_b) public{
            aa = _a;
            bb = _b;
        }
    }
    

    26. 抽象合约

    抽象合约: 至少有一个函数没有实现的合约。

    如果合约从一个抽象合约继承,而且没有全部实现抽象合约中的函数,那么这个合约就会继承这些未实现的函数,所以这个合约也是抽象合约。(说白了,就是这个合约继承了一个抽象合约,但是还有些继承自抽象合约的函数没有实现,于是这个合约也就有了一些函数没有实现,所以这个合约也就是抽象合约了。)

    抽象合约通常来实现多态,也就是用抽象合约的多个子合约创建多个实例,将这些实例赋给抽象合约类型变量
    由于这些子合约都实现了抽象合约中的函数,所以调用抽象合约中的函数会根据抽象合约类型变量的值不同,调用结果也不同,这就是称为多态。(调用同一个函数,会有多种不同表现形态)

    pragma solidity >=0.5.0 <=0.7.0;
    
    /**
        在MyContract合约中的test1和test2函数中分别创建了 MyContract1和MyContract2的实例,
        且将这两个合约的实例都赋值给了AbstractContract类型(抽象合约类型)的变量。
        在test1和test2函数中都调用了AbstractContract合约(父合约)中的add函数,且输入相同的实参值,
        不过返回结果却不一样,这就是多态。
        实际上,本质上调用的是MyContract1(子合约)和MyContract2合约(子合约)中的add函数。
     */
    
    contract AbstractContract{
        // add函数没有实现
        function add(uint m,uint n) public returns(uint);
        // 完整实现了sub函数
        function sub(int m,int n) public pure returns(int){
            return m-n;
        }
    }
    
    // 该合约从AbstractContract继承(即MyContract1是AbstractContract的一个子合约)
    contract MyContract1 is AbstractContract{
        // 实现了抽象合约中的add函数
        function add(uint m,uint n) public returns(uint){
            return m+n;
        }
    }
    
    // 该合约从AbstractContract继承(即MyContract2是AbstractContract的另一个子合约)
    contract MyContract2 is AbstractContract{
        // 实现了抽象合约中的add函数
        function add(uint m,uint n) public returns(uint){
            return 4*(m+n);  //不同于MyContract1中add函数的实现
        }
    } 
    
    // 该合约从MyContract1 继承,即继承了add函数和sub函数
    contract MyContract is MyContract1{
        function test1(uint m,uint n) public returns(uint){
            // 创建MyContract1 合约的实例
            AbstractContract abstractContract = new MyContract1();
            // 实际是调用了MyContract1 合约中的add函数
            return abstractContract.add(m,n);
        }
    
        function test2(uint m,uint n) public returns(uint){
            // 创建MyContract2 合约的实例
            AbstractContract abstractContract = new MyContract2();
            // 实际是调用了MyContract1 合约中的add函数
            return abstractContract.add(m,n);
        }
    }
    

    27. 接口

    接口与抽象合约类似,但是不能实现任何函数。(即所有接口中的方法都是未实现的)
    此外,接口还有如下限制:

    • (1)不能继承其他合约或接口
    • (2)不能定义构造函数;
    • (3)不能定义变量;
    • (4)不能定义结构体;
    • (5)不能定义枚举类型。
    interface interfaceName{
        //抽象方法(未被实现的方法)
    }
    

    注意:
    (1)接口应该定义在合约的外部(与合约是同一等级);
    (2)接口中定义的方法必须被external修饰;

    合约实现接口的方法与继承合约或抽象合约的方法类似, 使用is关键字.

    pragma solidity >=0.5.0 <=0.7.0;
    
    // 定义接口(定义在合约外面)
    interface MyInterface{
        function add(uint m,uint n) external returns(uint);
        function sub(int m,int n) external returns(int);
    }
    // InterfaceContract实现了MyInterface
    contract InterfaceContract is MyInterface{
        function add(uint m,uint n) public returns(uint){
            return m+n;
        }
        function sub(int m,int n) public returns(int){
            return m-n;
        }
    }
    

    28. gas limit和gas price

    (1)gas limit

    • (1) gas limit 表示完成转账交易最大消耗的gas数,如果超过这个gas数,交易就会失败,整个交易过程都会回滚。
    • (2) gas limit 主要是为了防止由于发布交易消耗过多的gas

    (2)gas price

    • 表示你愿意为单位gas支付的费用,以gwei为单位表示。
      1 gwei = 10^9 wei

    (3)两者的作用

    • (1)在交易中gasPrice是由发起交易人来决定的,每个矿工接收到交易请求之后,会根据gasPrice的高低来决定是否要打包进区块。
    • (2)每个交易中必须包含gas limit和gas price的值。gas limit代表了这个交易执行过程中最多被允许消耗的gas数量。
    • (3)gas limit和gas price 代表着交易发送者愿意为执行交易支付的wei的最大值。
      付款金额(单位 wei)= Gas数量 × GasPrice
    • (4)交易执行完成后,如果实际消耗的gas小于gaslimit,那么剩余的gas会以Ether的方式返回给交易发起者。
    • (5)如果在交易过程中,实际消耗的gas大于gas limit,那么就会出现“gas不足”的错误,这种情况下交易会被终止,交易之前的所有修改的状态会被回滚,同时在交易执行过程中所消耗的gas不会回退给交易发起者的

你可能感兴趣的:(区块链)