المصفوفات (Arrays) في Solidity: تخزين وإدارة قوائم البيانات داخل العقد الذكي
المصفوفات (Arrays) في Solidity: تخزين وإدارة قوائم البيانات داخل العقد الذكي
تُعد المصفوفات من أهم هياكل البيانات في Solidity لأنها تسمح بتخزين مجموعة من القيم تحت اسم واحد وبترتيب تسلسلي يمكن الوصول إليه عبر الفهارس. داخل العقود الذكية، تُستخدم المصفوفات لبناء قوائم العناوين، السجلات، المعرّفات، النتائج، أو أي بيانات متكررة يجب حفظها بطريقة منظمة وقابلة للاسترجاع.
إذا كنت قد قرأت مقال أساسيات لغة Solidity: أنواع البيانات والمتغيرات (State Variables) فستلاحظ أن المصفوفات امتداد طبيعي لفكرة تخزين البيانات على مستوى العقد. لكن الفرق الجوهري هو أن Arrays لا تحفظ قيمة منفردة، بل هيكل مرتب يمكن قراءته وتعديله وإدارته بعناية شديدة، خاصة لأن كل عملية كتابة على Blockchain ترتبط بتكلفة تنفيذ فعلية.
في هذا المقال سنشرح أنواع المصفوفات في Solidity، وكيفية تخزينها في storage وmemory، وأشهر العمليات عليها، مع توضيح الآثار الأمنية وتكاليف التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟ عند الإضافة والحذف والمرور على العناصر.
ما هي المصفوفة في العقود الذكية؟
المصفوفة هي بنية بيانات تخزن مجموعة من العناصر من نفس النوع، مثل uint256[] أو address[]. كل عنصر له موضع عددي يبدأ من الصفر، ويُسمى هذا الموضع index.
هذا النمط مفيد جداً عند تصميم DApps تحتاج إلى عرض قائمة المستخدمين، تتبع الطلبات، تخزين أسماء المقترحات في نظام تصويت، أو حفظ سجلات شراء داخل Smart Contracts.
أنواع المصفوفات في Solidity
1) المصفوفات ثابتة الطول
المصفوفة ثابتة الطول يكون حجمها معروفاً لحظة التصريح بها، ولا يمكن زيادته أو تقليله لاحقاً. هذا النوع مناسب عندما تكون بنية البيانات محددة مسبقاً، مثل تخزين 3 قيم فقط أو 12 شهراً مثلاً.
uint256[3] public fixedScores = [10, 20, 30];
في المثال السابق، عدد العناصر ثابت ويساوي 3. لا يمكنك استخدام push() لإضافة عنصر رابع، لأن بنية التخزين نفسها محددة مسبقاً.
2) المصفوفات الديناميكية
المصفوفة الديناميكية لا تحدد طولها عند التصريح، ويمكن أن تنمو أو تتقلص أثناء تشغيل العقد. هذا النوع هو الأكثر شيوعاً في التطبيقات اللامركزية العملية.
address[] public users;
uint256[] public orderIds;
يمكن إضافة عناصر جديدة باستخدام push()، وقراءة الحجم الحالي عبر الخاصية length.
التخزين في storage و memory
فهم الفرق بين storage وmemory أساسي عند التعامل مع المصفوفات. المصفوفة الموجودة كمتغير حالة داخل العقد تُخزن في storage، أي بشكل دائم على الشبكة.
أما memory فهي مساحة مؤقتة تُستخدم أثناء تنفيذ الدالة فقط. عند تمرير مصفوفة كوسيط أو إنشائها مؤقتاً داخل دالة، قد تحتاج إلى تحديد موقع البيانات صراحة.
pragma solidity ^0.8.20;
contract ArrayStorageExample {
uint256[] public numbers;
function addNumber(uint256 value) public {
numbers.push(value);
}
function getAllNumbers() public view returns (uint256[] memory) {
return numbers;
}
function replaceAll(uint256[] memory newNumbers) public {
numbers = newNumbers;
}
}
لاحظ أن الدالة getAllNumbers تُرجع مصفوفة من نوع memory لأنها نسخة قابلة للإرسال خارجياً، بينما المتغير الأصلي numbers محفوظ في storage.
أهم العمليات على المصفوفات
إضافة عنصر جديد
في المصفوفات الديناميكية، تُستخدم الدالة push() لإلحاق عنصر في نهاية القائمة. هذه العملية شائعة في أنظمة التسجيل والطلبات.
users.push(msg.sender);
قراءة عنصر عبر الفهرس
يمكن الوصول إلى أي عنصر مباشرة عبر رقم موضعه، مثل العنصر الأول أو الثاني. الوصول المباشر سريع ومنطقي طالما كان index معروفاً وصحيحاً.
uint256 firstValue = numbers[0];
معرفة طول المصفوفة
الخاصية length ترجع عدد العناصر الحالية، وهي ضرورية لمنع الوصول إلى مواضع غير موجودة.
uint256 total = numbers.length;
حذف عنصر
الحذف في Solidity ليس بسيطاً دائماً. استخدام delete على عنصر لا يعيد ترتيب المصفوفة، بل يعيد العنصر إلى قيمته الافتراضية فقط.
delete numbers[1];
إذا كانت المصفوفة من نوع uint256[] فستصبح القيمة في ذلك الموضع صفراً، لكن الحجم لن يتغير. أما تقليص المصفوفة فعادة يتم باستخدام pop() لحذف آخر عنصر فقط.
numbers.pop();
مثال عملي متقدم لإدارة قائمة الطلاب
المثال التالي يوضح عقداً ذكياً يستخدم مصفوفة ديناميكية لتخزين أسماء الطلاب، مع دوال للإضافة والقراءة والتحديث والحذف من آخر القائمة. ولتوسيع الفهم، راجع أيضاً الدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟ لفهم صلاحيات الدوال وآثارها على الحالة.
pragma solidity ^0.8.20;
contract StudentRegistry {
string[] private students;
function addStudent(string memory name) public {
students.push(name);
}
function getStudent(uint256 index) public view returns (string memory) {
require(index < students.length, "Invalid index");
return students[index];
}
function updateStudent(uint256 index, string memory newName) public {
require(index < students.length, "Invalid index");
students[index] = newName;
}
function removeLastStudent() public {
require(students.length > 0, "No students to remove");
students.pop();
}
function getStudentsCount() public view returns (uint256) {
return students.length;
}
function getAllStudents() public view returns (string[] memory) {
return students;
}
}
هذا النموذج مناسب كبداية، لكنه ليس مثالياً إذا كنت تريد حذف عنصر من منتصف القائمة مع الحفاظ على الترتيب، لأن ذلك يتطلب تحريك العناصر التالية، وهي عملية قد تكون مكلفة على مستوى Gas.
في العقود الذكية، تجنب إنشاء حلقات
forطويلة على مصفوفات قد تكبر بمرور الوقت، لأن ذلك قد يؤدي إلى تجاوز حد الغاز وفشل المعاملة. إذا كانت القائمة مرشحة للنمو الكبير، فكّر في استخدامmappingأو تقسيم العمليات إلى دفعات أصغر.
متى نستخدم Arrays ومتى نستخدم Mapping؟
المصفوفات ممتازة عندما تحتاج إلى ترتيب العناصر أو استعراضها أو إرجاعها كاملة إلى الواجهة الأمامية عبر Ethers.js. أما إذا كنت تحتاج إلى وصول سريع بمفتاح محدد مثل عنوان مستخدم أو معرف فريد، فغالباً يكون mapping أكثر كفاءة.
في كثير من التصاميم المتقدمة، يتم الجمع بين الاثنين: مصفوفة للاحتفاظ بالقائمة القابلة للاستعراض، وmapping لتسريع التحقق أو الوصول المباشر. هذا النمط شائع في عقود العضوية، القوائم البيضاء، وأنظمة التصويت.
تحسين استهلاك الغاز عند التعامل مع المصفوفات
كل كتابة داخل storage تستهلك رسوماً حقيقية على الشبكة. لذلك يجب تصميم المصفوفات بعقلية اقتصادية، خاصة عند العمل على شبكات إنتاجية. ويمكنك تعميق هذا الجانب بقراءة أنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas.
- استخدم
viewعند القراءة فقط لتجنب تعديل الحالة. - تجنب إعادة مصفوفات ضخمة في دوال عامة إذا كان استهلاك البيانات كبيراً.
- لا تنفذ عمليات حذف من منتصف المصفوفة إلا عند الضرورة.
- خفف الحلقات المتكررة على بيانات مخزنة على السلسلة.
- فكر في نقل بعض منطق الفلترة والترتيب إلى الواجهة الأمامية بدل تنفيذه داخل العقد.
أمنياً، لا تعتمد على فهارس المصفوفات القادمة من المستخدم دون التحقق منها بواسطة
require. أي وصول خارج حدود المصفوفة يؤدي إلىrevert، وقد يُستغل لإفشال عمليات منطقية إذا لم يكن التحقق مضبوطاً بدقة.
اختبار المصفوفات عملياً قبل النشر
قبل نشر أي عقد يستخدم Arrays، اختبره جيداً على بيئة تطوير مناسبة مثل محرر Remix IDE: كتابة ونشر أول عقد ذكي (Smart Contract) على المتصفح مباشرة أو عبر أدوات مثل Hardhat. تأكد من سيناريوهات الإضافة والحذف والوصول لفهارس غير صالحة وإرجاع البيانات الكبيرة.
كما أن العمل على شبكة اختبار يتطلب إعداد المحفظة والاتصال بالبيئة الصحيحة، ويمكن الرجوع إلى إعداد بيئة التطوير: تثبيت محفظة MetaMask والاتصال بشبكات الاختبار (Testnets) والحصول على عملات تجريبية مجانية (Faucet) للبدء في نشر واختبار العقود الذكية حتى تتمكن من تنفيذ التجارب دون مخاطرة مالية.
خلاصة
المصفوفات في Solidity هي أداة مركزية لتنظيم البيانات المتكررة داخل العقد الذكي، لكنها تتطلب فهماً عميقاً لطبيعة التخزين على EVM وتأثير كل عملية على الأمان والتكلفة. المبرمج المحترف لا ينظر إلى المصفوفة كقائمة فقط، بل كبنية لها آثار مباشرة على الأداء، القابلية للتوسع، وتجربة المستخدم في تطبيقات Web3.
كلما أتقنت الفرق بين المصفوفات الثابتة والديناميكية، وفهمت سلوك storage وmemory، وأحسنت تصميم عمليات الحذف والقراءة، أصبحت أكثر قدرة على بناء عقود ذكية مستقرة وقابلة للصيانة وفعالة من حيث Gas Fees.
14 comments