إدارة الذاكرة بذكاء: الفرق الحاسم بين Storage, Memory, و Calldata
إدارة الذاكرة بذكاء: الفرق الحاسم بين Storage, Memory, و Calldata
من أكثر النقاط التي تربك مطوري Solidity في المراحل الأولى والمتوسطة هي فهم مواقع تخزين البيانات داخل بيئة EVM. فالعقد الذكي لا يتعامل مع المتغيرات كلها بالطريقة نفسها، بل يوزعها بين Storage وMemory وCalldata بحسب مدة حياة البيانات وطريقة الوصول إليها وكلفتها.
هذا الفهم ليس رفاهية نظرية، بل يؤثر مباشرة على أمان العقد، واستهلاك التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟، وسلوك الدوال عند تمرير arrays وstructs وstrings. وإذا كنت قد قرأت سابقاً أساسيات لغة Solidity: أنواع البيانات والمتغيرات (State Variables) فهذه المقالة تكمل الصورة من منظور معماري أعمق.
ما المقصود بمواقع البيانات في Solidity؟
عندما تكتب متغيراً أو تستقبل بارامتر داخل دالة، فإن المترجم يحتاج إلى معرفة المكان الذي ستعيش فيه هذه البيانات. هل هي دائمة داخل البلوكتشين؟ هل هي مؤقتة أثناء تنفيذ الدالة فقط؟ أم أنها بيانات قراءة فقط قادمة من الاستدعاء الخارجي؟ هنا يظهر دور data location.
في العقود الذكية، الأنواع المرجعية مثل array وstruct وmapping تحتاج غالباً إلى تحديد صريح لموضع البيانات. أما الأنواع القيمية مثل uint وbool وaddress فتعاملها أبسط لأنها تنسخ بالقيمة عادة.
أولاً: Storage — التخزين الدائم داخل العقد
Storage هو المكان الذي تُحفظ فيه حالة العقد الذكي بشكل دائم على البلوكتشين. أي متغير حالة state variable يوجد عملياً داخل Storage. لهذا السبب، القراءة منه أرخص من الكتابة إليه، أما التعديل عليه فهو من أكثر العمليات استهلاكاً للغاز.
حين تعدّل قيمة مخزنة في Storage فأنت تغيّر حالة العقد نفسها، وهذا التغيير يُسجل على الشبكة ويحتاج إلى معاملة. لذلك فإن أي تصميم سيئ في استخدام التخزين سيؤدي إلى تضخم تكاليف التشغيل، خصوصاً في العقود التي تتعامل مع المصفوفات (Arrays) في Solidity: تخزين وإدارة قوائم البيانات داخل العقد الذكي أو الهياكل (Structs): تصميم أنواع بيانات مخصصة.
متى نستخدم Storage؟
- عند حفظ أرصدة المستخدمين أو الملكية أو الإعدادات الدائمة.
- عند تعديل بيانات داخل
mappingأوarrayمرتبطة بحالة العقد. - عندما تحتاج البيانات للبقاء بعد انتهاء تنفيذ الدالة.
الكتابة إلى
Storageمكلفة جداً مقارنة بالعمل على نسخة مؤقتة فيMemory. برمجياً، حاول تقليل عدد مرات التعديل على الحالة، واجمع العمليات الحسابية أولاً ثم اكتب النتيجة النهائية مرة واحدة متى أمكن.
ثانياً: Memory — مساحة مؤقتة أثناء التنفيذ
Memory هي منطقة مؤقتة تُستخدم خلال تنفيذ الاستدعاء فقط. بمجرد انتهاء الدالة، تختفي البيانات المخزنة فيها. لهذا السبب تُستخدم غالباً مع المتغيرات المؤقتة، ومع بناء قيم الإرجاع، أو إنشاء نسخ قابلة للتعديل من بيانات تم جلبها من مواقع أخرى.
العمل على Memory لا يغيّر حالة العقد مباشرة. وهذا يجعلها مناسبة للمعالجة الداخلية التي لا تتطلب بقاء البيانات بعد انتهاء التنفيذ. لكنها ليست مجانية تماماً، إذ إن تخصيص مساحات كبيرة أو نسخ مصفوفات ضخمة إليها يستهلك غازاً أيضاً.
متى نستخدم Memory؟
- عند استقبال أو إنشاء بيانات مؤقتة قابلة للتعديل داخل الدالة.
- عند إرجاع
dynamic arraysأوstrings. - عند إنشاء نسخة من بيانات دائمة لمعالجتها دون التأثير على الأصل.
ثالثاً: Calldata — مدخلات قراءة فقط ومنخفضة الكلفة
Calldata هو المكان الذي تصل إليه بيانات الاستدعاء الخارجي للدالة. أهم ما يميزه أنه للقراءة فقط، أي لا يمكنك تعديل محتواه من داخل الدالة. لكنه في المقابل غالباً أكثر كفاءة من نسخ البيانات إلى Memory، خصوصاً مع المصفوفات والسلاسل النصية الكبيرة.
في الدوال الخارجية external، يُعد استخدام calldata خياراً مهماً لتقليل تكاليف النسخ. وهذا يرتبط مباشرة بفهمك لبنية الدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟.
متى نستخدم Calldata؟
- عند استقبال مدخلات لدالة
externalلا تحتاج إلى تعديلها. - عند تمرير
stringأوbytesأوarrayكبيرة للحفاظ على كفاءة الغاز. - عندما تكون حاجتك قراءة البيانات فقط دون إنشاء نسخة جديدة.
إذا كانت الدالة
externalولا تحتاج إلى تعديل المصفوفة أو النص المُمرر، فاختيارcalldataبدلاً منmemoryيعد من أشهر أساليبGas Optimizationالعملية.
الفرق العملي بين الثلاثة في مثال واحد
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DataLocationsExample {
string[] public names;
constructor() {
names.push("Ali");
names.push("Sara");
}
function addName(string calldata newName) external {
names.push(newName);
}
function getNames() external view returns (string[] memory) {
return names;
}
function updateFirstName(string memory tempName) external {
tempName = string(abi.encodePacked(tempName, "_updated"));
names[0] = tempName;
}
function editStoredName() external {
string storage firstName = names[0];
firstName = "Omar";
}
}
تحليل المثال
- الخاصية
namesمخزنة فيstorageلأنها متغير حالة. - البارامتر
newNameفي الدالةaddNameموجود فيcalldataلأنه مدخل خارجي للقراءة فقط قبل دفعه إلى التخزين الدائم. - الدالة
getNamesتُرجع نسخة فيmemoryلأن الإرجاع لا يكون عادةً مباشرة من التخزين الدائم. - المتغير
tempNameفيmemoryيمكن تعديله مؤقتاً، ثم تُحفظ النتيجة لاحقاً فيstorage. - المتغير
firstNameفيeditStoredNameهو مرجع مباشر إلى بيانات مخزنة، وأي تعديل عليه ينعكس فوراً على حالة العقد.
أخطاء شائعة يقع فيها المطورون
1) الخلط بين النسخ والمرجع
عند استخدام storage مع نوع مرجعي، فأنت غالباً تتعامل مع مرجع مباشر. أما memory فينشئ نسخة مستقلة في كثير من الحالات. هذا الفرق قد يسبب تعديلات غير مقصودة على بيانات المستخدمين.
2) استخدام memory في مكان يصلح له calldata
هذا الخطأ لا يكسر الكود غالباً، لكنه يرفع تكاليف الغاز بلا فائدة. في الدوال الخارجية، راجع دائماً إن كانت البيانات تحتاج إلى تعديل فعلاً قبل اختيار موضعها.
3) تعديل الحالة داخل دوال يفترض أن تكون للقراءة
التمييز بين المواقع الثلاثة يتكامل مع فهم أنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas. فالدوال التي يفترض أن تقرأ فقط يجب ألا تنتهي إلى تغيير بيانات storage بشكل مباشر أو غير مباشر.
في المراجعات الأمنية
Security Auditing، يتم التدقيق كثيراً في المتغيرات المرجعية التي تشير إلىstorageلأن أي مرجع غير محسوب قد يؤدي إلى تعديل حالة حساسة دون قصد، خصوصاً داخلstructsالمركبة أو القوائم المتداخلة.
كيف تختار الموقع المناسب بسرعة؟
- اسأل أولاً: هل يجب أن تبقى البيانات بعد انتهاء التنفيذ؟ إذا نعم، فالمكان هو
storage. - إذا كانت البيانات مؤقتة وقابلة للتعديل داخل الدالة، فاستخدم
memory. - إذا كانت مدخلات خارجية للقراءة فقط، وخاصة في دوال
external، فابدأ بـcalldata.
الخلاصة
الفرق بين Storage وMemory وCalldata ليس مجرد فرق لغوي في الصياغة، بل هو قرار هندسي يحدد كيف تتحرك البيانات داخل العقد الذكي، وكم ستدفع من غاز، وهل سيتصرف الكود كما تتوقع أم لا. كل مطور يريد كتابة Smart Contracts قوية وفعالة يجب أن يتقن هذا الثلاثي بدقة.
وإذا كنت تطبق الأمثلة عملياً، فمن المفيد الرجوع إلى محرر Remix IDE: كتابة ونشر أول عقد ذكي (Smart Contract) على المتصفح مباشرة لاختبار السلوك الفعلي لكل حالة، ثم الانتقال إلى عقود أكثر تعقيداً تتضمن القواميس (Mappings) والتعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert حتى تبني فهماً عملياً متكاملاً، لا مجرد معرفة نظرية.
15 comments