کار با رشته‌‌ها در سالیدیتی‌‌ (Solidity)

این مطلب به برنامه‌‌نویسی برای بلاکچین اتریوم با استفاده از زبان سالیدیتی‌‌ می‌پردازد. در انجام این کار از هیچ برنامه‌‌ خاصی پیروی نمی‌‌کند: هدف آن تنها بررسی دو مانع موجود است که در یادگیری زبان سالیدیتی‌‌ و دشواری‌‌های عملی در کار روزانه‌‌ با آن روبرو هستیم.

0 140

دسترسی اساسی

امروز می‌‌خواهم درباره رشته‌‌ها (string) در زبان سالیدیتی‌‌ صحبت کنم. سالیدیتی‌‌ از نظر ترکیب به جاواسکریپت و سایر زبان‌‌های برنامه‌‌‌‌نویسی C شبیه است. به همین دلیل یادگیری سریع کار سالیدیتی‌‌ برای فرد تازه‌‌کاری که پیشینه‌‌ای در هرکدام از زبان‌‌های رایج برنامه‌‌نویسی داشته باشد، چندان دشوار نیست. با این حال سالیدیتی از نظر جزییات زبانی که دشواری پیش‌‌بینی‌‌نشده را مخفی می‌‌کند قوی است. این همان مورد نوع رشته و نوع بایت‌‌های مربوط به آن است.

هر دوی این‌‌ها از نوع ارایه دینامیک هستند، یعنی می‌‌‌‌توانند داده را با اندازه دلخواه ذخیره کنند. هر عنصر از یک متغیر نوع بایت، یک بایت واحد (single) است. هر عنصر از متغیر نوع رشته، ویژگی رشته است. خب تا اینجا که مشکلی وجود ندارد، اما ظاهر همیشه فریبنده است. کسی که پیشینه‌‌ای در زبان‌‌های دیگر داشته باشند، احتمالا انتظار دارد رشته کارکردهای زیادی را ارائه کند، از جمله:

  • تعیین طول رشته
  • خواندن یا تغییر ویژگی در هر بخشی از رشته
  • پیوستن به دو رشته
  • استخراج بخشی از رشته

خبر بد این است که رشته سالیدیتی‌‌ هیچ‌‌کدام از این کارها را انجام نمی‌‌دهد. اگر به هر کدام از موارد بالا احتیاج داشته باشیم باید آن را خودمان دستی انجام دهیم.

پس بیایید به برخی از این دشواری‌‌ها بنگریم و ببینیم چه می‌‌شود با آنان انجام داد. من برنامه Remix را باز می‌‌کنم و کد پایین را در یک فایل جدید به نام string.sol تایپ می‌‌کنم.

 string.sol

در Remix، بخش توسعه‌‌دهنده در سمت راست صفحه قرار دارد. در تب Compile گزینه‌‌ی Auto-Compile را فعال می‌‌کنم تا حین نوشتن کد، Remix خطاها و هشدارهای تحلیل کد را به من نشان دهد. تحلیل کد استاتیک توسط گزینه‌‌هایی که در تب Options هست کنترل می‌‌شود و معمولا همه گزینه‌‌های آن را فعال می‌‌‌‌کنم.

Remix

Remix در مورد کنونی دو هشدار از یک نوع گزارش می‌‌کند: روش‌‌هایی که نوشته‌‌ام می‌‌تواند هزینه‌‌ی سوخت یا Gas بالا تا بینهایتی داشته باشد. حالا در این مطلب از آن چشم‌‌پوشی می‌‌کنم.

در تب Run، گزینه‌‌ی Deploy را می‌‌زنم و اگر قرارداد مشکلی نداشته باشد، قسمت جدیدی در پایین آن دکمه ظاهر خواهد شد که هم آدرس قرارگیری قرارداد و هم کارکردهای موجود در آن وجود دارد.

Remix

Remix در ذیل قسمت کار، سابقه‌‌ای از نتیجه‌‌ی تراکنش‌‌ را با جزییات کامل می‌‌آورد. آن در ابتدا تنها خطی را نشان می‌‌دهد که نشانگر حساب اجراکننده‌‌ی قرارداد، قرارداد و روش مورد استفاده، مثلا رشته، و میزان اتر ارسال شده برای اجرا است (این معمولا به صورت وی (Wei) که کوچک‌‌ترین واحد اتر و برابر 10^-18 Ether است نشان داده می‌‌شود). با کلیک روی سربرگ آن می‌‌توان آن را باز کرد و سوابق، اجراها، هزینه‌‌های تراکنش، نتایج نهایی و … را دید.

Remix

در این نقطه می‌‌خواهم دکمه getStore را در سمت راست بزنم، دقت کنید که نتیجه را چطور در زیر آن نشان می‌‌دهد:

code

یک لیست تراکنش جدید در سمت چپ به وجود می‌‌آید و با کلیک روی آن می‌‌توانید چیزی شبیه به تصویر زیر ببینید:

code

حالا من «0123456789» را در تکست‌‌باکس سمت راست دکمه getStore تایپ کرده و آن دکمه را می‌‌زنم. سپس دوباره getStore را زده و آن رشته را دریافت می‌‌کنم. خیلی خوب، ما می‌‌توانیم کار ابتدایی و اساسی ذخیره/دریافت با رشته‌‌ها را انجام دهیم.

حالا بیایید به چیزهای جالب‌‌تر بپردازیم.

ایجاد رشته‌‌های جدید: موقعیت داده

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

من روش جدید را ایجاد می‌‌کنم که تنها یک رشته جدید را با سه حرف خاص برمی‌‌گرداند: “ABC”

string

این تلاش نیت خوبی دارد، اما کار نمی‌‌کند. Remix بلافاصله 4 خطا و 1 هشدار نمایش می‌‌دهد:

دو خطای از این خطاها روی یک خط قرار دارند: (string newString = new string(3

هشدار: متغیر به عنوان یک pointer ذخیره اعلام شده است. از یک کلیدواژه ذخیره واضح برای خاموش کردن این هشدار استفاده کنید.

Error: حافظه رشته تایپ به راحتی قابل تغییر به pointer ذخیره رشته تایپ نیست

سه Error دیگر در خطوط eg newString[0] = “A”  رخ می‌‌دهند و همه از این نوع هستند:

Error: دسترسی Index برای رشته ممکن نیست.

برای درک مسئله اول باید درمورد موقعیت داده اندکی برایتان توضیح بدهم. نوشتن در بلاکچین بسیار هزینه‌‌بر است. هر گره‌‌ای که تراکنش را اداره می‌‌کند باید مقداری بنویسد، که همین باعث می‌‌شود تراکنش مقداری گران‌‌تر و بلاکچین مقداری بزرگ‌‌تر شود. وقتی گره‌‌ای بلوکی که حاوی این تراکنش است را دانلود می‌‌کند، به دلیل همین کار نوشتن، هزینه‌‌های بیشتری را متحمل می‌‌شود. در اتریوم هر تراکنشی هزینه خاصی دارد که سوخت (gas) نامیده می‌‌شود و انگیزه‌‌ایست که برنامه‌‌نویسان تا سر حد ممکن اقتصادی عمل کنند.

نویسندگان در هنگام نوشتن یک قرارداد، انتخاب نوع داده را در اختیار دارند: حافظه ارزان است (مثلا Gas کمتری صرف می‌‌کند، اما داده‌‌ها نوسان خواهند داشت و پس از اینکه تابع کار خود را تمام کرد از دست می‌‌روند)؛ ذخیره گران‌‌ترین است (و برای وضعیت قرارداد بسیار ضروری است، وضعیتی که باید از تابعی به تابع دیگر پایدار باقی بماند)؛ چیزی به اسم موقعیت calldata هم دارم (که معادل مقادیر موجود در قالب پشته‌‌ی تابعی‌‌ست که در حال اجراست). این ارزان‌‌ترین موقعیت است، اما اندازه‌‌ محدودی دارد. این یعنی امکان دارد توابع از نظر تعداد استدلال‌‌هایشان محدود باشند.

هر نوع داده یک موقعیت پیش‌‌فرض دارد. این از مستندات سالیدیتی‌‌ است:

موقعیت اجباری داده:

پارامترها‌‌ی توابع جانبی: calldata

متغیرهای وضعیت: ذخیره

موقعیت پیش‌‌فرض داده:

پارامترها‌‌ی تابع: حافظه

همه‌‌ی متغیرهای محلی دیگر: ذخیره

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

حالا بایید به کد خودمان برگردیم و این خط را آزمایش کنیم: (string newString = new string(3

این یک متغیر محلی درون تابع است و به صورت پیش‌‌فرض در ذخیره قرار دارد. کلیدواژه جدید برای تعیین اندازه اولیه آرایه دینامیک حافظه به کار گرفته شده است. اندازه آرایه‌‌های حافظه را نمی‌‌توان تغییر داد. از سوی دیگر می‌‌توانیم اندازه آرایه دینامیک ذخیره را با تغییر lengthproperty آن تغییر دهیم، اما نمی‌‌توانیم اندازه جدید را با آن استفاده کنیم.

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

(string memory newString = new string(3

دسترسی مستقیم به رشته‌‌ها: معادل بایت

بیایید دومین نوع Errorها را ببینیم. این ساده و اجتناب‌‌ناپذیر است که سالیدیتی‌‌ اجازه دسترسی شاخص به رشته‌‌ها را نمی‌‌‌‌دهد. از بخش سوالات متداول سالیدیتی‌‌ آمده است که:

رشته اساسا معادل بایت است، تنها اینکه رشته قرار است کدبندی UTF-8 رشته‌‌ی واقعی را داراست. از آنجایی که رشته داده‌‌ها را در کدگذاری UTF-8 نگه‌‌داری می‌‌کند، محاسبه‌‌ی تعداد کاراکترها در رشته دشوار است (کدبندی بعضی از حروف بیش از یک بایت نیاز دارد). به همین دلیل، طول رشته پشتیبانی نمی‌‌شود و دسترسی‌‌ها را هم فهرست نمی‌‌کند.

جایگزین آن این است که اول رشته را به بایت تغییر دهیم، و سپس مستقیما به آن دسترسی پیدا کنیم. اگرچه این کار محدودیت‌‌هایی دارد، اما ممکن است چون رشته یک نوع آرایه است.

اما تله‌‌ای هست که باید مراقب آن بود. بایت‌‌ها داده‌‌های خام را ذخیره می‌‌کنند؛ رشته حروف UTF-8 را ذخیره می‌‌کند. کد زیر همیشه تعداد حروف در _S را برنمی‌‌گرداند:

https://cdn-images-1.medium.com/max/1600/1*UjjtYJ68B9GYBWwaYkEbuw.png

اگر _S حاوی هر حرفی باشد که نشان دادن آن به شکل UTF بیش از 1 بایت نیاز داشته باشد، مشکل رخ می‌‌دهد. در این مورد، تابع طول نمایش بایت رشته‌‌ی ورودی را برمی‌‌گرداند و از تعداد حروف بیشتر خواهد شد.

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

با برگشت به تابع پیشین خودمان این ممکن است:

https://cdn-images-1.medium.com/max/1600/1*7bvPnHvMh73Yg-s8-b_fdg.png

اما برای مثال کد پایین که سعی در تنظیم سومین کاراکتر رشته به عنوان X دارد، وقتی کاراکترهای چندبایتی را دریافت کند با شکست مواجه خواهد شد.

https://cdn-images-1.medium.com/max/1600/1*1GXozt0vhqhE61kad4PvqA.png

این برای ورودی “Abcdef”، “AbXdef” برمی‌‌گرداند، اما برای “€bÁnç!”، “XbÁnç!” می‌‌دهد.

نتیجه

هنوز هم مطالب زیادی است که درباره این موضوع گفته شود، اما این مقاله به اندازه کافی طول دارد، پس مطلب را جمع‌‌بندی می‌‌کنم. مفهوم کلیدی مروبط به نوع رشته این است که این یک آرایه با کاراکترهای UTF-8 است و می‌‌تواند بدون توقف به بایت تبدیل شود. این تنها راه دستکاری رشته است. اما باید در نظر داشت که کاراکترهای UTF-8 دقیقا با بایت‌‌های مطابقت ندارند. تبدیل در هر مسیری دقیق خواهد بود، اما هیچ رابطه‌‌ی بدیهی‌‌ بین هر شاخص بایت و شاخص رشته مربوط به آن وجود ندارد. برای بیشتر موارد، نمایش مستقیم رشته به عنوان بایت مزیت دارد و در استفاده از کاراکترهایی که به صورت UTF کدبندی شده‌‌اند، مراقب این باشید که بیشتر از 1 بایت نباشند.

شاید از این مطالب هم خوشتان بیاید.

ارسال پاسخ

آدرس ایمیل شما منتشر نخواهد شد.