创建合约

可以通过以太坊交易“从外部”或从 Solidity 合约内部创建合约。

创建合约时, 合约的 构造函数 (一个用关键字 constructor 声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。

构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。

可见性和 getter 函数

状态变量有 3 种可见性:

  • public
    对于 public 状态变量会自动生成一个 getter hanshu 函数(见下面)。 以便其他的合约读取他们的值。 当在用一个合约里使用是,外部方式访问 (如: this.x) 会调用getter 函数,而内部方式访问 (如: x) 会直接从存储中获取值。 Setter函数则不会被生成,所以其他合约不能直接修改其值。
  • internal
    内部可见性状态变量只能在它们所定义的合约和派生合同中访问。 它们不能被外部访问。 这是状态变量的默认可见性。
  • private
    私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。

函数可见性

由于 Solidity 有两种函数调用:外部调用则会产生一个 EVM 调用,而内部调用不会, 更进一步, 函数可以确定器被内部及派生合约的可访问性,这里有 4 种可见性:

  • external
    外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。
  • public
    public 函数是合约接口的一部分,可以在内部或通过消息调用。
  • internal
    内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。 由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。
  • private
    private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

Getter 函数

编译器自动为所有 public 状态变量创建 getter 函数。对于下面给出的合约,编译器会生成一个名为 data 的函数, 该函数没有参数,返回值是一个 uint 类型,即状态变量 data 的值。 状态变量的初始化可以在声明时完成。

pragma solidity>=0.4.16 <0.9.0;contract C {uint public data = 42;}contract Caller {C c = new C();function f() public {uint local = c.data();}}

getter 函数具有外部(external)可见性。

如果你有一个数组类型的 public 状态变量,那么你只能通过生成的 getter 函数访问数组的单个元素。 这个机制以避免返回整个数组时的高成本gas。 可以使用如 myArray(0) 用于指定参数要返回的单个元素。 如果要在一次调用中返回整个数组,则需要写一个函数,例如:

pragma solidity >=0.4.0 <0.9.0;contract arrayExample {// public state variableuint[] public myArray;// 指定生成的Getter 函数/*function myArray(uint i) public view returns (uint) {return myArray[i];}*/// 返回整个数组function getArray() public view returns (uint[] memory) {return myArray;}}

现在可以使用 getArray() 获得整个数组,而 myArray(i) 是返回单个元素。

函数 修改器

使用 修改器 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修改器 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual.。

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.1 <0.9.0;contract owned {constructor() { owner = payable(msg.sender); }address owner;// 这个合约只定义一个修改器,但并未使用: 它将会在派生合约中用到。// 修改器所修饰的函数体会被插入到特殊符号 _; 的位置。// 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。modifier onlyOwner {require(msg.sender == owner,"Only owner can call this function.");_;}}contract destructible is owned {// 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其应用于 `destroy` 函数,// 只有在合约里保存的 owner 调用 `destroy` 函数,才会生效。function destroy() public onlyOwner {selfdestruct(owner);}}contract priced {// 修改器可以接收参数:modifier costs(uint price) {if (msg.value >= price) {_;}}}contract Register is priced, destructible {mapping (address => bool) registeredAddresses;uint price;constructor(uint initialPrice) { price = initialPrice; }// 在这里也使用关键字 `payable` 非常重要,否则函数会自动拒绝所有发送给它的以太币。function register() public payable costs(price) {registeredAddresses[msg.sender] = true;}function changePrice(uint price_) public onlyOwner {price = price_;}}contract Mutex {bool locked;modifier noReentrancy() {require(!locked,"Reentrant call.");locked = true;_;locked = false;}// 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用`f`。// `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。function f() public noReentrancy returns (uint) {(bool success,) = msg.sender.call("");require(success);return 7;}}

修改器 或函数体中显式的 return 语句仅仅跳出当前的 修改器 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修改器 中的定义的 _ 之后继续执行。

Constant 和 Immutable 状态变量

状态变量声明为 constant (常量)或者 immutable (不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。

对于 constant 常量, 他的值在编译器确定,而对于 immutable, 它的值在部署时确定。

// SPDX-License-Identifier: GPL-3.0pragma solidity >0.7.4;uint constant X = 32**22 + 8;contract C {string constant TEXT = "abc";bytes32 constant MY_HASH = keccak256("abc");uint immutable decimals;uint immutable maxBalance;address immutable owner = msg.sender;constructor(uint decimals_, address ref) {decimals = decimals_;// Assignments to immutables can even access the environment.maxBalance = ref.balance;}function isBalanceTooHigh(address _other) public view returns (bool) {return _other.balance > maxBalance;}}

函数

可以在合约内部和外部定义函数

合约之外的函数(也称为“自由函数”)始终具有隐式的 internal 可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.1 <0.9.0;function sum(uint[] memory arr) pure returns (uint s) {for (uint i = 0; i < arr.length; i++)s += arr[i];}contract ArrayExample {bool found;function f(uint[] memory arr) public {// This calls the free function internally.// The compiler will add its code to the contract.uint s = sum(arr);require(s >= 10);found = true;}}

返回变量

函数返回变量的声明方式在关键词 returns 之后,与参数的声明方式相同

pragma solidity >=0.4.16 <0.9.0;contract Simple {function arithmetic(uint a, uint b)publicpurereturns (uint sum, uint product){sum = a + b;product = a * b;}}

也可以使用 return 语句指定,使用 return 语句可以一个或多个值

pragma solidity >=0.4.16 <0.9.0;contract Simple {function arithmetic(uint a, uint b)publicpurereturns (uint sum, uint product){return (a + b, a * b);}}

返回多个值

当函数需要使用多个值,可以用语句 return (v0, v1, …, vn) 。 参数的数量需要和声明时候一致。

状态可变性

可以将函数声明为 view 类型,这种情况下要保证不修改状态

下面的语句被认为是修改状态:

修改状态变量。产生事件。创建其它合约。使用 selfdestruct。通过调用发送以太币。调用任何没有标记为 view 或者 pure 的函数。使用低级调用。使用包含特定操作码的内联汇编。
pragma solidity>=0.5.0 <0.9.0;contract C {function f(uint a, uint b) public view returns (uint) {return a * (b + 42) + block.timestamp;}}

Getter 方法自动被标记为 view

Pure 纯函数

函数可以声明为 pure ,在这种情况下,承诺不读取也不修改状态变量。

以下被认为是读取状态:

读取状态变量。访问 address(this).balance 或者 .balance。访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。调用任何未标记为 pure 的函数。使用包含某些操作码的内联汇编。
pragma solidity >=0.5.0 <0.9.0;contract C {function f(uint a, uint b) public pure returns (uint) {return a * (b + 42);}}

纯函数能够使用 revert() 和 require() 在 发生错误 时去还原潜在状态更改

特别的函数

receive 接收以太函数

一个合约最多有一个 receive 函数, 声明函数为:

receive() external payable { … }

不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有 修改器 。

在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive 函数. 例如 通过 .send() or .transfer() 如果 receive 函数不存在, 但是有payable 的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会调用.

如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).

更糟的是,receive 函数可能只有 2300 gas 可以使用(如,当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :

写入存储创建合约调用消耗大量 gas 的外部函数发送以太币

一个没有定义 fallback 函数或  receive 函数的合约,直接接收以太币(没有函数调用,即使用 send 或 transfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接收以太币,必须实现receive函数(使用 payable fallback 函数不再推荐,因为payable fallback功能被调用,不会因为发送方的接口混乱而失败)

Fallback 回退函数

合约可以最多有一个回退函数。函数声明为:

fallback () external [payable] 或 fallback (bytes calldata input) external [payable] returns (bytes memory output)

没有 function 关键字。 必须是 external 可见性,它可以是 virtual 的,可以被重载也可以有 修改器 。

如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数 时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。

fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为 payable 。

payable 的fallback函数也可以在纯以太转账的时候执行, 如果没有 receive 以太函数 推荐总是定义一个receive函数,而不是定义一个payable 的fallback函数。

函数重载

约可以具有多个不同参数的同名函数,称为“重载”(overloading),这也适用于继承函数。以下示例展示了合约 A 中的重载函数 f。

pragma solidity >=0.4.16 <0.9.0;contract A {function f(uint value) public pure returns (uint out) {out = value;}function f(uint value, bool really) public pure returns (uint out) {if (really)out = value;}}

重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。

两个 f 函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity 中被认为是不同的。

// 以下代码无法编译pragma solidity >=0.4.16 <0.9.0;contract A {function f(B value) public pure returns (B out) {out = value;}function f(address value) public pure returns (address out) {out = value;}}contract B {}

重载解析和参数匹配

通过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。

pragma solidity >=0.4.16 <0.9.0;contract A {function f(uint8 val) public pure returns (uint8 out) {out = val;}function f(uint256 val) public pure returns (uint256 out) {out = val;}}

调用 f(50) 会导致类型错误,因为 50 既可以被隐式转换为 uint8 也可以被隐式转换为 uint256。 另一方面,调用 f(256) 则会解析为 f(uint256) 重载,因为 256 不能隐式转换为 uint8。

事件 Events

Solidity 事件是EVM的日志功能之上的抽象。 应用程序可以通过以太坊客户端的RPC接口订阅和监听这些事件。

事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。 这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在(现在开始会被永久保存,在 Serenity 版本中可能会改动)。 日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。

如果外部实体需要该日志实际上存在于区块链中的证明,可以请求日志的Merkle证明. 但需要留意的是,由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。

事件成员

  • event.selector: 对于非匿名事件,这是一个 bytes32 值,包含事件签名的 keccak256 哈希值,在默认主题中使用。
pragma solidity>=0.4.21 <0.9.0;contract ClientReceipt {event Deposit(address indexed from,bytes32 indexed id,uint value);function deposit(bytes32 id) public payable {// 事件使用 emit 触发事件。// 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。emit Deposit(msg.sender, id, msg.value);}}

函数重写(Overriding)

父合约标记为 virtual 函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override 修饰。

重写函数只能将覆盖函数的可见性从 external 更改为 public 。

可变性可以按照以下顺序更改为更严格的一种: nonpayable 可以被 view 和 pure 覆盖。 view 可以被 pure 覆盖。 payable 是一个例外,不能更改为任何其他可变性。

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.0 <0.9.0;contract Base{function foo() virtual external view {}}contract Middle is Base {}contract Inherited is Middle{function foo() override public pure {}}

对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。

pragma solidity >=0.7.0 <0.9.0;contract Base1{function foo() virtual public {}}contract Base2{function foo() virtual public {}}contract Inherited is Base1, Base2{// 继承自两个基类合约定义的foo(), 必须显示的指定 overridefunction foo() public override(Base1, Base2) {}}

如果函数没有标记为 virtual , 那么派生合约将不能更改函数的行为(即不能重写)。

private 的函数是不可以标记为 virtual 的。

从 Solidity 0.8.8 开始, 在重写接口函数时不再要求 override 关键字,除非函数在多个父合约定义。

修改器重写

改器重写也可以被重写,工作方式和 函数重写 类似。 需要被重写的修改器也需要使用 virtual 修饰, override 则同样修饰重载,例如:

pragma solidity >=0.7.0 <0.9.0;contract Base{modifier foo() virtual {_;}}contract Inherited is Base{modifier foo() override {_;}}

构造函数

如果没有构造函数, 合约将假定采用默认构造函数, 它等效于 constructor() {} 。

// SPDX-License-Identifier: GPL-3.0pragma solidity >0.6.99 <0.8.0;abstract contract A {uint public a;constructor(uint a) {a = a;}}contract B is A(1) {constructor() {}}

基类构造函数的参数

// SPDX-License-Identifier: GPL-3.0pragma solidity >0.6.99 <0.8.0;contract Base {uint x;constructor(uint x) { x = x; }}// 直接在继承列表中指定参数contract Derived1 is Base(7) {constructor() {}}// 或通过派生的构造函数中用 修饰符 "modifier"contract Derived2 is Base {constructor(uint y) Base(y * y) {}}// or declare abstract...abstract contract Derived3 is Base {}// and have the next concrete derived contract initialize it.contract DerivedFromDerived is Derived3 {constructor() Base(10 + 10) {}}

多重继承与线性化

编程语言实现多重继承需要解决几个问题。 一个问题是 钻石问题。 Solidity 借鉴了 Python 的方式并且使用“ C3 线性化 ”强制一个由基类构成的 DAG(有向无环图)保持一个特定的顺序。 这最终反映为我们所希望的唯一化的结果,但也使某些继承方式变为无效。尤其是,基类在 is 后面的顺序很重要:列出基类合约的 顺序从 “最基类” 到 “最派生类” 。请注意, 此顺序与 Python 中使用的顺序相反。

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.0 <0.8.0;contract X {}contract A is X {}// 编译出错contract C is A, X {}

代码编译出错的原因是 C 要求 X 重写 A (因为定义的顺序是 A, X ), 但是 A 本身要求重写 X,无法解决这种冲突。

可以通过一个简单的规则来记忆: 以从“最接近的基类”(most base-like)到“最远的继承”(most derived)的顺序来指定所有的基类。

当继承层次结构中有多个构造函数时,继承线性化特别重要。 构造函数将始终以线性化顺序执行,无论在继承合约的构造函数中提供其参数的顺序如何。 例如:

// SPDX-License-Identifier: GPL-3.0pragma solidity >0.6.99 <0.8.0;contract Base1 {constructor() {}}contract Base2 {constructor() {}}//构造函数以以下顺序执行://1 - Base1//2 - Base2//3 - Derived1contract Derived1 is Base1, Base2 {constructor() Base1() Base2() {}}// 构造函数以以下顺序执行://1 - Base2//2 - Base1//3 - Derived2contract Derived2 is Base2, Base1 {constructor() Base2() Base1() {}}// 构造函数仍然以以下顺序执行://1 - Base2//2 - Base1//3 - Derived3contract Derived3 is Base2, Base1 {constructor() Base1() Base2() {}}

抽象合约

如果未实现合约中的至少一个函数,则必须将合约标记为 abstract。 即使实现了所有功能,合约也可能被标记为abstract。

当合约中至少有一个函数没有被实现,或者合约没有为其所有的基类合约构造函数提供参数时,必须将合约标记为 abstract 。 即使不是这种情况,合约仍然可以被标记为抽象的,例如,当你不打算直接创建合约时。抽象合约类似于 接口 ,但是interface可以声明的内容更加有限。

如下例所示,可以使用关键字 abstract 定义抽象合约合约, 由于 utterance() 函数没有具体的实现(没有实现体 { } , 而是以 ; 结尾:

pragma solidity >=0.6.0 <0.9.0;abstract contract Feline {function utterance() public returns (bytes32);}

这样的抽象合约不能直接实例化。 如果抽象合约本身确实都有实现所有定义的函数,也是正确的。 下例显示了抽象合约作为基类的用法:

pragma solidity >=0.6.0 <0.9.0;abstract contract Feline {function utterance() public pure returns (bytes32);}contract Cat is Feline {function utterance() public pure returns (bytes32) { return "miaow"; }}

接口

接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:

无法继承其他合约,不过可以继承其他接口。接口中所有的函数都需要是 external,尽管在合约里可以是public无法定义构造函数。无法定义状态变量。不可以声明修改器。

将来可能会解除这里的某些限制。

pragma solidity >=0.6.2 <0.9.0;interface Token {enum TokenType { Fungible, NonFungible }struct Coin { string obverse; string reverse; }function transfer(address recipient, uint amount) external;}
pragma solidity >=0.6.2 <0.9.0;interface ParentA {function test() external returns (uint256);}interface ParentB {function test() external returns (uint256);}interface SubInterface is ParentA, ParentB {// 必须重新定义 test 函数,以表示兼容父合约含义function test() external override(ParentA, ParentB) returns (uint256);}

库与合约类似,库的目的是只需要在特定的地址部署一次,而它们的代码可以通过 EVM 的 DELEGATECALL (Homestead 之前使用 CALLCODE 关键字)特性进行重用。

这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即 this 指向调用合约,特别注意,他访问的是调用合约存储的状态。 因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。

因为我们假定库是无状态的,所以如果它们不修改状态(如果它们是 view 或者 pure 函数),库函数仅能通过直接调用来使用(即不使用 DELEGATECALL 关键字), 特别是,任何库不可能被销毁。

pragma solidity >=0.6.0 <0.9.0;// 我们定义了一个新的结构体数据类型,用于在调用合约中保存数据。struct Data {mapping(uint => bool) flags;}library Set {// 注意第一个参数是“storage reference”类型,因此在调用中参数传递的只是它的存储地址而不是内容。// 这是库函数的一个特性。如果该函数可以被视为对象的方法,则习惯称第一个参数为 `self`function insert(Data storage self, uint value)publicreturns (bool){if (self.flags[value])return false; // 已经存在self.flags[value] = true;return true;}function remove(Data storage self, uint value)publicreturns (bool){if (!self.flags[value])return false; // 不存在self.flags[value] = false;return true;}function contains(Data storage self, uint value)publicviewreturns (bool){return self.flags[value];}}contract C {Data knownValues;function register(uint value) public {// 不需要库的特定实例就可以调用库函数,// 因为当前合约就是“instance”。require(Set.insert(knownValues, value));}// 如果我们愿意,我们也可以在这个合约中直接访问 knownValues.flags。}

与合约相比,库的限制:

没有状态变量不能够继承或被继承不能接收以太币不可以被销毁

库的函数签名与选择器

外部库函数比外部合约函数支持更多的参数类型,例如递归结构和指向存储的指针。

因此,计算用于计算4字节选择器的函数签名遵循内部命名模式以及可对合约ABI中不支持的类型的参数使用内部编码。

与合约 ABI 相似,选择器由签名的Keccak256哈希的前四个字节组成。可以使用 .selector 成员从Solidity中获取其值,如下所示:

pragma solidity >=0.5.14 <0.9.0;library L {function f(uint256) external {}}contract C {function g() public pure returns (bytes4) {return L.f.selector;}}

Using For

在当前的合约上下里, 指令 using A for B; 可用于附加库函数(从库 A)到任何类型( B)作为成员函数。 这些函数将接收到调用它们的对象作为它们的第一个参数(像 Python 的 self 变量)。

Using For 可在文件或合约内部及合约级都是有效的。

第一部分 A 可以是以下之一:

一些库或文件级的函数列表(using {f, g, h, L.t} for uint;), 仅是那些函数被附加到类型。库名称 (using L for uint;) ,库里所有的函数(包括 public 和 internal 函数) 被附加到类型上。

在文件级,第二部分 B 必须是一个显式类型(不用指定数据位置)

在合约内,你可以使用 using L for *;, 表示库 L 中的函数被附加在所有类型上。

using A for B; 指令仅在当前作用域有效(要么是合约中,或当前模块、或源码单元),包括在作用域内的所有函数,在合约或模块之外则无效。

当 using for 指令在文件级别使用,并应用于一个用户定义类型(在用一个文件定义的文件级别的用户类型), global 关键字可以添加到末尾。 产生的效果是,这些函数被附加到使用该类型的任何地方(包括其他文件),而不仅仅是声明处所在的作用域。

struct Data { mapping(uint => bool) flags; }// Now we attach functions to the type.// The attached functions can be used throughout the rest of the module.// If you import the module, you have to// repeat the using directive there, for example as// import "flags.sol" as Flags;// using {Flags.insert, Flags.remove, Flags.contains}// for Flags.Data;using {insert, remove, contains} for Data;function insert(Data storage self, uint value)returns (bool){if (self.flags[value])return false; // already thereself.flags[value] = true;return true;}function remove(Data storage self, uint value)returns (bool){if (!self.flags[value])return false; // not thereself.flags[value] = false;return true;}function contains(Data storage self, uint value)viewreturns (bool){return self.flags[value];}contract C {Data knownValues;function register(uint value) public {// Here, all variables of type Data have// corresponding member functions.// The following function call is identical to// `Set.insert(knownValues, value)`require(knownValues.insert(value));}}