Solidity v0.8.8 引入了用户定义值类型,作为一种在基本值类型上创建零成本抽象的方法,同时提高类型安全性和可读性。
动机
原始值类型的一个问题是它们不是很有描述性:它们只指定数据如何存储,而不指定如何解释。例如,人们可能希望使用uint128 来存储某个对象的价钱以及可用的数量。拥有更严格的类型规则来避免这两种不同概念的混淆非常有用。例如,可能希望禁止将数量分配给价格或反之。
解决此问题的一种方法是使用结构体。例如,价格和数量可以抽象为如下结构体
struct Price { uint128 price; } struct Quantity { uint128 quantity; } function toPrice(uint128 price) returns(Price memory) { return Price(price); } function fromPrice(Price memory price) returns(uint128) { return price.price; } function toQuantity(uint128 quantity) returns(Quantity memory) { return Quantity(quantity); } function fromQuantity(Quantity memory quantity) returns(uint128) { return quantity.quantity; }
但是,结构体是一种引用类型,因此始终指向内存、calldata 或存储中的某个值。这意味着上述抽象存在运行时开销,即与仅使用uint128 表示底层值相比,会产生额外的 gas。特别是,函数toPrice 和toQuantity 涉及将值存储在内存中。类似地,函数fromPrice 和fromQuantity 从内存中读取相应的值。这些函数共同将值从栈 -> 内存 -> 栈传递,从而浪费内存并产生运行时成本。用户定义值类型解决了此问题,它是基本值类型(例如uint8 或address)的抽象,没有任何额外的运行时开销。
用户定义值类型的语法
用户定义值类型使用type C is V; 定义,其中C 是新引入类型的名称,而V 必须是内置值类型(“底层类型”)。它们可以在合约内部或外部定义(包括库和接口)。函数C.wrap 用于从底层类型转换为自定义类型。类似地,函数C.unwrap 用于从自定义类型转换为底层类型。
回到动机部分的问题,可以使用以下内容替换结构体
pragma solidity ^0.8.8; type Price is uint128; type Quantity is uint128;
函数toPrice 和toQuantity 可以分别替换为Price.wrap 和Quantity.wrap。类似地,函数fromPrice 和fromQuantity 可以分别替换为Price.unwrap 和Quantity.unwrap。
此类值的表示数据继承自底层类型,并且底层类型也用于 ABI。这意味着以下两个transfer 函数将是相同的,即它们具有相同的函数选择器 以及相同的ABI 编码和解码。这允许以向后兼容的方式使用用户定义值类型。
pragma solidity ^0.8.8; type Decimal18 is uint256; interface MinimalERC20 { function transfer(address to, Decimal18 value) external; } interface AnotherMinimalERC20 { function transfer(address to, uint256 value) external; }
请注意,在上面的示例中,用户定义类型Decimal18 如何明确表示某个值应该表示一个小数点后 18 位的数字。
示例
以下示例说明了一个自定义类型UFixed,它表示一个小数定点类型,具有 18 位小数,以及一个用于对该类型执行算术运算的最小库。
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.8; // Represent a 18 decimal, 256 bit wide fixed point type // using a user defined value type. type UFixed is uint256; /// A minimal library to do fixed point operations on UFixed. library FixedMath { uint constant multiplier = 10**18; /// Adds two UFixed numbers. Reverts on overflow, /// relying on checked arithmetic on uint256. function add(UFixed a, UFixed b) internal pure returns (UFixed) { return UFixed.wrap(UFixed.unwrap(a) + UFixed.unwrap(b)); } /// Multiplies UFixed and uint256. Reverts on overflow, /// relying on checked arithmetic on uint256. function mul(UFixed a, uint256 b) internal pure returns (UFixed) { return UFixed.wrap(UFixed.unwrap(a) * b); } /// Take the floor of a UFixed number. /// @return the largest integer that does not exceed `a`. function floor(UFixed a) internal pure returns (uint256) { return UFixed.unwrap(a) / multiplier; } /// Turns a uint256 into a UFixed of the same value. /// Reverts if the integer is too large. function toUFixed(uint256 a) internal pure returns (UFixed) { return UFixed.wrap(a * multiplier); } }
请注意UFixed.wrap 和FixedMath.toUFixed 如何具有相同的签名,但执行两种非常不同的操作:UFixed.wrap 函数返回一个与输入具有相同数据表示的UFixed,而toUFixed 返回一个具有相同数值的UFixed。可以通过仅在定义类型的文件中使用wrap 和unwrap 函数来允许某种形式的类型封装。
运算符和类型规则
不允许对其他类型进行显式和隐式转换。
目前,没有为用户定义值类型定义任何运算符。特别是,甚至== 运算符也没有定义。但是,目前正在讨论允许使用运算符。为了简要展望应用程序,人们可能希望引入一种始终执行环绕算术的新整数类型,如下所示
/// Proposal on defining operators on user defined value types /// Note: this does not fully compile on Solidity 0.8.8; only a concept. type UncheckedInt8 is int8; function add(UncheckedInt8 a, UncheckedInt8 b) pure returns(UncheckedInt8) { unchecked { return UncheckedInt8.wrap(UncheckedInt8.unwrap(a) + UncheckedInt8.unwrap(b)); } } function addInt(UncheckedInt8 a, uint b) pure returns(UncheckedInt8) { unchecked { return UncheckedInt8.wrap(UncheckedInt8.unwrap(a) + b); } } using {add as +, addInt as +} for UncheckedInt8; contract MockOperator { UncheckedInt8 x; function increment() external { // This would not revert on overflow when x = 127 x = x + 1; } function add(UncheckedInt8 y) external { // Similarly, this would also not revert on overflow. x = x + y; } }
您可以在Solidity 论坛 和问题 #11969 中加入或关注此讨论。此外,您可以在问题 #11953 中加入或关注有关允许用户定义值类型的构造函数语法的讨论。