انواع مقدار تعریف شده توسط کاربر در سالیدیتی

انواع مقدار تعریف شده توسط کاربر در سالیدیتی
انواع مقدار تعریف شده توسط کاربر در سالیدیتی

سالیدیتی ورژن 0.8.8 انواع مقادیر تعریف شده توسط کاربر (user defined value types) را به عنوان ابزاری برای ایجاد abstractionهایی با هزینه صفر بر روی یک نوع مقدار اولیه معرفی می‌کند که همچنین ایمنی نوع را افزایش داده و خوانایی را بهبود می‌بخشد.

انگیزه

یک مشکل در مورد انواع مقادیر اولیه این است که آنها بسیار توصیفی نیستند: آنها فقط نحوه ذخیره داده‌ها را مشخص می‌کنند و نحوه تفسیر آنها را مشخص نمی‌کنند. به عنوان مثال، ممکن است بخواهید از uint128 برای ذخیره قیمت برخی اجسام و همچنین مقدار موجود استفاده کنید. برای جلوگیری از درهم آمیختن دو مفهوم متفاوت، داشتن قوانین نوع سختگیرانه بسیار مفید است. به عنوان مثال، ممکن است بخواهید که مقدار را به قیمت اختصاص دهید یا برعکس.

یکی از گزینه‌های حل این مسئله استفاده از ساختارها است. به عنوان مثال، قیمت و مقدار را می‌توان به صورت ساختار به شرح زیر خلاصه کرد:

struct Price { uint128 price; }
struct Quantity { uint128 quantity; }
function toPrice(uint128 price) returns(Price memory) {
returnPrice(price);
}
function fromPrice(Price memory price) returns(uint128) {
returnprice.price;
}
function toQuantity(uint128 quantity) returns(Quantity memory) {
returnQuantity(quantity);
}
function fromQuantity(Quantity memory quantity) returns(uint128) {
returnquantity.quantity;
}

با این حال، struct یک نوع مرجع است و بنابراین همیشه به یک مقدار در memory ، calldata یا storage اشاره می‌کند. این بدان معناست که abstraction فوق دارای سربار زمان اجرا است، یعنی گَس اضافی در مقایسه با استفاده از فقط uint128 برای نشان دادن مقدار زیرین. به طور خاص، توابع toPrice و toQuantity شامل ذخیره مقدار در memory می‌شوند. به طور مشابه، توابع fromPrice و fromQuantity مقدار مربوطه را از memory می‌خوانند. با هم، این توابع مقدار stack -> memory -> stack را که memory را هدر می‌دهد و هزینه زمان اجرا را متحمل می شود، منتقل می‌کنند. این مسئله با انواع مقادیر تعریف شده توسط کاربر، که abstractionای از انواع مقادیر اولیه هستند (مانند uint8 یا آدرس)، بدون هیچ گونه هزینه اضافی برای زمان اجرا، حل می‌شود.

سینتکس برای انواع ارزش تعریف شده توسط کاربر

نوع مقدار تعریف شده توسط کاربر با استفاده از type C is V; تعریف می‌شود ؛ جایی که C نام نوع تازه معرفی شده است و V باید یک نوع مقدار داخلی باشد ("نوع اصلی"). آنها می توانند در داخل یا خارج از قرارداد (از جمله کتابخانه ها و رابط ها) تعریف شوند. تابع C.wrap برای تبدیل از نوع اصلی (underlying type) به نوع سفارشی (custom type) استفاده می‌شود. به طور مشابه، تابع 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 جایگزین شوند.

نمایش داده‌های (data-representation) مقادیر این نوع‌ها از نوع اصلی به ارث می‌رسد و نوع اصلی نیز در ABIاستفاده می‌شود. این بدان معناست که دو تابع transfer زیر یکسان هستند، یعنی دارای تابع انتخاب کننده ( function selector) یکسان و رمزگذاری و رمزگشایی 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 در فایلی‌ که نوع را مشخص می‌کند، می‌توان به فرمی از نوع کپسوله سازی (type-encapsulation) اجازه داد.

اپراتورها و قوانین نوع

تبدیل صریح و ضمنی به انواع و از انواع دیگر ممنوع است.

در حال حاضر، هیچ نوع عملگر برای انواع مقادیر تعریف شده توسط کاربر تعریف نشده است. به طور خاص، حتی اپراتور == تعریف نشده است. با این حال، در حال حاضر اجازه دادن به اپراتورها در حال بحث است. برای ارائه یک چشم انداز کوتاه در مورد برنامه های کاربردی، ممکن است بخواهید یک نوع عدد صحیح جدید را معرفی کنید که همیشه محاسبه wrapping را به شرح زیر انجام می دهد:

/// 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;
}
}