欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)

程序员文章站 2022-07-16 18:08:47
...

区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)

作者: AlexTan
CSDN: http://blog.csdn.net/alextan_
Github: https://github.com/AlexTan-b-z
e-mail: aaa@qq.com

前言

(ps:这是本博主撰写的第二部系列作品,第一部是写的java入门教程,受到了不少读者的喜欢,如果你也喜欢的话,欢迎关注哟!)

本教程主要面向区块链新手,用通俗易懂的方式讲解区块链技术。

这一小节,我们主要讲以太访、智能合约、web3三者之间的关系,以及智能合约(solidity)的基本特性。为了方便开发,理解这些是非常有必要的。

以太访、web3、智能合约

什么是以太访,到这里我想大家大概都已经明白了。那什么是web3(泛指web3.js、web3.py等)呢?前面也提到过,它就相当于是以太访区块链的接口(api),我们是通过web3,把智能合约部署在区块链上的,我们是通过web3获取到的区块链相关信息以及进行一些交易。更多细节请参考:web3.js API中文文档
区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)

web3

其实web3无非是封装的以太访的JSON-RPC,其JSON-RPC针对不同版本的以太访客户端支持以下请求方式:

cpp-ethereum go-ethereum py-ethereum parity
JSON-RPC 1.0
JSON-RPC 2.0
Batch requests
HTTP
IPC
WS

更多请参考:

智能合约,Solidity的特性介绍

以太访中,开发智能合约有很多种语言:

  • Solidity:这是一种类似于js的语言,并且这个语言被称为开发智能合约的旗舰语言。
  • Serpent:这是一种类似于Python的语言。
  • LLL:这是一种类似于汇编的低级语言。
  • Mutan:这是一种类似于C的语言,目前已被放弃。

而目前官方的最流行的就是solidity。

Solidity的合约其实就类似于面向对象里所说的类(从某种意义来说,其实就是!),但是和传统的类又有所不同,其不同点主要在以下几点(仅对代码而言):

    1.  调用机制不同
        2.  成员类型不同
        3.  构造函数不能重载

cankao我们将详细讲解以上几点:

Solidity的调用机制

Solidity的函数调用机制是消息调用的模式,什么是消息调用呢?

其实可以简单的把它理解成交易(ps:可以把智能合约的所有的函数调用都理解成交易,这也是为什么说智能合约是通过用户的交易行为触发的。),交易即要有发起交易的人,发送目标是什么,发送的数额是多少等。也就是说,每次的函数调用,也得有这些数据。

我们在合约函数内部可以用msg.sender获取调用函数的人(即上面提到的发起交易的人),msg.value获取用户发生的金额(可无),有没有发生目标根据函数实现的具体功能来确定。

而通过web3调用函数时,也得在函数参数后面加一个:{from:web3.eth.accounts[0], gas:44000000} 类似这样的一个参数。

调用函数的gas费

我们前面提到,调用合约函数是需要Gas费的,具体的说是,调用涉及写操作的函数是需要Gas费的,如果只是读操作,比如说读状态变量等,可以通过call()的方式调,这样是不需要消耗Gas费的,关于Gas费的计费方式我们前面已经讲到过,这里就不一一阐述了。

成员类型

像java、c++等面向对象的语言一般只有public、protected、private这三种类型,而solidity里有:public、private、internal、external四种类型,其中分别表示的意义是:

  • public: 默认是public属性,即内部和外部都可以访问
  • private:仅当前合约内可以访问
  • internal: 仅当前合约以及所继承的合约可以访问
  • external: 仅合约外部可以访问

需要注意的是,不像传统语言那样,成员变量的外部调用可以更改变量的值,solidity的成员外部调用是不可以修改变量值的,任何类型都不能修改

需要注意的另一个点:不同的类型的gas花费是不同的,能少用public尽量少用public,为什么呢?下面我们来看实例:

pragma solidity^0.4.18;



contract Test {

    uint[10] x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];



    function test(uint[10] a) public returns (uint){

         return a[9]*2;

    }



    function test2(uint[10] a) external returns (uint){

         return a[9]*2;

    }



    function calltest() {

        test(x);

    }



    function calltest2() {

        this.test2(x);

        //test2(x);  //不能在内部调用一个外部函数,会报编译错误。

    }  



}

打开Remix - Solidity IDE,帖入代码,创建合约。
然后,我们分别调用test及test2,对比执行花费的gas。
区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)
区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)
可以看到调用pubic函数花销更大,这是为什么呢?

当使用public 函数时,Solidity会立即复制数组参数数据到内存, 而external函数则是从calldata读取,而分配内存开销比直接从calldata读取要大的多。
那为什么public函数要复制数组参数数据到内存呢?是因为public函数可能会被内部调用,而内部调用数组的参数是当做指向一块内存的指针。
对于external函数不允许内部调用,它直接从calldata读取数据,省去了复制的过程。

所以,如果确认一个函数仅仅在外部访问,请用external

同样,我们接着对比calltest()及calltest2(),这里不截图了,大家自己运行对比一下,可以发现:calltest2的开销比calltest的开销大很多,这是因为通过this.f()模式调用,会有一个大开销的CALL调用,并且它传参的方式也比内部传递开销更大。

因此,极不建议用this.function()的方式在内部调用external的函数,如果需要内外部都能访问,还是老实用public吧。

同理,我们把external改成private或者internal,我们会发现external、private会比public的花费更少,因此,当成员变量只需要内部访问时,尽量用private或者internal

总之,当我们确定好成员的使用范围时,用其相应的类型就好,不要一位的使用public。

Solidity函数中view,constant,pure,payable的使用

  • contant: 函数有返回值,且返回的是状态变量。

  • pure:函数有返回值,且返回的不是变量,返回的是一个具体的值。

  • view:函数有返回值,返回的既不是具体的值也不是状态变量,例如:局部变量、msg.sender等。

  • payable: 可以通过这个函数发送以太币给合约。

Solidity的数据类型

Solidity 类型分为两类:值类型(Value Type)引用类型(Reference Types)

值类型(Value Type)

值类型包含:

  • 布尔类型(Booleans)
  • 整型(Integers)
  • 定长浮点型(Fixed Point Numbers)
  • 定长字节数组(Fixed-size byte arrays)
  • 有理数和整型常量(Rational and Integer Literals)
  • 字符串常量(String literals)
  • 十六进制常量(Hexadecimal literals)
  • 枚举(Enums)
  • 函数(Function Types)
  • 地址(Address)
  • 地址常量(Address Literals)

布尔类型(Booleans)

布尔(bool):可能的取值为常量值truefalse

布尔类型支持的运算符有:

  • !逻辑非
  • && 逻辑与
  • || 逻辑或
  • == 等于
  • != 不等于

注意:运算符&&||是短路运算符,如f(x)||g(y),当f(x)为真时,则不会继续执行g(y)。

cankao

整型(Integers)

int/uint: 表示有符号和无符号不同位数整数。支持关键字uint8uint256 (以8步进),
uintint 默认对应的是 uint256int256

支持的运算符:

  • 比较运算符: <=, < , ==, !=, >=, > (返回布尔值:true 或 false)
  • 位操作符: &,|,^(异或),~(位取反)
  • 算术操作符:+,-,一元运算-,一元运算+,,/, %(取余数), **(幂), << (左移位), >>(右移位)

说明:

  1. 整数除法总是截断的,但如果运算符是字面量(字面量稍后讲),则不会截断。
  2. 整数除0会抛异常。
  3. 移位运算的结果的正负取决于操作符左边的数。x << y 和 x * 2***y 是相等, x >> y 和 x / 2**y 是相等的。
  4. 不能进行负移位,即操作符右边的数不可以为负数,否则会抛出运行时异常。

注意:Solidity中,右移位是和除等价的,因此右移位一个负数,向下取整时会为0,而不像其他语言里为无限负小数。

定长浮点型(Fixed Point Numbers)

注意:定长浮点型 Solidity(发文时)还不完全支持,它可以用来声明变量,但不可以用来赋值。
fixed/ufixed: 表示有符号和无符号的固定位浮点数。关键字为ufixedMxNufixedMxN
M表示这个类型要占用的位数,以8步进,可为8到256位。
N表示小数点的个数,可为0到80之前

支持的运算符:

  • 比较运算符: <=, < , ==, !=, >=, > (返回布尔值:true 或 false)
  • 算术操作符:+,-,一元运算-,一元运算+,*,/, %(取余数)
    注意:它和大多数语言的float和double不一样,M是表示整个数占用的固定位数,包含整数部分和小数部分。因此用一个小位数(M较小)来表示一个浮点数时,小数部分会几乎占用整个空间。

定长字节数组(Fixed-size byte arrays)

关键字有:bytes1, bytes2, bytes3, …, bytes32。(以步长1递增)
byte代表bytes1。

支持的运算符:

  • 比较符: <=, <, ==, !=, >=, > (返回bool)
  • 位操作符: &, |, ^ (按位异或),~(按位取反), << (左移位), >> (右移位)
  • 索引(下标)访问: 如果x是bytesI,当0 <= k < I ,则x[k]返回第k个字节(只读)。

移位运算和整数类似,移位运算的结果的正负取决于操作符左边的数,且不能进行负移位。

成员变量:
.length:表示这个字节数组的长度(只读)。

cankao

变长(动态分配大小)字节数组(Dynamically-sized byte array)

  • bytes:动态分配大小字节数组, 参见Arrays,不是值类型!
  • string:动态分配大小UTF8编码的字符类型,参看Arrays。不是值类型!

根据经验:
bytes用来存储任意长度的字节数据,string用来存储任意长度的(UTF-8编码)的字符串数据。
如果长度可以确定,尽量使用定长的如byte1到byte32中的一个,因为这样更省空间。

有理数和整型常量(Rational and Integer Literals)

也有人把Literals翻译为字面量

整型常量是有一系列0-9的数字组成,10进制表示,比如:8进制是不存在的,前置0在Solidity中是无效的。

10进制小数常量(Decimal fraction literals)带了一个., 在.的两边至少有一个数字,有效的表示如:1., .11.3.

科学符号也支持,基数可以是小数,指数必须是整数, 有效的表示如: 2e10, -2e10, 2e-10, 2.5e1

数字常量表达式本身支持任意精度,也就是可以不会运算溢出,或除法截断。但当它被转换成对应的非常量类型,或者将他们与非常量进行运算,则不能保证精度了。
如:(2**800 + 1) - 2**800的结果为1(uint8整类) ,尽管中间结果已经超过计算机字长。另外:.5 * 8的结果是4,尽管有非整形参与了运算。

只要操作数是整形,整型支持的运算符都适用于整型常量表达式。
如果两个操作数是小数,则不允许进行位运算,指数也不能是小数。

注意:
Solidity对每一个有理数都有一个数值常量类型。整数常量和有理数常量从属于数字常量。所有的数字常表达式的结果都属于数字常量。所以1 + 2和2 + 1都属于同样的有理数的数字常量3

警告:
整数常量除法,在早期的版本中是被截断的,但现在可以被转为有理数了,如5/2的值为 2.5

注意:
数字常量表达式,一旦其中含有常量表达式,它就会被转为一个非常量类型。下面代码中表达式的结果将会被认为是一个有理数:

uint128 a = 1;

uint128 b = 2.5 + a + 0.5;

cankao上述代码编译不能通过,因为b会被编译器认为是小数型。

字符串常量

字符串常量是指由单引号,或双引号引起来的字符串 (“foo” or ‘bar’)。字符串并不像C语言,包含结束符,”foo”这个字符串大小仅为三个字节。和整数常量一样,字符串的长度类型可以是变长的。字符串可以隐式的转换为byte1,…byte32 如果适合,也会转为bytes或string。

字符串常量支持转义字符,比如\n,\xNN,\uNNNN。其中\xNN表示16进制值,最终转换合适的字节。而\uNNNN表示Unicode编码值,最终会转换为UTF8的序列。

十六进制常量(Hexadecimal literals)

十六进制常量,以关键字hex打头,后面紧跟用单或双引号包裹的字符串,内容是十六进制字符串,如hex”001122ff”。cankao
它的值会用二进制来表示。

十六进制常量和字符串常量类似,也可以转换为字节数组。

枚举(Enums)

在Solidity中,枚举可以用来自定义类型。它可以显示的转换与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。枚举类型应至少有一名成员。下面是一个枚举的例子:

pragma solidity ^0.4.0;



contract test {

    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }

    ActionChoices choice;

    ActionChoices constant defaultChoice = ActionChoices.GoStraight;



    function setGoStraight() {

        choice = ActionChoices.GoStraight;

    }



    // Since enum types are not part of the ABI, the signature of "getChoice"

    // will automatically be changed to "getChoice() returns (uint8)"

    // for all matters external to Solidity. The integer type used is just

    // large enough to hold all enum values, i.e. if you have more values,

    // `uint16` will be used and so on.

    function getChoice() returns (ActionChoices) {

        return choice;

    }



    function getDefaultChoice() returns (uint) {

        return uint(defaultChoice);

    }

}

引用类型

引用类型是一个复杂类型,占用的空间通常超过256位, 拷贝时开销很大,因此我们需要考虑将它们存储在什么位置,是memory(内存中,数据不是永久存在)还是storage(永久存贮在区块链中)
所有的复杂类型如数组(arrays)和数据结构(struct)有一个额外的属性:数据的存储位置(data location)。可为memorystorage

根据上下文的不同,大多数时候数据位置有默认值,也通过指定关键字storage和memory修改它。

函数参数(包含返回的参数)默认是memory
局部复杂类型变量(local variables)和 状态变量(state variables) 默认是storage

局部变量:局部作用域(越过作用域即不可被访问,等待被回收)的变量,如函数内的变量。状态变量:合约内声明的公有变量

还有一个存储位置是:calldata,用来存储函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多。

数据位置指定非常重要,因为他们影响着赋值行为。
在memory和storage之间或与状态变量之间相互赋值,总是会创建一个完全独立的拷贝。
而将一个storage的状态变量,赋值给一个storage的局部变量,是通过引用传递。所以对于局部变量的修改,同时修改关联的状态变量。
另一方面,将一个 memory 的引用类型赋值给另一个 memory 的引用,不会创建拷贝(即:memory 之间是引用传递)。

  1. 注意:不能将 memory 赋值给局部变量。
  2. 对于值类型,总是会进行拷贝。

下面看一段代码:

pragma solidity ^0.4.0;



contract C {

    uint[] x; //  x的存储位置是storage



    // memoryArray的存储位置是 memory

    function f(uint[] memoryArray) public {

        x = memoryArray;    // 从 memory 复制到 storage

        var y = x;          // storage 引用传递局部变量y(y 是一个 storage 引用)

        y[7];               // 返回第8个元素

        y.length = 2;       // x同样会被修改

        delete x;           // y同样会被修改



        // 错误, 不能将memory赋值给局部变量

        // y = memoryArray;  



        // 错误,不能通过引用销毁storage

        // delete y;        



        g(x);               // 引用传递, g可以改变x的内容

        h(x);               // 拷贝到memory, h无法改变x的内容

    }



    function g(uint[] storage storageArray) internal {}

    function h(uint[] memoryArray) public {}

}

总结

强制的数据位置(Forced data location)

  • 外部函数(External function)的参数(不包括返回参数)强制为:calldata
  • 状态变量(State variables)强制为: storage

默认数据位置(Default data location)

  • 函数参数及返回参数:memory
  • 复杂类型的局部变量:storage

深入分析

storage 存储结构是在合约创建的时候就确定好了的,它取决于合约所声明状态变量。但是内容可以被(交易)调用改变。

Solidity 称这个为状态改变,这也是合约级变量称为状态变量的原因。也可以更好的理解为什么状态变量都是storage存储。

memory 只能用于函数内部,memory 声明用来告知EVM在运行时创建一块(固定大小)内存区域给变量使用。

storage 在区块链中是用key/value的形式存储,而memory则表现为字节数组

关于栈(stack)

EVM是一个基于栈的语言,栈实际是在内存(memory)的一个数据结构,每个栈元素占为256位,栈最大长度为1024。
值类型的局部变量是存储在栈上。

不同存储的消耗(gas消耗)

  • storage 会永久保存合约状态变量,开销最大
  • memory 仅保存临时变量,函数调用之后释放,开销很小
  • stack 保存很小的局部变量,几乎免费使用,但有数量限制。

参考:Solidity官方文档-类型之data-location

捐助地址:
BTC:
区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)

36Q4ivp2bJer9fUQ6uyj4a4yLuTpp28D1T

ETH:
区块链入门教程之从比特币到以太访再到智能合约从架构概念到应用实战(DAPP)(四、以太访、web3、智能合约三者之间的关系及智能合约基本讲解)

0x786fda245ff497ce18e53618369a3e730a18fc1b

ENS: alextan.eth