انواع (Types)
سالیدیتی یک زبان نوع استاتیک است، به این معنی که نوع هر متغیر state) و (local باید مشخص شود. سالیدیتی چندین نوع اصلی را ارائه میدهد که میتوانند با هم ترکیب شوند و انواع پیچیدهای را تشکیل دهند.
علاوه بر این، انواع میتوانند در عبارات حاوی عملگر با یکدیگر تعامل داشته باشند. برای مراجعه سریع به عملگرهای مختلف، به بخش ترتیب تقدم عملگرها مراجعه کنید.
مفهوم مقادیر ” undefined ” یا ” null ” در سالیدیتی وجود ندارد، اما متغیرهای تازه اعلام شده همیشه یک مقدار پیش فرض وابسته به نوع آنها دارند. برای استفاده از مقادیر غیر منتظره، باید از تابع revert استفاده کنید تا کل تراکنش را برگردانید، یا یک تاپل را با مقدار bool دوم نشان دهید که موفقیت را نشان میدهد.
انواع مقدار
انواع زیر را نیز انواع مقدار مینامند زیرا متغیرهای این نوعها همیشه از نظر مقدار منتقل میشوند، یعنی وقتی که به عنوان آرگومان تابع یا در “انتسابها ” استفاده میشوند، همیشه کپی میشوند.
بولین
bool : مقادیر ممکن ثابتهای true و false هستند.
عملگرها:
- ! (logical negation)
- && (”logical conjunction, “and)
- || (”logical disjunction, “or)
- == (equality)
- != (inequality)
اپراتورهای || و && قوانین متداول اتصال کوتاه را اعمال میکنند. این بدان معنی است که در عبارت f(x) || g(y) ، اگر f(x) به صورت true ارزیابی شود، g(y) حتی اگر دارای عوارض جانبی باشد نیز ارزیابی نخواهد شد.
عدد صحیح
int / uint : عددهای صحیح با علامت و بدون علامت در اندازه های مختلف. کلمات کلیدی uint8 تا uint256 در گامهای 8 (بدون علامت 8 تا 256 بیت) و int8 تا int256 . uint و int به ترتیب نام مستعار برای uint256 و int256 هستند.
- مقایسهگرها : <=، <، ==، !=، >=، > (ارزیابی به bool)
- عملگرهای بیت: &، |، ^ (انحصاری بیتی یا)، ~ (نفی بیتی)
- عملگرهای Shift) << : Shift چپ)، >> (Shift راست)
- عملگرهای حسابی : +, –، unary – ( فقط برای اعداد صحیح با علامت) ، *، /، % (مدول) ، **(توان)
برای یک عدد صحیح نوع X، میتوانید از type(X).min و type(X).max برای دستیابی به حداقل و حداکثر مقدار قابل نمایش توسط نوع استفاده کنید.
هشدار
عدد صحیح در سالیدیتی به محدوده خاصی محدود میشود. به عنوان مثال، با uint32، این 0 تا 1 – 3**2 است. دو حالت وجود دارد که عملیات حسابی در این نوع انجام میشود: حالت ” wrapping” یا “unchecked ” و حالت .” checked “به طور پیش فرض، عملیات حسابی همیشه ” checked” میشود، به این معنی که اگر نتیجه عملیاتی خارج از محدوده مقدار نوع باشد، فراخوانی با یک ادعای ناموفق برگردانده میشود. میتوانید با استفاده از { … } unchecked به حالت “unchecked” تغییر دهید. جزئیات بیشتر را میتوانید در بخش “unchecked” مشاهده کنید.
مقایسه
مقدار مقایسه، مقداری است که از مقایسه مقدار صحیح بدست میآید.
مقایسهگرها
مقدار مقایسهگر، مقداری است که با مقایسه مقدار عدد صحیح بدست میآید. عملیات بیت (Bit operations) عملیات بیت بر روی نمایش مکمل دو انجام میشود. این بدان معنی است که به عنوان مثالint256(0) == int256(-1)~.
شیفتها
شیفتها نتیجه یک عمل جابجایی دارای نوع عملوند سمت چپ میباشند و نتیجه را متناسب با نوع آن کوتاه میکنند. عملوند سمت راست باید از نوع بدون علامت باشد. تلاش برای جابجایی با نوع با علامت خطای کامپایل ایجاد میکند.
- x << y معادل عبارت ریاضی x * 2**y است.
- x >> y معادل عبارت ریاضی x / 2**y است که به سمت بی نهایت منفی گرد شده است.
هشدار
قبل از نسخه 0.5.0، یک شیفت راست x >> y برای x منفی معادل عبارت ریاضی x/2**y بود که به سمت صفر گرد شده بود، به عنوان مثال، شیفت های راست از گرد کردن به سمت بالا (به سمت صفر) به جای گرد کردن (به سمت بی نهایت منفی) استفاده می کردند.
توجه داشته باشید
بررسی های اضافی هرگز برای عملیات شیفت انجام نمی شود چون آنها برای عملیات حسابی انجام می شود. در عوض، نتیجه همیشه کوتاه می شود.
جمع، تفریق و ضرب
جمع، تفریق و ضرب معناشناسی معمول را دارند، با توجه به دو حالت مختلف از نظر سرریز و زیرریز به طور پیش فرض، تمام محاسبات زیرریز و سریز بررسی میشود، اما این میتواند با استفاده از unchecked block غیرفعال شود، در نتیجه محاسبات پیچیده میشود. جزئیات بیشتر را میتوان در آن بخش یافت.
عبارت x- برابر است با (T(0) – x) که T نوع x است. فقط در انواع امضا شده قابل استفاده است. اگر x منفی باشد مقدار x- میتواند مثبت باشد. اخطار دیگری نیز وجود دارد که ناشی از نمایش مکمل دو است:
اگر ;int x = type(int).min داشته باشید، x- با رنج مثبت متناسب نیست. این به این معنی است که unchecked { assert(-x == x); } کار میکند، و عبارت x- هنگامی که در حالت checked استفاده شود، منجر به اعلان شکست میشود.
تقسیم
از آنجا که نوع نتیجه یک عملیات همیشه نوع یکی از عملوندها است، تقسیم بر اعداد صحیح همیشه منجر به یک عدد صحیح میشود. در سالیدیتی، تقسیم به سمت صفر گرد میشود. این بدان معنی است که int256(-5) / int256(2) == int256(-2).
توجه داشته باشید که در مقابل، تقسیم بر روی لیترالها منجر به مقادیر کسری دلخواه میشود.
توجه داشته باشید
تقسیم بر صفر باعث خطای Panic میشود. این بررسی از طریق { …} unchecked غیرفعال نمیشود.
توجه داشته باشید
عبارت type(int).min/(-1) تنها موردی است که تقسیم باعث سرریز میشود. در حالت حسابی بررسی شده ، باعث اعلان شکست میشود، در حالی که در حالت wrapping ، مقدار type(int).min خواهد بود.
مدول
عملیات مدول a % n پس از تقسیم عملوند a توسط عملوند n، باقی مانده r را حاصل میشود، جایی که q=int(a/n) و r=a-(n*q). این بدان معناست که مدول همان علامت عملوند سمت چپ (یا صفر) خود را نشان میدهد و a % n == -(-a % n) برای منفی a نگه میدارد:
- int256(5) % int256(2) == int256(1)
- int256(5) % int256(-2) == int256(1)
- int256(-5) % int256(2) == int256(-1)
- int256(-5) % int256(-2) == int256(-1)
توجه داشته باشید
باقیمانده با صفر باعث خطای Panic میشود. این بررسی از طریق { … } unchecked غیرفعال نمیشود.
به توان رساندن
به توان رساندن فقط برای انواع بدون علامت در توان در دسترس است. نوع توان در نتیجه همیشه با نوع پایه برابر است. لطفاً توجه داشته باشید که به اندازه کافی بزرگ باشد تا بتواند نتیجه را حفظ کند و برای اعلان شکست احتمالی یا رفتار پیچیده آماده شود.
توجه داشته باشید
در حالت بررسی شده ، توان فقط از آپکد exp نسبتاً ارزان برای پایههای کوچک استفاده میکند. برای موارد x**3، ممکن است عبارت x*x*x ارزان تر باشد. در هر صورت، تست هزینه گس و استفاده از بهینه ساز توصیه میشود.
توجه داشته باشید
توجه داشته باشید که 0**0 توسط EVM به صورت 1 تعریف میشود.
اعداد ثابت
هشدار
اعداد ثابت هنوز توسط سالیدیتی کاملاً پشتیبانی نمیشوند. میتوان آنها را مشخص کرد، اما نمیتوان آنها را به چیزی یا از چیزی اختصاص داد.
ufixed/ fixed : اعداد ثابت بدون علامت و باعلامت دراندازههای مختلف. کلمات کلیدی ufixedMxN و fixedMxN، جایی که M تعداد بیتهای گرفته شده توسط نوع را نشان میدهد و N نشان دهنده تعداد اعشار در دسترس است. M باید بر 8 قابل تقسیم باشد و از 8 به 256 بیت تبدیل شود. N باید شامل 0 تا 80 باشد. ufixed و fixed به ترتیب نامهای مستعار برای ufixed128x18 و fixed128x18 هستند.
عملگرها
مقایسه ها : <=،<، ==،!=، >=،> (ارزیابی به bool)
عملگرهای محاسباتی: +، –، unary –، *، /، % (مدول)
توجه داشته باشید
تفاوت اصلی بین اعداد float ( float و double در بسیاری از زبانها، به طور دقیقتر اعداد IEEE 754 ) و عدد ممیز ثابت در این است که تعداد بیتهای مورد استفاده برای عدد صحیح و قسمت کسری (قسمت بعد از نقطه اعشاری ) در قبل انعطاف پذیر است، در حالی که در دومی به طور دقیقاً تعریف شدهاست. به طور کلی، در اعداد float تقریباً از کل فضا برای نشان دادن عدد استفاده میشود، در حالی که فقط تعداد کمی بیت مکان نقطه اعشار را تعریف میکنند.
آدرس
نوع آدرس به دو صورت وجود دارد که تا حد زیادی یکسان هستند:
- address : دارای مقدار 20 بایت (اندازه آدرس اتریوم) است.
- address payable : همان address است، اما با اعضای اضافی transfer و send.
ایده پشت این تمایز این است که address payable آدرسی است که میتوانید اتر را به آن بفرستید، در حالی که نمیتوان با یک address ساده اتر ارسال کرد.
تبدیلهای نوع:
تبدیل ضمنی از address payable به address مجاز است، در حالی که تبدیل از address به address payable باید صریح از طریق payable(<address>) باشد.
تبدیل صریح به address و از address برای انواع uint160، اعداد صحیح، bytes20 و انواع قرارداد مجاز است.
فقط عبارات نوع address و نوع قرارداد را می توان از طریق تبدیل صریح (…)payable به address payable تبدیل کرد. برای نوع قرارداد، این تبدیل فقط در صورتی مجاز است که قرارداد بتواند اتر را دریافت کند، به عنوان مثال، قرارداد تابع دریافت یا برگشتی قابل پرداخت داشته باشد. توجه داشته باشید که payable(0) معتبر است و از این قاعده مستثنی است.
توجه داشته باشید
اگر به متغیری از نوع address نیاز دارید و قصد دارید اتر را به آن بفرستید، نوع آن را به عنوان address payable اعلام کنید تا این نیاز قابل مشاهده باشد. همچنین، سعی کنید هر چه زودتر این تمایز یا تبدیل را انجام دهید.
تمایز بین address و address payable با نسخه 0.5.0 معرفی شد. همچنین با شروع از آن نسخه، قراردادها به طور ضمنی قابل تبدیل به نوع address نیستند، اما همچنان میتوانند صریحاً به address یا address payable تبدیل شوند، در صورتی که تابع دریافتی یا قابل پرداخت باشند.
اپراتورها:
<=، <، ==، !=، >= و >
هشدار
اگر نوعی را که از اندازه بایت بزرگتر استفاده می کند به address تبدیل کنید، به عنوان مثال bytes32، آنگاه address کوتاه می شود برای کاهش ابهام تبدیل ورژن 0.4.24 و بالاتر کامپایلر شما را مجبور به کوتاه کردن صریح در تبدیل میکند. به عنوان مثال مقدار 32 بایت
0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC را در نظر بگیرید. می توانید از آدرس address(uint160(bytes20(b))) استفاده کنید که نتیجه آن 0x111122223333444455556666777788889999aAaa است، یا می توانید از آدرس address(uint160(uint256(b))) استفاده کنید که نتیجه آن 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc است.
توجه داشته باشید
اعداد هگزادسیمال مختلط مطابق با EIP-55 به طور خودکار به عنوان لفظی از نوع آدرس در نظر گرفته می شوند. به address Literals مراجعه کنید.
اعضای آدرس
برای مراجعه سریع به کلیه اعضای آدرس، به اعضای انواع آدرس مراجعه کنید.
- balance و transfer
میتوان با استفاده از ویژگی balance ، بالانس یک آدرس را جستوجو کرد و با استفاده از تابع transfer اتر (در واحدهای وی ) را به یک آدرس قابل پرداخت ارسال کرد:
address payable x = payable(0x123); address myAddress = address(this); if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
اگر بالانس قرارداد فعلی به اندازه کافی بزرگ نباشد یا انتقال اتر توسط حساب دریافت کننده رد شود، تابع transfer از کار میافتد. تابع transfer در صورت شکست برمیگردد.
توجه داشته باشید
اگر x یک آدرس قرارداد باشد، کد آن (به طور خاصتر: تابع Receive Ether در صورت وجود، یا در غیر این صورت تابع Fallback در صورت وجود) همراه با فراخوانی transfer اجرا میشود (این ویژگی EVM است و نمیتوان جلوی آن را گرفت ) اگر گس آن اجرا تمام شود یا به هر صورتی از کار بیفتد، انتقال اتر برگردانده میشود و قرارداد جاری با استثنا متوقف میشود.
send
Send نقطه مقابل سطح پایین transfer است. در صورت عدم اجرا، قرارداد فعلی با استثنا متوقف نخواهد شد، اما send مقدار false را برمیگرداند.
هشدار
استفاده از send خطرات زیادی دارد: اگر فراخوانی پشته عمق 1024 باشد (که همیشه میتواند توسط فراخوانی کننده مجبور شود) انتقال شکست میخورد و اگر گاز گیرنده شما تمام شود نیز از کار میافتد. بنابراین برای انجام مطمئن انتقال اتر، همیشه مقدار برگشتی send ، را با استفاده از transfer بررسی کنید یا حتی بهتراست که: از الگویی استفاده کنید که گیرنده پول را برداشت کند.
call،delegatecall و staticcall
برای برقراری ارتباط با قراردادهایی که به ABI پایبند نیستند، یا برای گرفتن کنترل مستقیمتری بر روی رمزگذاری ، توابع call،delegatecall و staticcall ارائه شدهاند. همه آنها یک پارامتر bytes memory را میگیرند و شرایط موفقیت (به عنوان bool) و دادههای برگشت (bytes memory) را برمیگردانند. از توابع abi.encode،abi.encodePacked،abi.encodeWithSelector و abi.encodeWithSignature میتوان برای رمزگذاری دادههای ساختار یافته استفاده کرد.
مثال:
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName"); (bool success, bytes memory returnData) = address(nameReg).call(payload); require(success);
هشدار
همه این توابع، توابع سطح پایینی هستند و باید با احتیاط استفاده شوند. به طور خاص، هر قرارداد ناشناختهای ممکن است مخرب باشد و در صورت تماس با آن، کنترل آن قرارداد را به شما واگذار میکند که میتواند به نوبه خود به قرارداد شما بازگردد، بنابراین در زمان بازگشت فراخوانیها خود را برای تغییراتی که روی متغیرهای حالت شما اتفاق میافتد آماده کنید. روش متداول برای برقراری ارتباط با سایر قراردادها، فراخوانی یک تابع در یک شی قرارداد (() x.f) است.
توجه داشته باشید
ورژن های قبلی سالیدیتی به این توابع اجازه میدهد آرگومانهای دلخواه را دریافت کنند و همچنین اولین آرگومان از نوع bytes4 را به گونه دیگری مدیریت کنند. این موارد در نسخه 0.5.0 حذف شدهاند.
تنظیم گس تامین شده با اصلاح کننده gas امکان پذیر است:
address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));
به طور مشابه، مقدار اتر عرضه شده نیز میتواند کنترل شود:
address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
سرانجام، این اصلاح کنندهها میتوانند ترکیب شوند. ترتیب آنها مهم نیست:
address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
به روشی مشابه میتوان از تابع delegatecall استفاده کرد: تفاوت در این است که فقط از کد آدرس داده شده استفاده میشود، تمام جنبههای دیگر (storage, balance, …) از قرارداد فعلی گرفته شدهاند. هدف از فراخوانی delegatecall استفاده از کد کتابخانه است که در قرارداد دیگری ذخیره شدهاست. کاربر باید اطمینان حاصل کند که ساختار storage در هر دو قرارداد برای استفاده از delegatecall مناسب است.
توجه داشته باشید
قبل از Homestead، فقط یک نوع محدود به نام callcode در دسترس بود که دسترسی به مقادیر msg.sender و msg.value اصلی را فراهم نمی کرد. این تابع در نسخه 0.5.0 حذف شد.
از آنجا که byzantium staticcall نیز میتواند مورد استفاده قرار گیرد. این اساساً همان call است، اما اگر تابع فراخوانی شده به هر طریقی حالت را تغییر دهد، برمیگردد. هر سه تابع call، delegatecall و staticcall تابعهای سطح پایینی هستند و فقط به عنوان آخرین راه حل باید از آنها استفاده شود زیرا باعث از بین رفتن ایمنی بودن نوع سالیدیتی میشوند. گزینه gas در هر سه روش موجود است، در حالی که گزینه value برای delegatecall پشتیبانی نمیشود.
توجه داشته باشید
بهتر است بدون توجه به اینکه آیا state از آن خوانده می شود یا روی آن نوشته شده است، از اتکا به مقادیر گس کدگذاری شده در کد قرارداد هوشمند خود اجتناب کنید، زیرا این امر میتواند مشکلات زیادی را به همراه داشته باشد. همچنین، دسترسی به گس ممکن است در آینده تغییر کند.
- code و codehash
می توانید کد مستقر شده را برای هر قرارداد هوشمندی جستجو کنید. از code. برای دریافت بایت کد EVM به عنوان یک bytes memory استفاده کنید، که ممکن است خالی باشد. برای دریافت هش Keccak-256 آن کد (به عنوان bytes32) از کد codehash. استفاده کنید. توجه داشته باشید که addr.codehash ارزان تر از استفاده از keccak256 (addr.code) است.
توجه داشته باشید
کلیه قراردادها را میتوان به نوع address تبدیل کرد، بنابراین میتوان بالانس قرارداد فعلی را با استفاده از address(this).balance جست وجو کرد.
انواع قرارداد
هر قراردادی نوع خاص خود را مشخص میکند. به طور ضمنی میتوانید قراردادها را به قراردادهایی که از آنها به ارث میبرند تبدیل کنید. قراردادها را میتوان به طور صریح به نوع address تبدیل کرد.
تبدیل صریح به نوع address payable فقط از آنجا امکان پذیر است که نوع قرارداد تابع برگشتی قابل دریافت یا پرداخت داشته باشد. تبدیل هنوز با استفاده از address(x) انجام میشود. اگر نوع قرارداد تابع برگشت پذیر یا قابل پرداخت نباشد، تبدیل به address payable را میتوان با استفاده از payable(address(x)) انجام داد. در بخش مربوط به نوع آدرس میتوانید اطلاعات بیشتری کسب کنید.
توجه داشته باشید
قبل از ورژن 0.5.0، قراردادها مستقیماً از نوع آدرس نشأت میگرفتند و هیچ تفاوتی بین address و address payable وجود نداشت.
اگر متغیر محلی را از نوع قرارداد MyContract c مشخص کنید، میتوانید توابع مربوط به آن قرارداد را فراخوانی کنید. مراقب باشید که آن را از جایی اختصاص دهید که همان نوع قرارداد باشد. شما همچنین میتوانید قراردادها را فوری (یعنی آنهایی که تازه ایجاد شدهاند) قرار دهید. جزئیات بیشتر را میتوانید در بخش “قرارداد از طریق new” پیدا کنید
نمایش داده های یک قرارداد با نوع address یکسان است و این نوع در ABI نیز استفاده می شود.
قراردادها از هیچ عملگری پشتیبانی نمیکنند. اعضای انواع قرارداد، توابع خارجی قرارداد شامل هر متغیر حالت است که به عنوان public مشخص شدهاست. برای قرارداد C میتوانید از type(C) برای دسترسی به اطلاعات مربوط به قرارداد استفاده کنید.
آرایههای بایت با اندازه ثابت
مقدارهای مختلف bytes1، bytes2، bytes3، …، bytes32 توالی بایت را از یک تا 32 نگه میدارد.
عملگرها:
- مقایسه ها: <=، <، ==، !=، >=، > (ارزیابی به bool)
- عملگرهای بیت: &، |، ^ (انحصاری بیتی یا)، ~ (نفی بیتی)
- عملگرهای Shift: << (Shift چپ)، >> (Shift راست)
دسترسی به فهرست: اگر x از نوع bytesI باشد، x[k] برای k<I و k ,o<= kامین بایت (فقط خواندنی) را برمیگرداند.
عملگر شیفت با نوع عدد صحیح بدون علامت به عنوان عملوند راست کار میکند (اما نوع عملوند سمت چپ را برمیگرداند)، که تعداد بیت های شیفت را نشان میدهد. جابجایی با نوع با علامت ، خطای کامپایل ایجاد میکند.
اعضا:
length. طول ثابت آرایه بایت را ارائه میدهد (فقط برای خواندن)
توجه داشته باشید
نوع []bytes1، آرایهای از بایت است. اما به دلیل قوانین padding، 31 بایت فضا را برای هر عنصر هدر میدهد (به جز درstorage ). بهتر است به جای آن از نوع bytes استفاده کنید.
توجه داشته باشید
قبل از ورژن 0.8.0 ، byte یک نام مستعار برای bytes1 بود.
آرایه بایت با اندازه پویا
bytes:
آرایه بایت در اندازه پویا، به آرایهها مراجعه کنید. از نوع مقدار نیست!
string:
رشتهای با کد UTF-8 به صورت پویا، به آرایهها مراجعه کنید. از نوع مقدار نیست!
لیترالهای Address
لیترالهای هگزادسیمال که از تست checksum آدرس استفاده میکنند، به عنوان مثال 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF از نوع address هستند. لیترالهای هگزادسیمال که دارای طول 39 تا 41 رقم هستند و از تست checksum عبور نمیکنند، خطایی ایجاد میکنند. برای حذف خطا میتوانید (برای انواع عدد صحیح) یا (برای انواع bytesNN ) صفرها را ضمیمه کنید.
توجه داشته باشید
قالب checksum address آدرس مختلط در EIP-55 تعریف شده است.
لیترالهای عدد گویا و صحیح
لیترالهای عدد صحیح از توالی اعداد در محدوده 0-9 تشکیل می شوند. آنها به عنوان دیسیمال تفسیر می شوند. به عنوان مثال 69 به معنای شصت و نه است. لیترالهای Octal در سالیدیتی وجود ندارند و صفرهای قبل از عدد نامعتبر هستند.
لیترالهای کسری دیسیمال توسط یک . با حداقل یک عدد در یک طرف تشکیل میشوند. مثالها شامل 1. و 1.3 است (اما نه .1)
نماد علمی به شکل 2e10 نیز پشتیبانی میشود، جایی که مانتیس می تواند کسری باشد اما توان باید یک عدد صحیح باشد. MeE تحت اللفظی معادل M * 10**E است. به عنوان مثال می توان به 2e10، -2e10، 2e-10، 2.5e1 اشاره کرد.
زیرخط میتواند برای جدا کردن رقم از لیترالهای عددی برای کمک به خوانایی استفاده شود. به عنوان مثال، دسیمال 000_123 هگزادسیمال 0x2eff_abde، نماد علمی دسیمال 1_2e345_678 همه معتبر هستند. زیرخط تنها بین دو رقم مجاز است و تنها یک زیرخط متوالی مجاز است. هیچ معنایی سمنتیک اضافی به لیترال عددی حاوی زیرخط اضافه نشده است.
عبارات لیترالهای عددی دقت دلخواه را حفظ میکنند تا زمانی که به یک نوع غیرلیترالی تبدیل شوند (به عنوان مثال استفاده از آنها همراه با یکدیگر با یک عبارت غیرلیترالی یا با تبدیل صریح). این بدان معناست که محاسبات سرریز نمیشود و تقسیمات در عبارات لیترال عددی کوتاه نمیشوند. به عنوان مثال، 2**800 – (2**800 + 1) منجر به ثابت 1 (از نوع uint8) میشود گرچند نتایج میانی حتی اندازه کلمه ماشین را فیت نمیکند. علاوه بر این، 8 *5. منجر به عدد صحیح 4 (گرچند در بین آنها غیر عدد صحیح استفاده میشود).
هشدار
در حالی که اکثر عملگرها هنگام اعمال به literals یک عبارت تحت اللفظی تولید می کنند، عملگرهای خاصی وجود دارند که از این الگو پیروی نمی کنند:
اپراتور سه تایی (… : … ? …)،
زیرنویس آرایه ([<array>[<index>).
ممکن است انتظار داشته باشید عباراتی مانند (true ? 1 : 0) +255 یا [0][1, 2, 3]+255 معادل استفاده مستقیم از 256 باشد، اما در واقع آنها در نوع uint8 محاسبه می شوند و می توانند سرریز شوند.
هر عملگری که میتواند به عدد صحیح اعمال شود، تا زمانی که عملوندها عدد صحیح باشند می تواند به لیترالهای عددی نیز اعمال شود. اگر هر یکی از این دو کسری باشند، عملیات بیت امکان پذیر نیست و نیز به توان رساندن اگر توان کسری باشد (زیرا ممکن است منجر به یک عدد غیر گویا شوند) امکان پذیر نیست.
تعویض و به توان رساندن با اعداد لیترال بطوریکه سمت چپ (یا پایه) عملوند و نوع عدد صحیح در سمت راست به عنوان عملوند (توان) همیشه در uint256 (برای لیترالهای غیر منفی) یا int256 (برای لیترالهای منفی)، بدون توجه به نوع سمت راست عملوند (توان)، عمل میکند.
هشدار
تقسیم بر روی لیترالهای عدد صحیح برای کوتاه کردن در سالیدیتی نسخههای قبلتر از نسخه 0.4.0 استفاده میشد، اما اکنون به یک عدد گویا تبدیل می شود، برای مثال 5/2 برابر با 2 نیست بلکه برابر با 2.5 میباشد.
توجه داشته باشید
سالیدیتی برای هر عدد گویا یک نوع لیترال عددی دارد. لیترال های عدد صحیح و لیترالهای عدد گویا به انواع لیترالهای عدد تعلق دارند. علاوه بر این، تمام عبارات لیترالهای عددی (یعنی عباراتی که فقط شامل لیترالهای عددی و عملگرها هستند) به انواع لیترالهای عددی تعلق دارند. بنابراین عبارات لیترال عددی 1 + 2 و 2 + 1 هر دو متعلق به همان نوع لیترال عددی برای عدد گویا سه هستند.
توجه داشته باشید
عبارات لیترال عددی به محض استفاده با عبارات غیر لیترال به نوع غیر لیترال تبدیل میشوند. با نادیده گرفتن انواع، مقدار عبارتی که به b در زیر اختصاص داده شده به عنوان عدد صحیح ارزیابی میشود. از آنجا که a از نوع uint128 است، عبارت a+ 2.5 باید نوع مناسبی داشته باشد. از آنجا که نوع متداولی برای نوع 2.5 و uint128 وجود ندارد، کامپایلر سالیدیتی این کد را قبول نمیکند.
uint128 a = 1; uint128 b = 2.5 + a + 0.5;
لیترالهای string و انواع
لیترالهای رشتهای با دو نقل قول يا تك نقل قولي نوشته ميشوند (“foo” or ‘bar‘) و همچنين ميتوانند به چند قسمت متوالي تقسيم شوند (“foo” “bar” معادل “foobar” است) که میتواند هنگام کار با رشتههای طولانی مفید باشد. آنها به مفهوم صفر انتهایی در C نیست . “foo” نشانگر سه بایت میباشد، نه چهار بایت.
همانند لیترالهای عدد صحیح، نوع آنها نیز میتواند متفاوت باشد، اما در صورت متناسب بودن آنها به بایتهای bytes1, …, bytes32 تبدیل میشوند، اگر متناسب باشند، به bytes و string تبدیل میشوند.
به عنوان مثال، با “bytes32 samevar = “stringliteral لیترال رشتهای وقتی به نوع bytes32 اختصاص یابد به معنای بایت خام تفسیر میشود.
لیترالهای رشتهای فقط میتوانند شامل کاراکترهای ASCII قابل چاپ باشند، به این معنی که کاراکترها، بین و شامل 0x20 .. 0x7E هستند.
علاوه بر این، لیترالهای رشتهای از کارکترهای escape زیر نیز پشتیبانی میکنند:
(escapes an actual newline)<newline>\
(backslash)\\
(single quote)‘\
(double quote)”\
n(newline)\
r(carriage return)\
t(tab)\
xNN(hex escape, see below)\
(uNNNN (unicode escape, see below\
xNN\ یک مقدار hex میگیرد و بایت مناسب را وارد میکند، در حالی که uNNNN\ یک کد رمز Unicode را میگیرد و یک توالی UTF-8 را وارد میکند.
توجه داشته باشید
تا نسخه 0.8.0 سه دنباله فرار اضافی وجود داشت: b، \f\ و v\. آنها معمولاً به زبان های دیگر در دسترس هستند اما در عمل به ندرت مورد نیاز هستند. اگر به آنها نیاز دارید، همچنان میتوان آنها را از طریق هگزادسیمال escape، یعنی به ترتیب x08، \x0c\ و x0b\ مانند هر کاراکتر ASCII دیگری وارد کرد.
رشته در مثال زیر دارای طول ده بایت است. این کار با یک بایت خط جدید و به دنبال آن یک دو نقل قول، یک تک نقل قول یک کاراکتر بک اسلش و سپس (بدون جدا کننده) توالی کاراکتر abcdef شروع میشود.
"\n\"\'\\abc\ def"
هر خاتمه دهنده خط Unicode که یک خط جدید نباشد ( به عنوان مثال LF ، VF ، FF ، CR ، NEL ، LS ، PS ) برای خاتمه لیترال رشته در نظر گرفته میشود. خط جدید فقط در صورتی لیترال رشته را خاتمه میدهد که قبل از آن \ وجود نداشته باشد.
لیترالهای Unicode
در حالی که لیترالهای رشتهای منظم فقط میتوانند حاوی ASCII باشند، لیترالهای یونیکد – با پیشوند کلمه کلیدی – unicode میتوانند حاوی هر توالی معتبر UTF-8 باشند. آنها همچنین از همان توالیهای escape به عنوان لیترالهای رشته منظم پشتیبانی میکنند.
;"😃 string memory a = unicode"Hello
لیترالهای هگزادسیمال
لیترالهای هگزادسیمال با پیشوند کلمه کلیدی hex هستند،که با دو نقل قول يا تك نقل قولي محصور شدهاند (“hex’0011_22_FF’،hex”001122FF) محتوای آنها باید ارقام هگزادسیمال باشد که به صورت اختیاری میتواند از یک زیر خط به عنوان جدا کننده بین مرز بایت استفاده کند. مقدار لیترال، نمایش دودویی توالی هگزادسیمال خواهد بود. لیترالهای مالتی هگزادسیمال جدا شده توسط فضای خالی به یک لیترال متصل میشوند: ”hex”00112233″ hex”44556677 معادل با ”hex”0011223344556677 است.
لیترالهای هگزادسیمال مانند لیترالهای رشتهای رفتار میکنند و محدودیتهای تبدیل پذیری یکسانی دارند.
Enums
Enums یکی از راههای ایجاد یک نوع) (typeتعریف شده توسط کاربر در سالیدیتی میباشد. آنها به طور صریح قابل تبدیل به انواع مختلف عدد صحیح هستند اما تبدیل ضمنی مجاز نیست. تبدیل صریح از عدد صحیح در زمان اجرا بررسی میکند که مقدار در محدوده enum باشد و در غیر این صورت باعث ایجاد خطای Panic میشود. Enums حداقل به یک عضو نیاز دارد و مقدار پیش فرض آن هنگام اعلام اولین عضو است. Enums نمیتواند بیش از 256 عضو داشته باشد.
نمایش داده ها مانند enums در C است: گزینهها با مقادیر صحیح بدون علامت بعدی که از 0 شروع میشوند نشان داده میشوند.
با استفاده از type(NameOfEnum).min و type(NameOfEnum).max میتوانید کوچکترین و به ترتیب بزرگترین مقدار enum داده شده را بدست آورید.
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.8; contract test { enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill } ActionChoices choice; ActionChoices constant defaultChoice = ActionChoices.GoStraight; function setGoStraight() public { choice = ActionChoices.GoStraight; } // Since enum types are not part of the ABI, the signature of "getChoice" // will automatically be changed to "getChoice() returns (uint8)" // for all matters external to Solidity. function getChoice() public view returns (ActionChoices) { return choice; } function getDefaultChoice() public pure returns (uint) { return uint(defaultChoice); } function getLargestValue() public pure returns (ActionChoices) { return type(ActionChoices).max; } function getSmallestValue() public pure returns (ActionChoices) { return type(ActionChoices).min; } }
توجه داشته باشید
Enums همچنین میتواند خارج از تعاریف قرارداد یا کتابخانه، در سطح فایل مشخص شوند.
انواع ارزش تعریف شده توسط کاربر
یک نوع ارزش تعریف شده توسط کاربر امکان ایجاد یک انتزاع هزینه صفر را نسبت به یک نوع ارزش ابتدایی فراهم می کند. این شبیه به نام مستعار است، اما با الزامات نوع سختگیرانهتر. یک نوع مقدار تعریف شده توسط کاربر با استفاده از type C is V تعریف می شود. که در آن C نام نوع تازه معرفی شده است و V باید یک نوع مقدار داخلی باشد (“نوع زیربنایی”). تابع C.wrap برای تبدیل از نوع اصلی به نوع سفارشی استفاده می شود. به طور مشابه، تابع C.unwrap برای تبدیل از نوع سفارشی به نوع زیربنایی استفاده می شود. نوع C هیچ عملگر یا توابع عضو محدودی ندارد. به طور خاص، حتی عملگر == نیز تعریف نشده است. تبدیل صریح و ضمنی به و از انواع دیگر مجاز نیست. نمایش دادههای مقادیر چنین انواعی از نوع زیربنایی به ارث میرسد و نوع زیربنایی نیز در ABI استفاده میشود.
مثال زیر یک نوع سفارشی UFixed256x18 را نشان می دهد که نشان دهنده یک نوع نقطه ثابت اعشاری با 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 UFixed256x18 is uint256; /// A minimal library to do fixed point operations on UFixed256x18. library FixedMath { uint constant multiplier = 10**18; /// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked /// arithmetic on uint256. function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) { return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b)); } /// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked /// arithmetic on uint256. function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) { return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b); } /// Take the floor of a UFixed256x18 number. /// @return the largest integer that does not exceed `a`. function floor(UFixed256x18 a) internal pure returns (uint256) { return UFixed256x18.unwrap(a) / multiplier; } /// Turns a uint256 into a UFixed256x18 of the same value. /// Reverts if the integer is too large. function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) { return UFixed256x18.wrap(a * multiplier); } }
توجه کنید که چگونه UFixed256x18.wrap و FixedMath.toUFixed256x18 دارای امضای یکسان هستند اما دو عملیات بسیار متفاوت را انجام می دهند: تابع UFixed256x18.wrap یک UFixed256x18 را برمیگرداند که نمایش دادهای مشابه ورودی دارد. در حالی که toUFixed256x18 یک UFixed256x18 را برمیگرداند که مقدار عددی یکسانی دارد.
Function types (انواع توابع)
Function types انواعی از توابع هستند. متغیرهای function type را میتوان از توابع اختصاص داد و پارامترهای تابع function type را میتوان برای انتقال توابع به توابع برگشتی و از فراخوانیهای تابع استفاده کرد. Function types به دو صورت هستند– توابع داخلی و خارجی:
توابع داخلی را فقط میتوان در داخل قرارداد فعلی فراخوانی کرد (به طور خاص، در داخل واحد کد فعلی، که شامل توابع کتابخانه داخلی و توابع وراثتی نیز میشود) زیرا نمیتوانند خارج از متن قرارداد فعلی اجرا شوند. فراخوانی یک تابع داخلی با پرش به برچسب ورودی آن انجام میشود، دقیقاً مانند هنگام فراخوانی داخلی توابع قرارداد فعلی.
توابع خارجی شامل یک آدرس و یک امضای تابع میباشند و میتوان آنها را از طریق فراخوانیهای تابع خارجی منتقل کرد و از آنها بازگرداند.
Function types به شرح زیر ذکر شدهاست:
function (<parameter types>) {internal|external}
[pure|view|payable] [returns (<return types>)]
در مقابل انواع پارامترها، انواع بازگشت نمیتوانند خالی باشند – اگر function type نباید چیزی را برگرداند، کل قسمت returns (<return types>) باید حذف شود. به طور پیش فرض، function type داخلی هستند، بنابراین میتوان کلمه کلیدی internal را حذف کرد. توجه داشته باشید که این فقط در Function types اعمال میشود. قابلیت مشاهده به طور صریح برای توابع تعریف شده در قراردادها مشخص میشود، آنها پیش فرض ندارند.
تبدیلها:
یک تابع نوع (function type) A به طور ضمنی قابل تبدیل به یک تابع نوع (function type) B است اگر و فقط اگر انواع پارامترهای آنها یکسان باشد، انواع بازگشت آنها یکسان، ویژگی internal/external آنها یکسان باشد و تغییرپذیری حالت A محدود کنندهتر از تغییر پذیری حالت B باشد. به طور خاص :
- توابع pure را میتوان به view و توابع non-payable تبدیل کرد.
- توابع view را میتوان به توابع non-payable پرداخت تبدیل کرد.
- توابع payable را میتوان به توابع non-payable پرداخت تبدیل کرد.
هیچ تبدیل دیگری بین انواع توابع امکان پذیر نیست.
قانون مربوط به payable و non-payable ممکن است کمی گیج کننده باشد، اما در اصل، اگر تابعی payable باشد، این بدان معناست که پرداخت صفر اتر را نیز میپذیرد، بنابراین non-payable نیز میباشد. از طرف دیگر، یک تابع non-payable اتر ارسال شده به آن را رد میکند، بنابراین توابع non-payable نمیتوانند به توابع payable تبدیل شوند.
اگر یک متغیر از نوع تابع مقداردهی اولیه نشده باشد، فراخوانی آن منجر به خطای Panic میشود. اگر پس از استفاده از delete تابع آن را فراخوانی کنید، همین اتفاق میافتد.
اگر از انواع توابع خارجی خارج از زمینه سالیدیتی استفاده شود، با آنها به عنوان نوع function رفتار میشود، که آدرس و به دنبال آن شناسه تابع را با هم در یک تک نوع bytes24 رمزگذاری میکند. توجه داشته باشید که توابع عمومی قرارداد جاری میتوانند هم به عنوان تابع داخلی و هم به عنوان تابع خارجی استفاده شود. برای استفاده از f به عنوان یک تابع داخلی ، فقط از f استفاده کنید، اگر میخواهید از فرم خارجی آن استفاده کنید، از this.f استفاده کنید.
یک تابع از یک نوع داخلی را میتوان به یک متغیر از یک نوع تابع داخلی بدون در نظر گرفتن مکان تعریف شده اختصاص داد. این شامل توابع خصوصی، داخلی و عمومی قراردادها و کتابخانهها و همچنین توابع رایگان است. از طرف دیگر، انواع توابع خارجی فقط با توابع قرارداد عمومی و خارجی سازگار هستند. کتابخانهها از این مستثنی هستند چونکه به یک delegatecall نیاز دارند و از یک کنوانسیون مختلف ABI برای انتخابگرهای خود استفاده میکنند. توابع اعلام شده در رابطها تعریفی ندارند، بنابراین اشاره به آنها نیز معنی ندارد.
اعضا:
توابع خارجی (یا عمومی) اعضای زیر را دارند:
- address. آدرس قرارداد تابع را برمیگرداند.
- selector. انتخابگر تابع ABI را برمیگرداند.
توجه داشته باشید
توابع خارجی (یا عمومی) برای داشتن اعضای اضافی gas(uint). و value(uint). استفاده میشود. اینها در سالیدیتی نسخه 0.6.2 منسوخ شده و در سالیدیتی نسخه 0.7.0 حذف شدند. در عوض از {… :gas} و {… :value} برای تعیین مقدار گس یا مقدار wei ارسال شده به یک تابع استفاده کنید. برای اطلاعات بیشتر به فراخوانی تابع خارجی مراجعه کنید.
مثالی که نحوه استفاده از اعضا را نشان میدهد:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.4 <0.9.0; contract Example { function f() public payable returns (bytes4) { assert(this.f.address == address(this)); return this.f.selector; } function g() public { this.f{gas: 10, value: 800}(); } }
مثالی که نحوه استفاده از انواع توابع داخلی را نشان میدهد:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; library ArrayUtils { // internal functions can be used in internal library functions because // they will be part of the same code context function map(uint[] memory self, function (uint) pure returns (uint) f) internal pure returns (uint[] memory r) { r = new uint[](self.length); for (uint i = 0; i < self.length; i++) { r[i] = f(self[i]); } } function reduce( uint[] memory self, function (uint, uint) pure returns (uint) f ) internal pure returns (uint r) { r = self[0]; for (uint i = 1; i < self.length; i++) { r = f(r, self[i]); } } function range(uint length) internal pure returns (uint[] memory r) { r = new uint[](length); for (uint i = 0; i < r.length; i++) { r[i] = i; } } } contract Pyramid { using ArrayUtils for *; function pyramid(uint l) public pure returns (uint) { return ArrayUtils.range(l).map(square).reduce(sum); } function square(uint x) internal pure returns (uint) { return x * x; } function sum(uint x, uint y) internal pure returns (uint) { return x + y; } }
مثال دیگری که از انواع توابع خارجی استفاده میکند:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; contract Oracle { struct Request { bytes data; function(uint) external callback; } Request[] private requests; event NewRequest(uint); function query(bytes memory data, function(uint) external callback) public { requests.push(Request(data, callback)); emit NewRequest(requests.length - 1); } function reply(uint requestID, uint response) public { // Here goes the check that the reply comes from a trusted source requests[requestID].callback(response); } } contract OracleUser { Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract uint private exchangeRate; function buySomething() public { ORACLE_CONST.query("USD", this.oracleResponse); } function oracleResponse(uint response) public { require( msg.sender == address(ORACLE_CONST), "Only oracle can call this." ); exchangeRate = response; } }
توجه داشته باشید
توابع Lambda یا inline برنامه ریزی شدهاند اما هنوز پشتیبانی نمیشوند.
انواع مرجع
مقادیر نوع مرجع را میتوان از طریق چندین نام مختلف اصلاح کرد. هر زمان که متغیر از نوع مقدار استفاده شود، در جایی که یک کپی مستقل دریافت میکنید نوع مرجع را با انواع مقدار مقایسه کنید. به همین دلیل، انواع مرجع باید با دقت بیشتری از انواع مقادیر رسیدگی شوند. در حال حاضر، انواع مرجع شامل ساختارها (structs)، آرایههاarrays)) و نگاشتها(mappings) است. اگر از یک نوع مرجع استفاده میکنید، همیشه باید صریحاً منطقه دادهای را که نوع در آن ذخیره شده است ارائه دهید memory (که طول عمر آن محدود به فراخوانی تابع خارجی است)، storage (مکانی که متغیرهای حالت در آن ذخیره میشوند، جایی که طول عمر آنها به طول عمر قرارداد محدود میشود) یا calldata (مکان داده ویژهای که شامل آرگومانهای تابع است.)
یک انتساب یا تبدیل نوع که مکان داده را تغییر میدهد، همیشه موجب یک عملیات کپی خودکار خواهد شد، در حالی که انتساب در داخل همان مکان داده فقط در برخی موارد برای انواع storage کپی میشوند.
مکان داده
هر نوع مرجع حاوی یادداشت اضافی است، “data location”، در مورد مکانی که ذخیره میشود. سه مکان داده وجود دارد: memory ، storage و calldata.
Calldata یک منطقه غیرقابل تغییر و غیرقابل ماندگاری است که آرگومانهای تابع در آن ذخیره میشود و بیشتر مانند مِمُوری رفتار میکند. برای پارامترهای توابع خارجی لازم است اما میتواند برای سایر متغیرها نیز استفاده شود.
توجه داشته باشید
اگر میتوانید، سعی کنید از calldata به عنوان مکان داده استفاده کنید زیرا از کپی جلوگیری میکند و همچنین مطمئن میشوید که دادهها قابل اصلاح نیستند. آرایهها و structهای دارای مکان داده calldata نیز میتوانند از توابع برگردانده شوند، اما اختصاص چنین نوعهایی امکان پذیر نیست.
توجه داشته باشید
قبل از نسخه 0.6.9 مکان داده برای آرگومان های نوع مرجع محدود به calldata در توابع خارجی، memory در توابع عمومی و memory یا storage در توابع داخلی و خصوصی بود. اکنون memory و calldata در همه عملکردها بدون در نظر گرفتن قابلیت مشاهده آنها مجاز است.
توجه داشته باشید
قبل از نسخه 0.5.0، مکان داده میتوانست حذف شود، و بسته به نوع متغیر، نوع تابع و غیره، بهطور پیشفرض در مکانهای مختلف قرار میگیرد، اما همه انواع پیچیده اکنون باید یک مکان داده مشخص ارائه دهند.
مکان داده و رفتار انتساب
مکان داده نه تنها برای ماندگاری دادهها بلکه برای معنای انتسابها نیز مهم هستند:
• انتسابها بین storage و memory ( یا از calldata) همیشه یک کپی مستقل ایجاد میکنند.
انتسابها از memory به memory فقط مراجع را ایجاد میکند. این بدان معناست که تغییرات در یک متغیر مِمُوری در سایر متغیرهای مِمُوری که به دادههای مشابه ارجاع میکنند نیز قابل مشاهده است.
• انتسابها از storage به یک متغیر storage محلی نیز فقط یک مرجع اختصاص میدهند.
• سایر انتسابات به storage همیشه کپی میشوند. نمونههایی برای این مورد، انتساب به متغیرهای حالت یا اعضای متغیرهای محلی از نوع ساختار ذخیرهسازی میباشند، حتی اگر متغیر محلی فقط یک مرجع باشد.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; contract C { // The data location of x is storage. // This is the only place where the // data location can be omitted. uint[] x; // The data location of memoryArray is memory. function f(uint[] memory memoryArray) public { x = memoryArray; // works, copies the whole array to storage uint[] storage y = x; // works, assigns a pointer, data location of y is storage y[7]; // fine, returns the 8th element y.pop(); // fine, modifies x through y delete x; // fine, clears the array, also modifies y // The following does not work; it would need to create a new temporary / // unnamed array in storage, but storage is "statically" allocated: // y = memoryArray; // Similarly, "delete y" is not valid, as assignments to local variables // referencing storage objects can only be made from existing storage objects. // It would "reset" the pointer, but there is no sensible location it could point to. // For more details see the documentation of the "delete" operator. // delete y; g(x); // calls g, handing over a reference to x h(x); // calls h and creates an independent, temporary copy in memory } function g(uint[] storage) internal pure {} function h(uint[] memory) public pure {} }
آرایهها
آرایهها میتوانند اندازه ثابت زمان کامپایل داشته باشند، یا میتوانند اندازه پویا داشته باشند. نوع آرایهای با اندازه ثابت k و نوع عنصر T به صورت T[k] و آرایهای با اندازه پویا به صورت []T نوشته میشود.
به عنوان مثال، آرایهای از 5 آرایه دینامیکی uint به صورت uint[][5] نوشته میشود. علامت گذاری در مقایسه با برخی از زبانهای دیگر معکوس میشود. در سالیدیتی ، X[3] همیشه یک آرایه است که شامل سه عنصر از نوع X است، حتی اگر X خودش یک آرایه باشد. این مورد در زبانهای دیگر مانند C وجود ندارد.
شاخصها مبتنی بر صفر هستند و دسترسی در خلاف جهت اعلامیه است.
به عنوان مثال، اگر یک متغیر uint[][5] memory x داشته باشید، با استفاده از x[2][6] به uint دوم در آرایه پویای سوم دسترسی پیدا میکنید و برای دسترسی به آرایه پویای سوم، از x[2] استفاده کنید. باز هم اگر یک آرایه T[5] a برای نوع T دارید که میتواند یک آرایه نیز باشد، a[2] همیشه نوع T را دارد.
عناصر آرایه میتوانند از هر نوع شامل نگاشت یا ساختار باشند. محدودیتهای کلی برای انواع اعمال میشود، به این دلیل که نگاشتها فقط در محل داده storage میتوانند ذخیره شوند و توابع قابل مشاهده به صورت عمومی، نیاز به پارامترهایی دارند که از نوع ABI باشند.
میتوان آرایههای متغیر حالت را به صورت public علامت گذاری کرد و از سالیدیتی برای ایجاد یک getter استفاده کرد. شاخص عددی به یک پارامتر مورد نیاز برای getter تبدیل میشود.
دستیابی به آرایهای که از انتهای آن گذشته است، ادعای ناموفقی را ایجاد میکند. از روش های ()push. و push(value). میتوان برای افزودن یک عنصر جدید در انتهای آرایه استفاده کرد، جایی که ()push. یک عنصر مقداردهی شده صفر را اضافه میکند و مرجعی را به آن برمیگرداند.
bytes و string به صورت آرایهها
متغیرهای نوع bytes و string آرایههای خاصی هستند. نوع bytes مانند []bytes1 است، اما در calldata و مِمُوری کاملاً بسته بندی شده است. string برابر با bytes است اما اجازه دسترسی به طول یا index را نمیدهد.
سالیدیتی توابع دستکاری string ندارد، اما کتابخانههای string طرف سوم وجود دارد. همچنین میتوانید با استفاده از keccak256(abi.encodePacked(s1))== keccak256(abi.encodePacked(s2)) دو رشته را بر اساس keccak256-hash آنها مقایسه کنید و با استفاده از (string.concat(s1, s2 دو رشته را به هم متصل کنید.
از آنجایی که استفاده از[]bytes1 در memory بین عناصر 31 بایت padding اضافه میکند، شما باید از bytes بیش از []bytes1 استفاده کنید زیرا ارزانتر است. توجه داشته باشید که در ذخیره سازی، لایهگذاری ناشی از بسته بندی وجود ندارد ( the padding is absent due to tight packing)، به بایت ها و رشته ها مراجعه کنید. به عنوان یک قاعده کلی از bytes برای دادههای خام با طول دلخواه و از string برای دادههای رشته با طول دلخواه (UTF-8) استفاده کنید. اگر میتوانید طول را به تعداد مشخصی از بایت محدود کنید، همیشه از یکی از انواع مقدار bytes1 تا bytes32 استفاده کنید زیرا بسیار ارزانتر هستند.
توجه داشته باشید
اگر میخواهید به نمایش بایت یک رشتهی s دسترسی پیدا کنید، از ;’bytes(s).length /bytes(s)[7] = ‘x استفاده کنید. بخاطر داشته باشید که شما به بایتهای سطح پایین، پیش نمایش UTF-8 و نه به کارکترهای جداگانه دسترسی پیدا میکنید.
توابع bytes.concat و string.concat
با استفاده از string.concat می توانید تعدادی دلخواه از مقادیر string را به هم متصل کنید. تابع یک آرایه memory string را برمی گرداند که حاوی محتویات آرگومان ها بدون padding است. اگر می خواهید از پارامترهایی از انواع دیگر استفاده کنید که به طور ضمنی قابل تبدیل به string نیستند، ابتدا باید آنها را به string تبدیل کنید.
به طور مشابه، تابع bytes.concat می تواند تعداد دلخواه bytes یا مقادیر bytes1 … bytes32 را به هم متصل کند. تابع یک آرایه حافظه تک بایتی(bytes memory) را برمیگرداند که حاوی محتویات آرگومان ها بدون padding است. اگر می خواهید از پارامترهای رشته یا انواع دیگری استفاده کنید که به طور ضمنی قابل تبدیل به bytes نیستند، ابتدا باید آنها را به bytes یا bytes1/…/bytes32 تبدیل کنید.
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.12; contract C { string s = "Storage"; function f(bytes calldata bc, string memory sm, bytes16 b) public view { string memory concatString = string.concat(s, string(bc), "Literal", sm); assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length); bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b); assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length); } }
اگر بدون آرگومان bytes.concat فراخوانی کنید، آرایهای خالی از bytes را برمیگرداند.
تخصیص آرایه های مِمُوری
آرایههای مِمُوری با طول پویا را میتوان با استفاده از عملگر new ایجاد کرد. در مقایسه با آرایههای ذخیرهسازی، تغییر اندازه آرایههای مِمُوری امکان پذیر نیست (به عنوان مثال توابع عضو push. در دسترس نیستند). یا باید اندازه مورد نیاز را از قبل محاسبه کنید یا یک آرایه مِمُوری جدید ایجاد کنید و هر عنصر را کپی کنید.
مثل همهِ متغیرها در سالیدیتی، عناصر آرایههای تازه تخصیص یافته همیشه با مقدار پیش فرض مقداردهی میشوند.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract C { function f(uint len) public pure { uint[] memory a = new uint[](7); bytes memory b = new bytes(len); assert(a.length == 7); assert(b.length == len); a[6] = 8; } }
آرایههای لیترال
آرایه لیترال لیستی جدا شده با کاما از یک یا چند عبارت است که در بِراکت مربعی محصور شده است ([…]). به عنوان مثال [1, a, f(3)]. نوع آرایه به صورت زیر تعیین میشود:
همیشه یک آرایه مِمُوری با اندازه ایستا است که طول آن تعداد عبارات است.
نوع پایهی آرایه، نوع اولین عبارت در لیست است به طوری که میتوان بقیه عبارات را به طور ضمنی به آن تبدیل کرد. اگر این امکان وجود نداشته باشد خطای نوع است.
کافی نیست نوعی وجود داشته باشد که همه عناصر بتوانند به آن تبدیل شوند. یکی از عناصر باید از آن نوع باشد. در مثال زیر، نوع [1, 2, 3] ، uint8[3] memory میباشد، زیرا نوع هر یک از این ثابتها uint8 است. اگر میخواهید نتیجه از نوع uint8[3] memory باشد، باید اولین عنصر را به uint8 تبدیل کنید.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract C { function f() public pure { g([uint(1), 2, 3]); } function g(uint[3] memory) public pure { // ... } }
آرایه لیترال [1-,1] نامعتبر است زیرا نوع عبارت اول uint8 است در حالی که نوع دوم int8 است و نمیتوان آنها را به طور ضمنی به یکدیگر تبدیل کرد. برای استفاده از آن، میتوانید از [int8(1),-1] استفاده کنید.
از آنجا که آرایههای مِمُوری با اندازه ثابت از انواع مختلف قابل تبدیل به یکدیگر نیستند (حتی اگر انواع پایه بتوانند)، اگر میخواهید از لیترالهای دو بعدی استفاده کنید، باید یک نوع پایه مشترک را به طور صریح مشخص کنید:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract C { function f() public pure returns (uint24[2][4] memory) { uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]]; // The following does not work, because some of the inner arrays are not of the right type. // uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]]; return x; } }
آرایه های مِمُوری با اندازه ثابت را نمیتوان به آرایههای مِمُوری با اندازه پویا اختصاص داد، یعنی موارد زیر امکان پذیر نیست:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; // This will not compile. contract C { function f() public { // The next line creates a type error because uint[3] memory // cannot be converted to uint[] memory. uint[] memory x = [uint(1), 3, 4]; } }
در آینده برنامه ریزی شدهاست که سالیدیتی این محدودیت را برطرف کند، اما به دلیل نحوهِ انتقال آرایهها در ABI، مشکلاتی ایجاد میشود.
اگر میخواهید آرایههایی با اندازه پویا را شروع کنید، باید عناصر جداگانه را اختصاص دهید:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; contract C { function f() public pure { uint[] memory x = new uint[](3); x[0] = 1; x[1] = 3; x[2] = 4; } }
اعضای آرایه
Length:
آرایهها دارای یک عضو length هستند که شامل تعداد عناصر آنها است. طول آرایههای مِمُوری پس از ایجاد ثابت است (اما پویا، یعنی میتواند به پارامترها در زمان اجرا بستگی داشته باشد).
()push:
آرایههای ذخیرهسازی و bytes پویا (نه string ) دارای یک عضو تابع به نام ()push هستند که میتوانید از آن برای افزودن یک عنصر مقداردهی شده صفر در انتهای آرایه استفاده کنید. یک ارجاع به عنصر را برمیگرداند، بنابراین میتوان از آن مانند x.push().t = 2 یا x.push() = b استفاده کرد.
push(x):
آرایههای ذخیرهسازی و bytes پویا (نه string ) دارای یک عضو تابع به نام push(x) هستند که میتوانید از آن برای افزودن یک عنصر مشخص در انتهای آرایه استفاده کنید. تابع هیچ چیزی بر نمیگرداند.
()pop:
آرایههای ذخیرهسازی و bytes پویا (نه string ) دارای یک عضو تابع به نام ()pop هستند که میتوانید برای حذف یک عنصر از انتهای آرایه استفاده کنید. همچنین به طور ضمنی delete را روی عنصر حذف شده فراخوانی میکند.
توجه داشته باشید
افزایش طول یک آرایه ذخیرهسازی با فراخوانی ()push هزینه گس ثابت را دارد زیرا مقداردهی اولیه ذخیرهسازی صفر میباشد، در حالی که کاهش طول با فراخوانی ()pop هزینهای دارد که به “اندازه” عنصر حذف شده بستگی دارد. اگر آن عنصر آرایهای باشد، میتواند بسیار پرهزینه باشد، زیرا شامل پاک کردن صریح عناصر حذف شده مشابه با فراخوانی delete روی آنها است.
توجه داشته باشید
برای استفاده از آرایه های آرایه ها در توابع خارجی (به جای عمومی)، باید ABI coder v2 را فعال کنید.
توجه داشته باشید
در نسخههای EVM قبل از Byzantium، دسترسی به آرایههای پویا از برگشتیِ توابعِ فراخوانی امکان پذیر نبود. اگر توابعی را فراخوانی میکنید که آرایههای پویا را برمیگردانند، حتماً از EVMی استفاده کنید که روی حالت Byzantium تنظیم شدهاست.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; contract ArrayContract { uint[2**20] aLotOfIntegers; // Note that the following is not a pair of dynamic arrays but a // dynamic array of pairs (i.e. of fixed size arrays of length two). // In Solidity, T[k] and T[] are always arrays with elements of type T, // even if T itself is an array. // Because of that, bool[2][] is a dynamic array of elements // that are bool[2]. This is different from other languages, like C. // Data location for all state variables is storage. bool[2][] pairsOfFlags; // newPairs is stored in memory - the only possibility // for public contract function arguments function setAllFlagPairs(bool[2][] memory newPairs) public { // assignment to a storage array performs a copy of ``newPairs`` and // replaces the complete array ``pairsOfFlags``. pairsOfFlags = newPairs; } struct StructType { uint[] contents; uint moreInfo; } StructType s; function f(uint[] memory c) public { // stores a reference to ``s`` in ``g`` StructType storage g = s; // also changes ``s.moreInfo``. g.moreInfo = 2; // assigns a copy because ``g.contents`` // is not a local variable, but a member of // a local variable. g.contents = c; } function setFlagPair(uint index, bool flagA, bool flagB) public { // access to a non-existing index will throw an exception pairsOfFlags[index][0] = flagA; pairsOfFlags[index][1] = flagB; } function changeFlagArraySize(uint newSize) public { // using push and pop is the only way to change the // length of an array if (newSize < pairsOfFlags.length) { while (pairsOfFlags.length > newSize) pairsOfFlags.pop(); } else if (newSize > pairsOfFlags.length) { while (pairsOfFlags.length < newSize) pairsOfFlags.push(); } } function clear() public { // these clear the arrays completely delete pairsOfFlags; delete aLotOfIntegers; // identical effect here pairsOfFlags = new bool[2][](0); } bytes byteData; function byteArrays(bytes memory data) public { // byte arrays ("bytes") are different as they are stored without padding, // but can be treated identical to "uint8[]" byteData = data; for (uint i = 0; i < 7; i++) byteData.push(); byteData[3] = 0x08; delete byteData[2]; } function addFlag(bool[2] memory flag) public returns (uint) { pairsOfFlags.push(flag); return pairsOfFlags.length; } function createMemoryArray(uint size) public pure returns (bytes memory) { // Dynamic memory arrays are created using `new`: uint[2][] memory arrayOfPairs = new uint[2][](size); // Inline arrays are always statically-sized and if you only // use literals, you have to provide at least one type. arrayOfPairs[0] = [uint(1), 2]; // Create a dynamic byte array: bytes memory b = new bytes(200); for (uint i = 0; i < b.length; i++) b[i] = bytes1(uint8(i)); return b; } }
ارجاعات آویزان به عناصر آرایه ذخیره سازی
هنگام کار با آرایه های ذخیره سازی، باید مراقب باشید که از ارجاعات آویزان خودداری کنید. مرجع آویزان مرجعی است که به چیزی اشاره میکند که دیگر وجود ندارد یا بدون به روزرسانی مرجع منتقل شده است. به عنوان مثال، اگر یک مرجع را در یک عنصر آرایه در یک متغیر محلی و سپس ()pop. را از آرایه حاوی ذخیره کنید، ممکن است مرجع آویزان رخ دهد:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; contract C { uint[][] s; function f() public { // Stores a pointer to the last array element of s. uint[] storage ptr = s[s.length - 1]; // Removes the last array element of s. s.pop(); // Writes to the array element that is no longer within the array. ptr.push(0x42); // Adding a new element to ``s`` now will not add an empty array, but // will result in an array of length 1 with ``0x42`` as element. s.push(); assert(s[s.length - 1][0] == 0x42); } }
علیرغم این واقعیت که ptr به یک عنصر معتبر از s اشاره نمیکند، نوشتن در ptr.push(0x42) برگردانده نمیشود. از آنجایی که کامپایلر فرض میکند که فضای ذخیرهسازی استفاده نشده همیشه صفر است، ()s.push بعدی به طور صریح صفرها را در فضای ذخیرهسازی نمینویسد، بنابراین آخرین عنصر s بعد از آن ()push دارای طول 1 و شامل 0x42 به عنوان عنصر اول خود خواهد بود.
توجه داشته باشید که سالیدیتی اجازه اعلان ارجاع به انواع مقادیر در ذخیره سازی را نمی دهد. این نوع ارجاعات آویزان صریح به انواع مرجع تو در تو محدود می شود. با این حال، هنگام استفاده از عبارات پیچیده در تکالیف چندتایی، ارجاعات آویزان نیز می توانند به طور موقت رخ دهند:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; contract C { uint[] s; uint[] t; constructor() { // Push some initial values to the storage arrays. s.push(0x07); t.push(0x03); } function g() internal returns (uint[] storage) { s.pop(); return t; } function f() public returns (uint[] memory) { // The following will first evaluate ``s.push()`` to a reference to a new element // at index 1. Afterwards, the call to ``g`` pops this new element, resulting in // the left-most tuple element to become a dangling reference. The assignment still // takes place and will write outside the data area of ``s``. (s.push(), g()[0]) = (0x42, 0x17); // A subsequent push to ``s`` will reveal the value written by the previous // statement, i.e. the last element of ``s`` at the end of this function will have // the value ``0x42``. s.push(); return s; } }
همیشه امنتر است که در هر عبارت فقط یک بار به ذخیرهسازی اختصاص داده شود و از عبارات پیچیده در سمت چپ یک تکلیف اجتناب شود.
هنگام برخورد با ارجاعات به عناصر آرایه های bytes، باید مراقب باشید، زیرا یک ()push. روی آرایه بایت ممکن است در فضای ذخیره سازی از طرح بندی کوتاه به طرح بلند تغییر کند.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; // This will report a warning contract C { bytes x = "012345678901234567890123456789"; function test() external returns(uint) { (x.push(), x.push()) = (0x01, 0x02); return x.length; } }
در اینجا، هنگامی که اولین ()x.push ارزیابی می شود، x همچنان در طرح بندی کوتاه ذخیره می شود، در نتیجه ()x.push ارجاع به یک عنصر در اولین شکاف ذخیره سازی x را برمی گرداند. با این حال، ()x.push دومین آرایه بایت را به طرحبندی بزرگ تغییر می دهد. اکنون عنصری که ()x.push به آن اشاره کرده است در ناحیه داده آرایه است در حالی که مرجع هنوز به محل اصلی خود اشاره می کند، که اکنون بخشی از فیلد طول است و انتساب به طور موثر طول x را مخدوش می کند. برای ایمن بودن، فقط آرایههای بایت را حداکثر تا یک عنصر در طول یک انتساب بزرگ کنید و به طور همزمان به آرایه در همان عبارت (index-access) دسترسی نمایه نکنید.
برشهای آرایه
برشهای آرایه نمایی از قسمت پیوسته آرایه است. آنها به صورت x[start:end] نوشته میشوند، جایی که start و end عباراتی هستند که منجر به نوع uint256 میشوند (یا به طور ضمنی قابل تبدیل به آن هستند). اولین عنصر برش x[start] و آخرین عنصر x[end – 1] میباشد.
اگر start از end بیشتر باشد یا اگر end از طول آرایه بیشتر باشد، یک استثنا ایجاد میشود. start و end هر دو اختیاری هستند: start به طور پیشفرض 0 و end به طور پیش فرض به طول آرایه میباشد.
برشهای آرایه هیچ عضوی ندارند. آنها به طور ضمنی قابل تبدیل به آرایههایی از نوع اصلی و دسترسی به index را پشتیبانی میکنند. دسترسی index در آرایه اصلی قطعی نیست، اما وابسته به شروع برش است.
برشهای آرایه دارای نام نوع نیستند، به این معنی که هیچ متغیری نمیتواند برشهای آرایهای را به عنوان نوع داشته باشد، آنها فقط در عبارات میانی وجود دارند.
توجه داشته باشید
از هم اکنون، برشهای آرایه فقط برای آرایههای فراخوانی داده پیاده سازی میشوند.
برشهای آرایه برای رمزگشایی با دادههای ثانویه ABI که در پارامترهای تابع منتقل میشوند مفید هستند:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.5 <0.9.0; contract Proxy { /// @dev Address of the client contract managed by proxy i.e., this contract address client; constructor(address client_) { client = client_; } /// Forward call to "setOwner(address)" that is implemented by client /// after doing basic validation on the address argument. function forward(bytes calldata payload) external { bytes4 sig = bytes4(payload[:4]); // Due to truncating behaviour, bytes4(payload) performs identically. // bytes4 sig = bytes4(payload); if (sig == bytes4(keccak256("setOwner(address)"))) { address owner = abi.decode(payload[4:], (address)); require(owner != address(0), "Address of owner cannot be zero."); } (bool status,) = client.delegatecall(payload); require(status, "Forwarded call failed."); } }
ساختارها
سالیدیتی راهی برای تعریف انواع جدید در قالب ساختار ارائه می دهد که در مثال زیر نشان داده شده است:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; // Defines a new type with two fields. // Declaring a struct outside of a contract allows // it to be shared by multiple contracts. // Here, this is not really needed. struct Funder { address addr; uint amount; } contract CrowdFunding { // Structs can also be defined inside contracts, which makes them // visible only there and in derived contracts. struct Campaign { address payable beneficiary; uint fundingGoal; uint numFunders; uint amount; mapping (uint => Funder) funders; } uint numCampaigns; mapping (uint => Campaign) campaigns; function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) { campaignID = numCampaigns++; // campaignID is return variable // We cannot use "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)" // because the right hand side creates a memory-struct "Campaign" that contains a mapping. Campaign storage c = campaigns[campaignID]; c.beneficiary = beneficiary; c.fundingGoal = goal; } function contribute(uint campaignID) public payable { Campaign storage c = campaigns[campaignID]; // Creates a new temporary memory struct, initialised with the given values // and copies it over to storage. // Note that you can also use Funder(msg.sender, msg.value) to initialise. c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value}); c.amount += msg.value; } function checkGoalReached(uint campaignID) public returns (bool reached) { Campaign storage c = campaigns[campaignID]; if (c.amount < c.fundingGoal) return false; uint amount = c.amount; c.amount = 0; c.beneficiary.transfer(amount); return true; } }
قرارداد کارکرد کامل، قرارداد تامین مالی جمعی را ارائه نمی کند، اما حاوی مفاهیم اساسی لازم برای درک ساختارها است. انواع ساختارها را می توان در داخل نقشه ها و آرایه ها استفاده کرد و خود می توانند حاوی نگاشت ها و آرایه ها باشند.
این امکان وجود ندارد که یک ساختار دارای عضوی از نوع خود باشد، اگرچه خود ساختار میتواند نوع مقدار یک عضو نگاشت یا میتواند دارای یک آرایه با اندازه پویا از نوع خود باشد. این محدودیت ضروری است، زیرا اندازه ساختار باید محدود باشد.
توجه داشته باشید که چگونه در همه توابع، یک نوع ساختار به یک متغیر محلی با storage مکان داده اختصاص داده میشود. این کار ساختار را کپی نمیکند، بلکه فقط یک مرجع را ذخیره میکند تا انتساب به اعضای متغیر محلی در state بنویسد.
البته، شما همچنین می توانید مستقیماً به اعضای ساختار دسترسی داشته باشید بدون اینکه آن را به یک متغیر محلی اختصاص دهید، مانند campaigns[campaignID].amount = 0
توجه داشته باشید
تا قبل از Solidity 0.7.0، ساختارهای حافظه حاوی اعضایی از انواع ذخیرهسازی (مثلاً mappings) مجاز بودند و تکالیفی مانند کمپینها campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0) در مثال بالا کار میکردند و فقط بیصدا از آن اعضا رد میشدند.
انواع نگاشتها (Mapping )
انواع نگاشت از سینتکس mapping(KeyType => ValueType) استفاده میکنند و متغیرهای نوع نگاشت با استفاده از سینتکس mapping(KeyType => ValueType) VariableName مشخص میشوند. KeyType میتواند هر نوع مقدار داخلی، bytes، string یا هر نوع قرارداد یا enum باشد. سایر نوعهای پیچیده یا تعریف شده توسط کاربر، مانند نگاشت، ساختارها یا انواع آرایه مجاز نیستند . ValueType میتواند هر نوعی باشد، از جمله نگاشتها، آرایهها و ساختارها.
میتوانید نگاشتها را به صورت جداول هش در نظر بگیرید که عملاً مقداردهی اولیه میشوند به گونهای که هر کلید امکان وجود دارد و به مقداری نگاشت میشود که پیش نمایش بایت آن صفر است،(یک نوع مقدار پیش فرض.) شباهت در اینجا پایان مییابد، دادههای کلیدی در نگاشت ذخیره نمیشوند، فقط از هش keccak256 برای جستجوی مقدار استفاده میشود.
به همین دلیل، نگاشتها طول یا مفهومی از کلید یا مقدار تنظیم شده ندارند و بنابراین بدون اطلاعات اضافی در مورد کلیدهای اختصاص داده شده پاک نمیشوند (به پاکسازی نگاشت مراجعه کنید).
نگاشتها فقط میتوانند یک مکان داده از storage را داشته باشند و بنابراین برای متغیرهای حالت، به عنوان نوعهای مرجع storage در توابع، یا به عنوان پارامترهای توابع کتابخانه مجاز هستند. آنها نمیتوانند به عنوان پارامترها یا پارامترهای بازگشتی توابع قرارداد که در معرض دید عموم قرار دارند، مورد استفاده قرار گیرند. این محدودیتها برای آرایهها و ساختارهای حاوی نگاشت نیز صادق است.
شما میتوانید متغیرهای حالت از نوع نگاشت را به صورت public علامتگذاری کنید و سالیدیتی یک گیرنده (getter) برای شما ایجاد میکند. KeyType به یک پارامتر برای getter تبدیل میشود. اگر ValueType یک مقدار نوع یا یک struct، getter ValueType را برمیگرداند. اگر ValueType یک آرایه یا نگاشت باشد،getter به صورت بازگشتی برای هر KeyType یک پارامتر دارد.
در مثال زیر، قرارداد MappingExample یک نگاشت از balances عمومی را تعریف میکند، با نوع کلید یک address و یک نوع مقدار یک uint، و یک آدرس اتریوم را به یک مقدار صحیح بدون علامت نگاشت میکند. از آنجا که uint یک نوع مقدار است، گیرنده مقداری را متناسب با نوع آن برمیگرداند که میتوانید آن را در قرارداد MappingUser مشاهده کنید که مقدار را در آدرس مشخص شده برمیگرداند.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract MappingExample { mapping(address => uint) public balances; function update(uint newBalance) public { balances[msg.sender] = newBalance; } } contract MappingUser { function f() public returns (uint) { MappingExample m = new MappingExample(); m.update(100); return m.balances(address(this)); } }
مثال زیر یک نسخه ساده شده از توکن ERC20 است. allowances_ نمونه ای از یک نوع نگاشت در داخل یک نوع نقشه برداری دیگر است. مثال زیر از allowances_ برای ثبت مبلغی استفاده میکند که شخص دیگری مجاز است از حساب شما برداشت کند.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; contract MappingExample { mapping (address => uint256) private _balances; mapping (address => mapping (address => uint256)) private _allowances; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); function allowance(address owner, address spender) public view returns (uint256) { return _allowances[owner][spender]; } function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) { require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough."); _allowances[sender][msg.sender] -= amount; _transfer(sender, recipient, amount); return true; } function approve(address spender, uint256 amount) public returns (bool) { require(spender != address(0), "ERC20: approve to the zero address"); _allowances[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function _transfer(address sender, address recipient, uint256 amount) internal { require(sender != address(0), "ERC20: transfer from the zero address"); require(recipient != address(0), "ERC20: transfer to the zero address"); require(_balances[sender] >= amount, "ERC20: Not enough funds."); _balances[sender] -= amount; _balances[recipient] += amount; emit Transfer(sender, recipient, amount); } }
نگاشتهای تکرارپذیر
نمیتوانید بر روی نگاشتها تکرار کنید، یعنی نمیتوانید کلیدهای آنها را بشمارید.گرچند امکان اجرای یک ساختار داده در بالای آنها و تکرار آن وجود دارد. به عنوان مثال، کد زیر یک کتابخانه IterableMapping را پیادهسازی میکند که قرارداد User دادهها را نیز اضافه میکند و تابع sum تکرار میشود تا تمام مقادیر را جمع کند.
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.8; struct IndexValue { uint keyIndex; uint value; } struct KeyFlag { uint key; bool deleted; } struct itmap { mapping(uint => IndexValue) data; KeyFlag[] keys; uint size; } type Iterator is uint; library IterableMapping { function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) { uint keyIndex = self.data[key].keyIndex; self.data[key].value = value; if (keyIndex > 0) return true; else { keyIndex = self.keys.length; self.keys.push(); self.data[key].keyIndex = keyIndex + 1; self.keys[keyIndex].key = key; self.size++; return false; } } function remove(itmap storage self, uint key) internal returns (bool success) { uint keyIndex = self.data[key].keyIndex; if (keyIndex == 0) return false; delete self.data[key]; self.keys[keyIndex - 1].deleted = true; self.size --; } function contains(itmap storage self, uint key) internal view returns (bool) { return self.data[key].keyIndex > 0; } function iterateStart(itmap storage self) internal view returns (Iterator) { return iteratorSkipDeleted(self, 0); } function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) { return Iterator.unwrap(iterator) < self.keys.length; } function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) { return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1); } function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) { uint keyIndex = Iterator.unwrap(iterator); key = self.keys[keyIndex].key; value = self.data[key].value; } function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) { while (keyIndex < self.keys.length && self.keys[keyIndex].deleted) keyIndex++; return Iterator.wrap(keyIndex); } } // How to use it contract User { // Just a struct holding our data. itmap data; // Apply library functions to the data type. using IterableMapping for itmap; // Insert something function insert(uint k, uint v) public returns (uint size) { // This calls IterableMapping.insert(data, k, v) data.insert(k, v); // We can still access members of the struct, // but we should take care not to mess with them. return data.size; } // Computes the sum of all stored data. function sum() public view returns (uint s) { for ( Iterator i = data.iterateStart(); data.iterateValid(i); i = data.iterateNext(i) ) { (, uint value) = data.iterateGet(i); s += value; } } }
اپراتورها
عملگرهای حسابی و بیتی را می توان اعمال کرد حتی اگر دو عملوند یک نوع نداشته باشند. به عنوان مثال، می توانید y = x + z را محاسبه کنید، جایی که x یک uint8 است و z دارای نوع uint32 است. در این موارد، مکانیسم زیر برای تعیین نوع محاسبه عملیات (در صورت سرریز شدن مهم است) و نوع نتیجه اپراتور استفاده خواهد شد:
- اگر می توان نوع عملوند راست را به طور ضمنی به نوع عملوند چپ تبدیل کرد، از نوع عملوند چپ استفاده کنید.
- اگر می توان نوع عملوند چپ را به طور ضمنی به نوع عملوند راست تبدیل کرد، از نوع عملوند راست استفاده کنید.
- در غیر این صورت، عملیات مجاز نیست.
در صورتی که یکی از عملوندها یک عدد لیترال باشد، ابتدا به “نوع متحرک (mobile type)” آن تبدیل می شود، که کوچکترین نوع است که میتواند مقدار را نگه دارد (انواع بدون علامت با همان عرض بیت “کوچکتر” از انواع علامت دار در نظر گرفته می شوند) . اگر هر دو اعداد لیترال باشند، عملیات با دقت نامحدودی محاسبه میشود، بدین صورت که عبارت با هر دقتی که لازم است ارزیابی میشود، به طوری که هنگام استفاده از نتیجه با نوع غیر لیترال، هیچ کدام از بین نمیرود.
نوع نتیجه عملگر همان نوع عملی است که عملیات در آن انجام می شود، به جز برای عملگرهای مقایسه که نتیجه همیشه bool است.
عملگرهای ** (نمایش)، << و >> از نوع عملوند سمت چپ برای عملیات و نتیجه استفاده میکنند.
اپراتور سه تایی
عملگر سه تایی در عباراتی به شکل زیر استفاده میشود: ? :
بسته به نتیجه ارزیابی اصلی، یکی از دو عبارت اخیر را ارزیابی می کند. اگر به درستی ارزیابی شود، ارزیابی می شود، در غیر این صورت ارزیابی می شود.
نتیجه عملگر سه تایی دارای یک نوع عدد گویا نیست، حتی اگر همه عملوندهای آن حرف اعداد گویا باشند. نوع نتیجه از انواع دو عملوند به همان روش بالا تعیین می شود و در صورت لزوم ابتدا به نوع متحرک آنها تبدیل می شود.
در نتیجه، (true ? 1 : 0) + 255 به دلیل سرریز حسابی برمی گردد. دلیل آن این است که (true ? 1 : 0) از نوع uint8 است که باعث می شود جمع در uint8 نیز انجام شود و 256 از محدوده مجاز برای این نوع فراتر می رود.
نتیجه دیگر این است که عبارتی مانند 1.5 + 1.5 معتبر است اما (true ? 1.5 : 2.5) + 1.5 معتبر نیست. دلیلش این است که اولی عبارتی عقلانی است که با دقت نامحدود ارزیابی شده و فقط ارزش نهایی آن مهم است. دومی شامل تبدیل یک عدد گویا کسری به یک عدد صحیح است که در حال حاضر مجاز نیست.
اپراتورهای مرکب و افزایشی/کاهشی
اگر a یک LValue باشد (به عنوان مثال یک متغیر یا چیزی که میتوان به آن اختصاص داد)، عملگرهای زیر به صورت مختصر در دسترس هستند:
a += e معادل a = a + e است. عملگرها =-،=*، =/، =%، =|، =& ، =^، <<= و >>= بر این اساس تعریف میشوند. ++a و –a معادل a += 1 / a -= 1 هستند اما این عبارت هنوز مقدار قبلی a را دارد. در مقابل ، a– و a++ تأثیر یکسانی در a دارند اما مقدار را پس از تغییر برمیگردانند.
حذف
delete a یک مقدار اولیه را برای نوع، به a اختصاص میدهد. یعنی برای اعداد صحیح معادل a = 0 است، اما همچنین میتواند در آرایه ها مورد استفاده قرار گیرد، جایی که یک آرایه پویا از طول صفر یا یک آرایه ایستا با همان طول را با تمام عناصر تنظیم شده روی مقدار اولیه خود اختصاص میدهد. delete a[x] مورد را در شاخص x آرایه حذف میکند و سایر عناصر و طول آرایه را دست نخورده باقی میگذارد. این کار به طور ویژه به این معنی است که در آرایه شکاف ایجاد میکند. اگر قصد حذف موارد را دارید، نگاشت احتمالاً انتخاب بهتری است.
برای ساختارها، یک ساختار را با تنظیم مجدد همه اعضا اختصاص میدهد. به عبارت دیگر، مقدار a پس از delete a همانی است که اگر a بدون انتساب اعلام شود، با توجه به هشدار زیر:
delete تأثیری در نگاشت ندارد (زیرا ممکن است کلیدهای نگاشت دلخواه باشند و به طور کلی ناشناخته باشند). بنابراین اگر یک ساختار را حذف کنید، همه اعضا را که نگاشت نباشند مجدد تنظیم میکند و همچنین به عضوها بازگشت مییابد مگر اینکه آنها نگاشت باشند. با این حال، کلیدهای جداگانه و به آنچه نگاشت میشوند میتوانند حذف شوند: اگر a نگاشت باشد، سپس delete a[x] مقدار ذخیره شده در x را حذف خواهد کرد.
توجه به این نکته مهم است که delete a واقعاً مانند انتساب به a رفتار میکند، یعنی یک شی جدید را در a ذخیره میکند. این تمایز زمانی قابل مشاهده است که a متغیر مرجع باشد: فقط یک a را خود مجدد تنظیم میکند نه مقداری که قبلاً به آن اشاره کرده بود.
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract DeleteExample { uint data; uint[] dataArray; function f() public { uint x = data; delete x; // sets x to 0, does not affect data delete data; // sets data to 0, does not affect x uint[] storage y = dataArray; delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also // y is affected which is an alias to the storage object // On the other hand: "delete y" is not valid, as assignments to local variables // referencing storage objects can only be made from existing storage objects. assert(y.length == 0); } }
ترتیب اولویت اپراتورها
در زیر ترتیب اولویت برای اپراتورها آمده است که به ترتیب ارزیابی فهرست شده است.
Precedence | Description | Operator |
---|---|---|
1 | Postfix increment and decrement |
|
New expression |
| |
Array subscripting |
| |
Member access |
| |
Function-like call |
| |
Parentheses |
| |
2 | Prefix increment and decrement |
|
Unary minus |
| |
Unary operations |
| |
Logical NOT |
| |
Bitwise NOT |
| |
3 | Exponentiation |
|
4 | Multiplication, division and modulo |
|
5 | Addition and subtraction |
|
6 | Bitwise shift operators |
|
7 | Bitwise AND |
|
8 | Bitwise XOR |
|
9 | Bitwise OR |
|
10 | Inequality operators |
|
11 | Equality operators |
|
12 | Logical AND |
|
13 | Logical OR |
|
14 | Ternary operator |
|
Assignment operators |
| |
15 | Comma operator |
|
تبدیل بین نوعهای اصلی
تبدیل ضمنی
یک تبدیل نوع ضمنی به طور خودکار توسط کامپایلر در برخی موارد در طول انتساب، هنگام ارسال آرگومان ها به توابع و هنگام اعمال عملگرها اعمال می شود. به طور کلی، تبدیل ضمنی بین انواع مقدار در صورتی امکان پذیر است که از نظر معنایی معنا داشته باشد و هیچ اطلاعاتی از بین نرود.
به عنوان مثال، uint8 به uint16 و int128 به int256 قابل تبدیل است، اما int8 به uint256 قابل تبدیل نیست، زیرا uint256 نمیتواند مقادیری مانند 1- را نگه دارد.
اگر یک عملگر بر روی نوعهای مختلف اعمال شود، کامپایلر سعی میکند به طور ضمنی یکی از عملوندها را به نوع دیگری تبدیل کند (این امر برای انتسابها نیز صادق است). این بدان معناست که عملیات همیشه در نوع یکی از عملوندها انجام میشود.
برای جزئیات بیشتر در مورد اینکه کدام یک از تغییرات ضمنی امکان پذیر است، لطفاً با بخشهایی هر نوع مراجعه کنید.
در مثال زیر، y و z، عملوندهای جمع، یک نوع ندارند، اما uint8 را میتوان به طور ضمنی به uint16 تبدیل کرد و نه بالعکس. به همین دلیل، y قبل از اینکه جمع در نوع uint16 انجام شود، به نوع z تبدیل میشود. uint16 نوع حاصل از عبارت y + z است. از آنجا که به یک متغیر از نوع uint32 اختصاص داده شده است، تبدیل ضمنی دیگر پس از جمع انجام میشود.
uint8 y; uint16 z; uint32 x = y + z;
تبدیلهای صریح
اگر کامپایلر اجازه تبدیل ضمنی را ندهد اما اطمینان دارید که یک تبدیل کار میکند، تبدیل صریح نوع گاهی اوقات امکان پذیر است. این ممکن است منجر به یک رفتار غیر منتظره شود و به شما امکان میدهد برخی از ویژگیهای امنیتی کامپایلر را دور بزنید، بنابراین مطمئن شوید که نتیجه همان چیزی است که شما میخواهید و انتظار دارید! مثال زیر را در نظر بگیرید که int منفی را به uint تبدیل میکند:
int y = -3; uint x = uint(y);
در پایان این قطعه کد، x دارای مقدار 0xfffff..fd (64 کاراکتر هگز) را خواهد داشت که در نمایش مکمل این دو با 256 بیت برابر با 3- است.
اگر یک عدد صحیح صریح به یک نوع کوچکتر تبدیل شود، بیتهای مرتبه بالاتر بریده میشوند:
uint32 a = 0x12345678; uint16 b = uint16(a); // b will be 0x5678 now
اگر یک عدد صحیح صریح به یک نوع بزرگتر تبدیل شود، در سمت چپ (یعنی در انتهای مرتبه بالاتر) قرار میگیرد. نتیجه تبدیل برابر با عدد صحیح اصلی خواهد بود:
uint16 a = 0x1234; uint32 b = uint32(a); // b will be 0x00001234 now assert(a == b);
انواع بایتهای اندازه ثابت در هنگام تبدیل متفاوت عمل میکنند. میتوان آنها را به عنوان دنبالهای از بایتهای منفرد در نظر گرفت و تبدیل به نوع کوچکتر، دنباله را قطع میکند:
bytes2 a = 0x1234; bytes1 b = bytes1(a); // b will be 0x12
اگر یک نوع بایت با اندازه ثابت صراحتاً به یک نوع بزرگتر تبدیل شود، در سمت راست قرار می گیرد. دسترسی به بایت در یک شاخص ثابت به همان مقدار قبل و بعد از تبدیل منجر می شود (اگر شاخص هنوز در محدوده باشد):
bytes2 a = 0x1234; bytes4 b = bytes4(a); // b will be 0x12340000 assert(a[0] == b[0]); assert(a[1] == b[1]);
از آنجایی که اعداد صحیح و آرایههای بایت با اندازه ثابت رفتار متفاوتی در هنگام کوتاه کردن یا اضافه کردن دارند، تبدیل صریح بین اعداد صحیح و آرایههای بایت با اندازه ثابت تنها در صورتی مجاز است که هر دو اندازه یکسان داشته باشند. اگر میخواهید بین اعداد صحیح و آرایههای بایت با اندازه ثابت، اندازههای مختلف تبدیل کنید، باید از تبدیلهای میانی استفاده کنید که قوانین کوتاهسازی و padding مورد نظر را واضح میسازد:
bytes2 a = 0x1234; uint32 b = uint16(a); // b will be 0x00001234 uint32 c = uint32(bytes4(a)); // c will be 0x12340000 uint8 d = uint8(uint16(a)); // d will be 0x34 uint8 e = uint8(bytes1(a)); // e will be 0x12
آرایههای bytes و تکههای داده فراخوانی bytes را میتوان به طور صریح به انواع بایتهای ثابت (bytes1/…/ bytes32) تبدیل کرد. در صورتی که آرایه طولانیتر از نوع بایتهای ثابت هدف باشد، برش در انتها اتفاق میافتد. اگر آرایه کوتاهتر از نوع هدف باشد، در انتها با صفر پر میشود.
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.5; contract C { bytes s = "abcdefgh"; function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) { require(c.length == 16, ""); bytes16 b = bytes16(m); // if length of m is greater than 16, truncation will happen b = bytes16(s); // padded on the right, so result is "abcdefgh\0\0\0\0\0\0\0\0" bytes3 b1 = bytes3(s); // truncated, b1 equals to "abc" b = bytes16(c[:8]); // also padded with zeros return (b, b1); } }
تبدیل بین لیترالها و نوعهای اصلی
انواع عدد صحیح
لیترالهای عدد دسیمال و هگزادسیمال را میتوان به طور ضمنی به هر نوع عدد صحیحی که به اندازه کافی بزرگ باشد و بتوان آن را بدون کوتاه سازی نشان داد، تبدیل کرد:
uint8 a = 12; // fine uint32 b = 1234; // fine uint16 c = 0x123456; // fails, since it would have to truncate to 0x3456
توجه داشته باشید
قبل از نسخه 0.8.0، هر عدد لیترال اعشاری یا هگزا دسیمال میتواند به طور ضمنی به یک نوع صحیح تبدیل شود. از 0.8.0 به بعد چنین تبدیلهای صریحی به اندازه تبدیلهای ضمنی سختگیرانه هستند، یعنی تنها در صورتی مجاز هستند که کلمه لیترال در محدوده حاصله مطابقت داشته باشد.
آرایههای بایت با اندازه ثابت
اعداد اعشاری لیترال را نمیتوان به صورت ضمنی به آرایههای بایت با اندازه ثابت تبدیل کرد. میتواند لیترالهای عددی هگزادسیمال باشد، اما فقط در صورتی که تعداد ارقام هگز دقیقاً متناسب با اندازه نوع بایت باشد. به عنوان یک استثنا، هر دو لیترالهای دسیمال و هگزادسیمال که مقدار آنها صفر است، میتوانند به هر نوع بایت با اندازه ثابت تبدیل شوند:
bytes2 a = 54321; // not allowed bytes2 b = 0x12; // not allowed bytes2 c = 0x123; // not allowed bytes2 d = 0x1234; // fine bytes2 e = 0x0012; // fine bytes4 f = 0; // fine bytes4 g = 0x0; // fine
در صورتی که تعداد کاراکترهای آنها با اندازه نوع بایت مطابقت داشته باشد، لیترالهای رشتهای و لیترالهای رشتهای هگزی میتوانند به طور ضمنی به آرایههای بایتی با اندازه ثابت تبدیل شوند:
bytes2 a = hex"1234"; // fine bytes2 b = "xy"; // fine bytes2 c = hex"12"; // not allowed bytes2 d = hex"123"; // not allowed bytes2 e = "x"; // not allowed bytes2 f = "xyz"; // not allowed
آدرس ها
همانطور که در آدرس لیترالها (Address Literals) توضیح داده شد، حروف هگزا با اندازه صحیح که از آزمون چکسام عبور میکنند از نوع address هستند. هیچ لیترال دیگری را نمیتوان به طور ضمنی به نوع address تبدیل کرد.
تبدیل صریح به address فقط از bytes20 و uint160 مجاز است.
یک address a را می توان به صراحت به address payable از طریق payable(a) تبدیل کرد.
توجه داشته باشید
قبل از نسخه 0.8.0، امکان تبدیل صریح از هر نوع عدد صحیح (با هر اندازه، امضا شده یا بدون علامت) به address یا address payable وجود داشت. با شروع نسخه 0.8.0 تنها تبدیل از uint160 مجاز است.