区块链2.0的很大特色是引入了智能合约,智能合约的最大好处是可以 把任何交易自动化。当然任何过程的执行都是依赖编程意义上的过程:函数。Solidity语言的函数设计了与众不同的风格。在数据的访问保护还是有特色的,实现的是语法级别的保护。
这个文章主要介绍两种函数:构造函数与功能函数。
创建合约
合约创建与构造器
- 构造器的说明
- 语法基本上与Javascript与C++一样。
-
创建合约时,会执行一次构造函数(与合约同名的函数)。
-
构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。
-
在内部,构造函数参数在合约代码之后通过 ABI 编码传递。
-
如果你使用 web3.js 则不必关心这个问题。
-
构造器语法
constructor(参数......) public { ...... }
-
构造器可以使用public或者internal修饰
- 如果没有构造器,则产生public的默认构造器。
- 使用internal修饰的构造器,则意味这个合同是抽象合同。
- 合约构造器例子
pragma solidity ^0.5.0;
contract Contrac_Base{
uint age;
constructor(uint age_) public {
age = age_;
}
function getAge() public view returns (uint){
// return 99;
return age;
}
function setAge(uint age_) public {
age = age_;
}
}
使用web3创建合约
- web3创建合约传递参数,通过deploy函数来传递,等价于通过 ABI 编码传递。
var contract_addr ;
await new web3.eth.Contract(abi)
.deploy(
{
data: '0x' + bytecode, // 需要注意 字节码需要添加 '0x' 不然会有各种错误
arguments: [55] // 构造器参数
}
)
实验
- 不是带构造器的合约,并调用函数查看构造器的结果。
智能合约
- 文件名:
contract2_constructor.sol
pragma solidity ^0.5.0;
contract Contrac_Base{
uint age;
constructor(uint age_) public {
age = age_;
}
function getAge() public view returns (uint){
// return 99;
return age;
}
function setAge(uint age_) public {
age = age_;
}
}
Web部署、调用与执行
- 采用了异步执行的方式,并使用await同步。
/**
* 这个智能合约增加了构造器
*/
const path = require('path');
const fs = require('fs');
const solc = require('solc');
const Web3 = require("web3");
const web3 = new Web3();
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:9999"));
const file_name = "contract2_constructor.sol"
const sol_file = ".\\sols\\"+ file_name
const contract_name = "Contrac_Base"
// 获取智能合约的绝对路径
var contract_path = path.resolve(sol_file);
// console.log("合同路径: " + contract_path);
// 读取合约内容
var contract_source = fs.readFileSync(contract_path, 'utf-8');
// console.log(contract_source)
// 编译
var contract_json = JSON.stringify({
language: 'Solidity',
sources: {
"contract2_constructor.sol" : { // 指明编译的文件名
content: contract_source, // solidity 源代码
},
},
settings: { // 自定义编译输出的格式。以下选择输出全部结果。
outputSelection: {
'*': {
'*': [ '*' ]
}
}
},
});
var contract_compiled = solc.compile(contract_json);
// console.log(contract_compiled);
// 注意: solc的版本。
var contract_obj = JSON.parse(contract_compiled);
var ou = contract_obj.contracts[file_name][contract_name];
var abi = ou.abi;
var bytecode = ou.evm.bytecode.object;
console.log(abi);
console.log(bytecode);
// 部署合约前的准备
web3.eth.getAccounts().then(async function(accouts_){ // 设置函数是异步
accounts = accouts_;
// 1. 解锁账号
// (设置异步后,可以使用await等待解锁成功后(就是then调用后)Promise才继续执行。)
await web3.eth.personal.unlockAccount(accounts[0], "gaoke2020", 0).then(
function(result){
if (result){
console.log("解锁账号成功!");
}
else{
console.log("解锁账号失败!");
}
}
)
// 2. 部署合约
var contract_addr ;
await new web3.eth.Contract(abi)
.deploy(
{
data: '0x' + bytecode, // 需要注意 字节码需要添加 '0x' 不然会有各种错误
arguments: [55] // 构造器参数
}
).send(
{
from: accounts[0],
gas: '4712388',
gasPrice: '1000000'
}
)
.on(
'transactionHash',
function (hash) {
console.log("交易Hash:" + hash)
}
)
.on(
'receipt',
function (receipt) {
console.log("交易收据:" , receipt);
}
)
// .on( // 不注释会一直调用
// 'confirmation',
// function (confirmationNumber, receipt) {
// console.log( "交易确认:" + confirmationNumber);
// }
// )
.on(
'error',
function(error){
console.log("错误:" + error);
}
)
.then(
function(newContractInstance){ // 部署成功,返回一个新的合同实例,包含合同的部署地址
// console.log("合同地址:" + newContractInstance.options.address)
contract_addr = newContractInstance.options.address;
}
)
console.log("合同地址:" + contract_addr)
// 3. 执行合同完毕后,查询是否修改成功
await new web3.eth.Contract(abi, contract_addr)
.methods
.getAge()
.call(
{
from: accounts[0]
}
)
.then(
function(result){
console.log("查询构造器初始化结果[55]:" , result);
}
);
// 4. 获取刚刚部署好的合同, 并调用合约函数修改数据
await new web3.eth.Contract(abi, contract_addr)
.methods
.setAge(128)
.send(
{
from: accounts[0],
gas: '4712388',
gasPrice: '1000000'
}
)
.on(
'transactionHash',
function (hash) {
console.log("交易Hash:" + hash);
}
)
.on(
'receipt',
function (receipt) {
console.log("交易收据:" , receipt);
}
)
.on(
'error',
function(error){
console.log("错误:" + error);
}
)
.then(
function(result){
var keys = Object.getOwnPropertyNames(result);
console.log(keys);
for(var k in result){
console.log(k + ":" + result[k]);
}
}
);
// 3. 执行合同完毕后,查询是否修改成功
var age;
await new web3.eth.Contract(abi, contract_addr)
.methods
.getAge()
.call(
{
from: accounts[0]
}
)
.then(
function(result){
age = result;
}
);
console.log("返回的数据:", age); // 看看是否成功
});
- 后面只使用局部代码来说明
状态变量
状态变量的修饰符
-
对函数也有一样的修饰符。
-
状态变量四个修饰符:external ,public ,internal 或者 private
- 状态变量默认是internal
- external :
- 外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。 当收到大量数据的时候,外部函数有时候会更有效率。
- public :
- public 函数是合约接口的一部分,可以在内部或通过消息调用。对于公共状态变量, 会自动生成一个 getter 函数(见下面)。
- internal :
- 这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用。
- private :
- private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
- 注意:
- 合约中的所有内容对外部观察者都是可见的。设置一些 private 类型只能阻止其他合约访问和修改这些信息, 但是对于区块链外的整个世界它仍然是可见的。
修饰符的使用
- 可见性标识符的定义位置,对于状态变量来说是在类型后面,对于函数是在参数列表和返回关键字中间。
pragma solidity ^0.4.16;
contract ContractStorage {
uint public data;
}
public修饰符与getter函数
-
编译器自动为所有 public 状态变量创建 getter 函数。
- getter函数没有参数,函数名与状态变量名一样。该函数不会接收任何参数并返回一个状态变量。
- 可以在声明时完成状态变量的初始化。
-
getter 函数具有外部可见性。
- 如果在内部访问 getter(即没有 this. ),它被认为一个状态变量。
- 如果它是外部访问的(即用 this. ),它被认为为一个函数。
-
备注:
- this变量也可以使用,与C++的意义一样。
Constant 状态变量
-
状态变量可以被声明为 constant。
- 在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。
- 任何通过访问 storage,区块链数据(例如 now, this.balance 或者 block.number)或执行数据( msg.gas ) 或对外部合约的调用来给它们赋值都是不允许的。
- 在内存分配上有边界效应(side-effect)的表达式是允许的,但对其他内存对象产生边界效应的表达式则不行。
- 内建(built-in)函数 keccak256,sha256,ripemd160,ecrecover,addmod 和 mulmod 是允许的(即使他们确实会调用外部合约)。
-
注意:
- 不是所有类型的状态变量都支持用 constant 来修饰,当前支持的仅有值类型和字符串。
-
例子
pragma solidity ^0.5.0;
contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}
函数
函数基础
函数的基本语法
-
函数的语法:
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
-
函数支持与状态变量一样的修饰符。
函数参数
- 输入参数的声明方式与变量相同。但是有一个例外,未使用的参数可以省略参数名。
函数的返回值
-
输出参数的声明方式在关键词 returns 之后,与输入参数的声明方式相同。
-
输出参数名可以被省略。输出值也可以使用 return 语句指定。 return 语句也可以返回多值
-
返回的输出参数被初始化为 0;如果它们没有被显式赋值,它们就会一直为 0。
-
输入参数和输出参数可以在函数体中用作表达式。因此,它们也可用在等号左边被赋值。
-
返回多值:
- return (v0, v1, ..., vn)
- 必须与returns的返回值列表个数相同。
函数类型
- 函数分成三种:
- view函数(constant函数)
- pure函数
- fallback函数
view函数
-
保证不修改状态的函数需要声明成pure函数。下面的语句被认为是修改状态:
- 修改状态变量。
- 产生事件。
- 创建其它合约。
- 使用 selfdestruct。
- 通过调用发送以太币。
- 调用任何没有标记为 view 或者 pure 的函数。
- 使用低级调用。
- 使用包含特定操作码的内联汇编。
-
constant 是 view 的别名。
-
Getter 方法被标记为 view。
pure函数
-
pure 表示不读取或修改状态。除了上面解释的状态修改语句列表之外,以下被认为是从状态中读取:
- 读取状态变量。
- 访问 this.balance 或者 <address>.balance。
- 访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。
- 调用任何未标记为 pure 的函数。
- 使用包含某些操作码的内联汇编。
fallback函数
-
合约可以有一个未命名的函数。这个函数不能有参数也不能有返回值。
-
如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。
-
除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。 如果不存在这样的函数,则合约不能通过常规交易接收以太币。
-
在这样的上下文中,通常只有很少的 gas 可以用来完成这个函数调用(准确地说,是 2300 gas),所以使 fallback 函数的调用尽量廉价很重要。
-
注意:
- 调用 fallback 函数的交易(而不是内部调用)所需的 gas 要高得多,因为每次交易都会额外收取 21000 gas 或更多的费用,用于签名检查等操作。
- 请确保您在部署合约之前彻底测试您的 fallback 函数,以确保执行成本低于 2300 个 gas。
-
以下操作会消耗比 fallback 函数更多的 gas:
- 写入存储
- 创建合约
- 调用消耗大量 gas 的外部函数
- 发送以太币
-
fallback函数必须使用external修饰,而不是public。老版本是public。
-
fallback函数例子
pragma solidity >0.4.99 <0.6.0;
contract Test {
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the `payable`
// modifier.
function() external { x = 1; }
uint x;
}
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
function() external payable { }
}
函数重载
-
合约可以具有多个不同参数的同名函数。这也适用于继承函数。
-
重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。
-
重载解析和参数匹配
- 过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。
-
例子 :
pragma solidity ^0.5.0;
contract A {
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}
//调用 f(50) 会导致类型错误,因为 50 既可以被隐式转换为 uint8 也可以被隐式转换为 uint256。
// 另一方面,调用 f(256) 则会解析为 f(uint256) 重载,因为 256 不能隐式转换为 uint8。
附录
- 数据位置的指定非常重要,因为它们影响着赋值行为:
- 在 存储storage 和 内存memory 之间两两赋值,或者 存储storage 向状态变量(甚至是从其它状态变量)赋值都会创建一份独立的拷贝。
- 然而状态变量向局部变量赋值时仅仅传递一个引用,而且这个引用总是指向状态变量,因此后者改变的同时前者也会发生改变。
- 另一方面,从一个 内存memory 存储的引用类型向另一个 内存memory 存储的引用类型赋值并不会创建拷贝。














网友评论