Solidity programming essentials-编写智能合约

本章内容

在本章中,您将开始编写真正的智能合约。它将讨论智能合约的设计方面,包括定义和实现合约,以及使用不同的机制(例如使用 new关键字和使用已知地址)部署和创建合约。

Solidity提供了丰富的面向对象特性,本章将深入探讨面向对象的概念和实现,例如继承、多重继承、声明抽象类和接口,以及为抽象函数和接口提供方法实现。

Solidity用于编写智能合约。本章专门介绍智能合约。您将从这里开始编写智能合约。本章将讨论智能合约的设计方面,包括合约的定义和实现,以及使用不同的机制(例如使用新的关键字和已知地址)来部署和创建合约。Solidity 提供了丰富的

面向对象特性,本章将深入探讨面向对象的概念和实现,例如继承、多重继承、声明抽象类和接口,以及为抽象函数和接口提供方法实现。

本章涵盖以下主题:

Creating contracts
Creating contracts via new
Inheritance
Abstract contracts
Interfaces

智能合约

什么是智能合约?每个人都试图理解合约的含义以及“智能”一词在合约语境中的意义,但往往一头雾水。智能合约本质上是部署在EVM(执行虚拟机)中的代码段或程序。合约一词通常用于法律领域,在编程领域意义不大。用Solidity编写智能合约并不意味着编写一份具有法律效力的合同。

此外,合约与其他任何程序代码一样,包含Solidity代码,并在被调用时执行。它本身并不智能。智能合约是区块链术语,指的是在EVM中执行的编程逻辑和代码。

智能合约与 C++、Java 或 C# 类非常相似。正如类由状态(变量)和行为(方法)组成一样,合约包含状态变量和函数。状态变量的作用是维护合约的当前状态,而函数负责执行逻辑并对当前状态进行更新和读取操作。

我们在上一章已经看到了一些智能合约的示例;

现在是时候深入探讨这个主题了。

编写一个简单的合约

合约使用 contract 关键字和一个标识符来声明,如下面的代码片段所示:

contract SampleContract { }

在括号内是状态变量和函数定义的声明。合约的完整定义已在第 3 章“Solidity 简介”中讨论过,我再次提供该定义,以便快速参考。

此合约包含状态变量、结构体、定义、枚举声明、函数定义、修饰符和事件。

状态变量、结构体和枚举已在第 4 章“全局变量和函数”中详细讨论过。

函数、修饰符和事件将在接下来的两章中详细讨论。

创建合约

在 Solidity 中,创建和使用合约有两种方法:

使用 new 关键字

使用已部署合约的地址

使用 new 关键字

Solidity 中的 new 关键字用于部署并创建一个新的合约实例。它通过部署合约、初始化状态变量、运行构造函数、将 nonce 值设置为 1 来初始化合约实例,最终将实例地址返回给调用者。部署合约包括检查请求者是否提供了足够的 gas 来完成部署,使用请求者的地址和 nonce 值生成一个新的账户/地址用于合约部署,并将随附的任何以太币传递给该账户/地址。

在下一张截图中,定义了两个合约:HelloWorld 和 client。
在这种情况下,一个合约(client)部署并创建了另一个合约(HelloWorld)的一个新实例。
它使用 new 关键字来实现这一点,如下面的代码片段所示:

HelloWorld myObj = new HelloWorld();

使用已部署合约的地址

当合约已部署并实例化时,可以使用此方法创建合约实例。此方法使用已部署合约的地址。不会创建新的实例;而是重用现有实例。
通过地址创建对现有合约的引用。

在下一个代码示例中,定义了两个合约:HelloWorld 和 client。

在这种情况下,一个合约(client)使用另一个合约(HelloWorld)的已知地址来创建对其的引用。

它使用地址数据类型并将实际地址强制转换为 HelloWorld 合约类型。
myObj 对象包含现有合约的地址,

如下面的代码片段所示:

构造函数(constructor)

Solidity 支持在合约中声明构造函数。

构造函数在 Solidity 中是可选的。如果开发者没有显式定义构造函数,编译器会自动生成一个默认构造函数。

构造函数在部署合约时执行一次。这与其他编程语言截然不同。
在其他语言中,每当创建一个对象实例时,构造函数都会被执行一次。

然而在 Solidity 中,构造函数仅在合约被部署到 EVM 的那一刻执行。构造函数通常用于初始化状态变量,并且一般应避免在其中书写过多或过于复杂的 Solidity 代码。

构造函数代码是一个合约生命周期中最先被执行的代码。
一个合约最多只能有一个构造函数,这与某些支持多个构造器重载的编程语言不同。
构造函数可以接受参数,这些参数应在部署合约时提供。

构造函数的名称应当与合约名称相同,两者必须一致。(注:这是 Solidity 0.4.x 的旧语法。新版已改为 constructor()。)
从可见性角度说,构造函数可以是 public 或 internal,但不能是 external 或 private。

(现代 Solidity 的构造函数不写可见性修饰符,默认是 public。)
构造函数不能显式返回任何数据。

在下面的示例中,定义了一个与 HelloWorld 合约同名的构造函数。它将存储变量 value 设置为 5。

当然可以 😊
下面是一个完整、适合在 Remix IDE 中直接运行的教学合约,
用于演示构造函数(constructor)的作用、参数传入、以及初始化状态变量的过程。


🧩 文件名:HelloWorld.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title Constructor 教学合约
/// @notice 演示 constructor 的参数传递与状态变量初始化
contract HelloWorld {
string public name;
uint public value;
address public owner;

// 构造函数:部署时自动执行一次
constructor(string memory _name, uint _value) {
name = _name; // 初始化字符串
value = _value; // 初始化数值
owner = msg.sender; // 记录部署者地址
}

// 普通函数:修改状态变量(可手动调用)
function updateValue(uint newValue) public {
require(msg.sender == owner, "Only owner can update value");
value = newValue;
}

// 只读函数:返回合约当前信息
function getInfo() public view returns (string memory, uint, address) {
return (name, value, owner);
}
}


🧠 学习要点解析

概念 说明
constructor 构造函数,在合约部署时自动执行,仅执行一次。
_name, _value 部署时传入的参数,用来初始化状态变量。
msg.sender 部署合约的账户地址,会被存入 owner
require 确保只有合约部署者可以更新 value
view 表示只读函数,不会修改链上数据。

🧪 Remix 测试步骤

  1. 打开 Remix IDE

  2. 新建文件 HelloWorld.sol,粘贴上方代码

  3. 选择编译器版本 0.8.24 或更高

  4. 点击 Deploy & Run Transactions

    • 在部署栏输入:
      name = "Solidity Demo"
      value = 5
  5. 点击 Deploy

部署成功后,你可以在 Deployed Contracts 看到:

1
2
3
name()  → Solidity Demo
value()5
owner() → 你的地址(msg.sender)

🧭 可选扩展:多个构造函数的历史写法(仅了解)

旧版本 Solidity(0.4.x 以前)用“同名函数”来定义构造函数:

1
2
3
4
5
6
7
contract HelloWorld {
uint value;

function HelloWorld(uint _v) public {
value = _v;
}
}

在现代版本(≥0.5.0)中,这种写法已被弃用,
统一使用 constructor(...) {} 语法。

合约组合(Contract composition)

Solidity 支持合约组合。所谓“组合”,是指将多个合约或数据类型组合在一起,以创建更复杂的数据结构和合约。

我们之前已经看到过许多合约组合的例子。例如,在本章前面展示的、使用 new 关键字创建合约的代码片段中。

在 Solidity 中,将复杂问题拆解为多个合约,再通过合约组合将它们组织起来,是一种良好的实践。

当然可以 👍 我们可以把“父子关系”改成“继承关系”“上层与下层合约关系”,这样就更中性、专业,也没有性别色彩。下面是改写后的版本:


继承(Inheritance)

继承是面向对象编程的核心概念之一,Solidity 同样支持智能合约之间的继承。

所谓继承,是指通过继承关系定义多个相互关联的合约。
被继承的合约称为基合约(base contract)
而继承它的合约称为派生合约(derived contract)

继承的主要目的在于代码复用(code reusability)
基合约与派生合约之间存在一种 “is-a”(是一个)关系
并且所有 publicinternal 作用域的函数与状态变量都可以被派生合约访问。

实际上,Solidity 编译器会将基合约的字节码复制到派生合约的字节码中。
在派生合约中使用关键字 is 来继承基合约。

继承是每一位 Solidity 开发者必须掌握的重要概念,
因为它直接影响合约的版本管理部署方式

Solidity 支持多种形式的继承,包括多重继承(multiple inheritance)
编译时,Solidity 会将所有基合约的内容复制进派生合约,
最终生成一个完整的合约,并由系统分配一个唯一地址
供所有具备继承关系的合约共享使用。

单一继承(Single inheritance)

单一继承是指一个派生合约(derived contract)只继承一个基合约(base contract)
通过单一继承,派生合约可以继承基合约中的变量、函数、修饰器(modifier)以及事件(event)
从而实现代码复用与逻辑扩展。

可以想象成:“下层合约在上层合约的基础上继续发展功能”。


Solidity 示例:单一继承(Single Inheritance)

下面这个例子展示了一个简单的单一继承结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 基合约(Base Contract)
contract HelloWorld {
string public message;

// 构造函数
constructor(string memory _msg) {
message = _msg;
}

// 公共函数
function sayHello() public view returns (string memory) {
return message;
}
}

// 派生合约(Derived Contract)
// 使用 is 关键字继承 HelloWorld 合约
contract HelloClient is HelloWorld {
// 派生合约的构造函数
constructor(string memory _msg) HelloWorld(_msg) {}

// 新增函数:调用基合约函数
function greet() public view returns (string memory) {
return string(abi.encodePacked("Client says: ", sayHello()));
}
}

🧩 说明

  • HelloWorld基合约,定义了一个状态变量 message 和一个函数 sayHello()
  • HelloClient派生合约,通过 is HelloWorld 继承了 HelloWorld 的所有 publicinternal 成员。
  • HelloClient 的构造函数中,通过 HelloWorld(_msg) 显式调用了基合约的构造函数。
  • HelloClient 还定义了自己的新函数 greet(),展示了在继承基础上扩展功能的用法。

🧠 总结:

单一继承让一个合约可以完整继承另一个合约的逻辑与数据,
既避免重复编写代码,又方便在此基础上扩展新特性。

Multi-level多层继承

多层继承与单层继承非常相似;不同的是,它不是只有一层“基合约与派生合约”的关系,而是存在多个层级的继承结构。
换句话说,一个合约可以继承自另一个,而那个合约又继承自更上层的合约,形成继承链。

1
2
3
4
5
6
contract A {}

contract B is A {}

contract C is B {}


🧩 Solidity 示例:多层继承(Multi-level Inheritance)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// 🌱 最上层:基础合约
contract Base {
string internal message;

function setMessage(string memory _msg) public {
message = _msg;
}

function getMessage() public view returns (string memory) {
return message;
}
}

// 🌿 中间层:扩展合约
contract Intermediate is Base {
function addExclamation() public {
message = string(abi.encodePacked(message, "!"));
}
}

// 🌻 最底层:最终合约
contract Final is Intermediate {
function addSignature(string memory _sig) public {
message = string(abi.encodePacked(message, " - ", _sig));
}
}

🧠 运行说明(在 Remix 中)


🧭 结构图

1
2
Base  →  Intermediate  →  Final
(基础合约) (中间层合约) (最终合约)

层级继承(Hierarchical inheritance)与简单继承类似;

不过,在这种继承中,一个合约同时作为多个其他合约的基础合约

如下图所示:
合约 A 被同时继承在合约 B 和合约 C 中。

1
2
3
4
5
6
contract A {}

contract B is A {}

contract C is A {}

多重继承

Solidity 支持多重继承。可以存在多级单继承(即一个合约继承自另一个合约,然后再被另一个合约继承)。然而,也可以有多个合约从同一个基合约派生出来。这些派生合约可以在进一步的子合约中作为基合约一起使用。当合约同时继承自这些子合约时,就形成了多重继承,如下图所示:

✅ 小结

类型 示例结构 含义
单层继承 Base → Derived 一层扩展关系
多层继承 Base → Intermediate → Final 多个层级连续扩展
多重继承 A, B → C 从多个基础合约继承
菱形继承 A→B, A→C, B+C→D 需要线性化解决重复继承问题

封装

封装是面向对象编程(OOP)最重要的支柱之一。
封装指的是隐藏状态变量或控制对其的直接访问,以改变其状态的过程。它指的是一种模式,即声明变量,使客户端无法直接访问这些变量,只能通过函数进行修改。这有助于对变量访问进行约束,同时又允许类提供足够的访问接口以便执行操作。Solidity 提供了多种可见性修饰符(visibility modifiers),如 externalpublicinternalprivate,它们会影响状态变量在合约内部、继承的子合约或外部合约中的可见性。