{ 跳至内容 }

Solidity 中的用户定义值类型

发布者:Solidity 团队 于 2021 年 9 月 27 日

解释器

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。特别是,函数toPricetoQuantity 涉及将值存储在内存中。类似地,函数fromPricefromQuantity 从内存中读取相应的值。这些函数共同将值从栈 -> 内存 -> 栈传递,从而浪费内存并产生运行时成本。用户定义值类型解决了此问题,它是基本值类型(例如uint8address)的抽象,没有任何额外的运行时开销。

用户定义值类型的语法

用户定义值类型使用type C is V; 定义,其中C 是新引入类型的名称,而V 必须是内置值类型(“底层类型”)。它们可以在合约内部或外部定义(包括库和接口)。函数C.wrap 用于从底层类型转换为自定义类型。类似地,函数C.unwrap 用于从自定义类型转换为底层类型。

回到动机部分的问题,可以使用以下内容替换结构体

pragma solidity ^0.8.8;

type Price is uint128;
type Quantity is uint128;

函数toPricetoQuantity 可以分别替换为Price.wrapQuantity.wrap。类似地,函数fromPricefromQuantity 可以分别替换为Price.unwrapQuantity.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.wrapFixedMath.toUFixed 如何具有相同的签名,但执行两种非常不同的操作:UFixed.wrap 函数返回一个与输入具有相同数据表示的UFixed,而toUFixed 返回一个具有相同数值的UFixed。可以通过仅在定义类型的文件中使用wrapunwrap 函数来允许某种形式的类型封装。

运算符和类型规则

不允许对其他类型进行显式和隐式转换。

目前,没有为用户定义值类型定义任何运算符。特别是,甚至== 运算符也没有定义。但是,目前正在讨论允许使用运算符。为了简要展望应用程序,人们可能希望引入一种始终执行环绕算术的新整数类型,如下所示

/// 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 中加入或关注有关允许用户定义值类型的构造函数语法的讨论。

上一篇

下一篇

参与进来

GitHub

Twitter

Mastodon

Matrix

了解更多

博客文档使用案例贡献关于论坛

2024Solidity 团队

安全策略

行为准则