本节是《Solidity by Example》的中文翻译与深入讲解,专为零基础或刚接触区块链开发的小白朋友打造。我们将通过“示例 + 解说 + 提示”的方式,带你逐步理解每一段 Solidity 代码的实际用途与背后的逻辑。
Solidity 是以太坊等智能合约平台使用的主要编程语言,就像写网页要用 HTML 和 JavaScript,写智能合约就需要会 Solidity。
如果你从没写过区块链代码也没关系,只要你了解一点点编程概念,比如“变量”“函数”“条件判断”,我们就能从最简单的例子开始,一步步建立你的 Solidity 编程思维。
User Defined Value Types
用户定义值类型
什么是用户定义值类型(UDVT)?
用户定义值类型(User Defined Value Types, UDVT)是 Solidity 0.8.8 及以上版本引入的一种特性,允许开发者基于内置值类型(如uint64)定义新的类型。
- 比喻:想象你有一个普通的“数字”类型(比如
uint64
),但你想给它取一个更有意义的名称,比如Duration
(持续时间)或Timestamp
(时间戳),以便代码更直观、更安全。UDVT 就像给数字穿上“标签”,让它们在语义上更清晰。 - 语法:
type NewType is BaseType
;NewType
:新定义的类型名称。BaseType
:基础类型,如uint64
、address
等。
- UDVT 提供两种操作:
NewType.wrap(value)
:将基础类型值包装为用户定义类型。NewType.unwrap(value)
:将用户定义类型解包为基础类型值。
- 比喻:想象你有一个普通的“数字”类型(比如
特点:
- 类型安全:UDVT 限制变量的用法,防止混淆不同类型的参数(比如把时间戳误用为持续时间)。
- 可读性:用
Duration
或Timestamp
代替uint64
,代码更直观。 - 零成本抽象:UDVT 在底层仍使用基础类型(如
uint64
),不会增加 Gas 成本。 - 不可扩展:UDVT 仅为类型别名,不能添加新功能。
用途:
- 提高代码可读性:用语义化的名称(如
Duration
)代替通用类型(如uint64
)。 - 增强类型安全:防止参数顺序错误或类型混淆。
- 简化复杂逻辑:结合库函数(如
LibClock
),处理复合数据(如时间戳和持续时间的组合)。
- 提高代码可读性:用语义化的名称(如
// SPDX-License-Identifier: MIT
// 使用 MIT 许可证,允许自由使用、修改和分发代码。
pragma solidity ^0.8.26;
// 指定 Solidity 编译器版本,必须为 0.8.26 或更高(但低于 0.9.0)。
// Code copied from optimism
// https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/dispute/lib/LibUDT.sol
// 代码来自 Optimism 项目,链接指向其 GitHub 仓库中的 LibUDT.sol 文件。
// Optimism 是一个以太坊二层解决方案,代码展示了 UDVT 在实际项目中的应用。
type Duration is uint64;
// 定义一个用户定义值类型 Duration,基于 uint64。
// 表示“持续时间”,语义上比 uint64 更清晰。
// 例如,Duration 表示时间段(如 3600 秒),而 uint64 只是普通数字。
type Timestamp is uint64;
// 定义一个用户定义值类型 Timestamp,基于 uint64。
// 表示“时间戳”,语义上表示某个时间点(如当前区块时间戳)。
type Clock is uint128;
// 定义一个用户定义值类型 Clock,基于 uint128。
// 表示一个复合类型,用于组合 Duration 和 Timestamp。
// uint128 提供足够空间存储两个 uint64(64 位 + 64 位 = 128 位)。
library LibClock {
// 定义一个名为 LibClock 的库,包含操作 Clock 类型的函数。
// 库是无状态的代码集合,函数为 internal 或 pure,不直接存储数据。
function wrap(Duration _duration, Timestamp _timestamp) internal pure returns (Clock clock_)
{
// 定义一个函数 wrap,接受 Duration 和 Timestamp 类型的参数,组合为 Clock 类型。
// internal:仅限合约内部或继承的合约调用。
// pure:不读取或修改区块链状态,链下计算,免费。
// 返回 Clock 类型,表示组合后的值。
assembly {
// 使用内联汇编(assembly)进行低级操作。
// data | Duration | Timestamp
// bit | 0 ... 63 | 64 ... 127
// Clock 是一个 128 位值,低 64 位存储 Timestamp,高 64 位存储 Duration。
clock_ := or(shl(0x40, _duration), _timestamp)
// shl(0x40, _duration):将 _duration 左移 64 位(0x40 = 64),占据高 64 位。
// or:将左移后的 _duration 和 _timestamp 按位或,合并为 128 位值。
// 结果存储在 clock_ 中,返回 Clock 类型。
}
}
function duration(Clock _clock) internal pure returns (Duration duration_)
{
// 定义一个函数 duration,从 Clock 类型中提取 Duration 值。
// internal:仅限内部调用。
// pure:不读取或修改区块链状态,免费。
// 返回 Duration 类型。
assembly {
duration_ := shr(0x40, _clock)
// shr(0x40, _clock):将 _clock 右移 64 位(0x40 = 64),提取高 64 位的 Duration 值。
// 结果存储在 duration_ 中,返回 Duration 类型。
}
}
function timestamp(Clock _clock) internal pure returns (Timestamp timestamp_)
{
// 定义一个函数 timestamp,从 Clock 类型中提取 Timestamp 值。
// internal:仅限内部调用。
// pure:不读取或修改区块链状态,免费。
// 返回 Timestamp 类型。
assembly {
timestamp_ := shr(0xC0, shl(0xC0, _clock))
// shl(0xC0, _clock):将 _clock 左移 192 位(0xC0 = 192),清除高 64 位。
// shr(0xC0, ...):再右移 192 位,提取低 64 位的 Timestamp 值。
// 结果存储在 timestamp_ 中,返回 Timestamp 类型。
}
}
}
library LibClockBasic {
// 定义一个名为 LibClockBasic 的库,展示不使用 UDVT 的版本。
// 与 LibClock 功能相同,但使用普通类型 uint64 和 uint128。
function wrap(uint64 _duration, uint64 _timestamp) internal pure returns (uint128 clock)
{
// 定义一个函数 wrap,接受两个 uint64 参数,组合为 uint128。
// internal:仅限内部调用。
// pure:不读取或修改区块链状态,免费。
// 返回 uint128 类型。
assembly {
clock := or(shl(0x40, _duration), _timestamp)
// 与 LibClock 的 wrap 逻辑相同,但参数和返回值是普通类型。
// 低 64 位存储 _timestamp,高 64 位存储 _duration。
}
}
}
contract Examples {
// 定义一个名为 Examples 的合约,展示 UDVT 和非 UDVT 的用法对比。
function example_no_uvdt() external view {
// 定义一个函数 example_no_uvdt,展示不使用 UDVT 的情况。
// external:仅限外部调用(通过交易或合约调用)。
// view:只读取区块链状态,不修改,链下调用免费。
// Without UDVT
// 不使用用户定义值类型
uint128 clock;
// 声明一个 uint128 类型的变量 clock,表示复合时间值。
uint64 d = 1;
// 声明一个 uint64 类型的变量 d,表示持续时间(1 秒)。
uint64 t = uint64(block.timestamp);
// 声明一个 uint64 类型的变量 t,表示当前区块时间戳(转换为 uint64)。
clock = LibClockBasic.wrap(d, t);
// 调用 LibClockBasic 的 wrap 函数,将 d 和 t 组合为 clock。
// 正确顺序:d(持续时间)在高位,t(时间戳)在低位。
// Oops! wrong order of inputs but still compiles
// 错误!参数顺序错误,但代码仍能编译
clock = LibClockBasic.wrap(t, d);
// 错误调用:将 t(时间戳)作为第一个参数,d(持续时间)作为第二个参数。
// 由于 LibClockBasic 使用普通类型 uint64,编译器无法检测这种错误。
// 运行时会导致逻辑错误:时间戳被当作持续时间,持续时间被当作时间戳。
}
function example_uvdt() external view {
// 定义一个函数 example_uvdt,展示使用 UDVT 的情况。
// external:仅限外部调用。
// view:只读取区块链状态,不修改,免费。
// Turn value type into user defined value type
// 将值类型转换为用户定义值类型
Duration d = Duration.wrap(1);
// 将 uint64 值 1 包装为 Duration 类型,表示持续时间 1 秒。
Timestamp t = Timestamp.wrap(uint64(block.timestamp));
// 将当前区块时间戳(block.timestamp)转换为 uint64,再包装为 Timestamp 类型。
// Turn user defined value type back into primitive value type
// 将用户定义值类型转换回基础值类型
uint64 d_u64 = Duration.unwrap(d);
// 将 Duration 类型解包为 uint64,得到原始值 1。
uint64 t_u64 = Timestamp.unwrap(t);
// 将 Timestamp 类型解包为 uint64,得到原始时间戳值。
// LibClock example
// LibClock 示例
Clock clock = Clock.wrap(0);
// 初始化 clock 为 0,包装为 Clock 类型。
clock = LibClock.wrap(d, t);
// 调用 LibClock 的 wrap 函数,将 d(Duration)和 t(Timestamp)组合为 clock。
// 正确顺序:d(持续时间)在高位,t(时间戳)在低位。
// Oops! wrong order of inputs
// 错误!参数顺序错误
// This will not compile
// 这将无法编译
// clock = LibClock.wrap(t, d);
// 错误调用:将 t(Timestamp)作为第一个参数,d(Duration)作为第二个参数。
// 由于 LibClock 的 wrap 函数要求第一个参数是 Duration,第二个是 Timestamp,
// 编译器会检测到类型不匹配,报错,防止逻辑错误。
}
}
代码包含三个部分:
- 用户定义值类型定义:定义了
Duration
、Timestamp
和Clock
三个类型,基于uint64
和uint128
。 - 库合约
LibClock
:使用 UDVT 操作复合数据(将Duration
和Timestamp
组合为Clock
)。 - 库合约
LibClockBasic
:不使用 UDVT 的对比版本,展示普通类型的用法。 - 合约
Examples
:展示 UDVT 和非 UDVT 的用法对比,突出类型安全的优势。
UDVT 的本质
- UDVT 是一种给基础类型(如
uint64
)取“别名”的方式,增加语义和类型安全。 - 比喻:想象你在超市买东西,
uint64
像一个普通的水瓶,UDVT 给它贴上标签,比如“牛奶”或“果汁”。标签让代码更清楚(牛奶不是果汁),还能防止误用(不会把果汁当牛奶喝)。 - 在底层,UDVT 仍使用基础类型(如
uint64
),不增加 Gas 成本,但编译器会严格检查类型匹配。 - 好处:
- 可读性:
Duration
比uint64
更直观,表示“持续时间”。 - 类型安全:防止参数顺序错误或类型混淆(如
Duration
和Timestamp
互换)。 - 零成本:UDVT 是编译时特性,不增加运行时 Gas 成本。
- 封装性:结合库(如
LibClock
),方便操作复杂数据。
- 可读性:
代码功能
- 类型定义:
Duration
:表示持续时间,基于uint64
。Timestamp
:表示时间戳,基于uint64
。Clock
:表示复合时间(持续时间 + 时间戳),基于uint128
。
- LibClock 库:
wrap(Duration, Timestamp)
:将Duration
和Timestamp
组合为Clock
,高 64 位存Duration
,低 64 位存Timestamp
。duration(Clock)
:从Clock
提取Duration
。timestamp(Clock)
:从Clock
提取Timestamp
。- 使用内联汇编(
assembly
)实现高效位操作。
- LibClockBasic 库:
- 与
LibClock
功能相同,但使用普通类型(uint64
和uint128
),没有类型安全保证。
- 与
- Examples 合约:
example_no_uvdt
:展示不使用 UDVT 的问题,参数顺序错误仍能编译,可能导致逻辑错误。example_uvdt
:展示使用 UDVT 的优势,参数顺序错误会导致编译错误,防止逻辑错误。
深入学习
- 存储与 Gas:
- UDVT 是编译时特性,底层使用基础类型(如
uint64
),不增加 Gas 成本。 LibClock
使用内联汇编(assembly
)进行位操作,效率高,但需谨慎确保正确性。- 读取(
pure
或view
函数)链下免费,适合复杂逻辑。
- UDVT 是编译时特性,底层使用基础类型(如
- 类型安全:
- UDVT 防止参数混淆(如
Duration
和Timestamp
),降低逻辑错误风险。 - 结合库函数(如
LibClock
),封装复杂操作,提高代码可维护性。
- UDVT 防止参数混淆(如
- 安全性:
- 内联汇编需仔细验证,防止位操作错误。
- 确保 UDVT 的基础类型(如
uint64
)适合用例(避免溢出)。
- 复杂用例:
- 时间管理:用
Duration
和Timestamp
管理区块链计时器。 - 资产管理:用
Amount
(基于uint256
)和Account
(基于address
)管理代币。
- 时间管理:用