المصفوفات (Arrays) في Solidity: تخزين وإدارة قوائم البيانات داخل العقد الذكي


اليوم سنتعلم كيفية استخدام المصفوفات (Arrays) في Solidity لتخزين وإدارة قوائم البيانات بكفاءة داخل العقود الذكية على بلوكتشين الإيثيريوم.

سنتناول المصفوفات ذات الحجم الثابت والمتحرك، وكيفية إضافتها، تحديثها، وحذف عناصرها.

1. فهم المصفوفات ذات الحجم الثابت والمتحرك

في Solidity، يمكن تعريف المصفوفات بنوعين رئيسيين: ذات حجم ثابت (Fixed-size Arrays) و ديناميكية (Dynamic Arrays).

  • المصفوفات ذات الحجم الثابت: يتم تحديد حجمها عند التصريح بها ولا يمكن تغييره لاحقاً. هذه المصفوفات تكون فعالة للبيانات التي نعرف حجمها مسبقاً.
  • المصفوفات الديناميكية: لا يتم تحديد حجمها عند التصريح ويمكن أن تنمو أو تتقلص حسب الحاجة. تُستخدم هذه للبيانات المتغيرة.

لنبدأ بتعريف هذه الأنواع داخل عقد Solidity:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ArrayManager {
    // مصفوفة ذات حجم ثابت لتخزين 5 أرقام من نوع uint
    uint[5] public fixedSizeArray = [1, 2, 3, 4, 5];

    // مصفوفة ديناميكية لتخزين أسماء (سلاسل نصية)
    string[] public dynamicStringArray;

    // مصفوفة ديناميكية لتخزين أرقام
    uint[] public dynamicUintArray;

    constructor() {
        // تهيئة بعض القيم الأولية للمصفوفة الديناميكية عند نشر العقد
        dynamicStringArray.push("Apple");
        dynamicStringArray.push("Banana");
    }
    // ... بقية وظائف العقد ستأتي لاحقاً
}

ملاحظة تقنية: عند تعريف المصفوفات كمتغيرات حالة (state variables) باستخدام public، يقوم Solidity تلقائياً بإنشاء دالة getter عامة للوصول إلى عناصرها الفردية أو المصفوفة بأكملها (للديناميكية). المصفوفات في Solidity يمكن أن تكون في storage (متغيرات الحالة) أو memory (متغيرات مؤقتة داخل الوظائف).

2. إضافة العناصر والوصول إليها

تختلف طريقة إضافة العناصر والوصول إليها بين المصفوفات ذات الحجم الثابت والديناميكية:

  • المصفوفات ذات الحجم الثابت: يتم الوصول إلى العناصر وتحديثها باستخدام الفهرس (index) مباشرة. لا يمكن إضافة عناصر جديدة تتجاوز حجمها المحدد.
  • المصفوفات الديناميكية: تستخدم الدالة push() لإضافة عنصر جديد إلى نهاية المصفوفة. يمكن الوصول إلى العناصر باستخدام الفهرس أيضاً.

لنضف بعض الوظائف للتعامل مع هذه العمليات:

// ... داخل عقد ArrayManager

    // --- وظائف المصفوفة ذات الحجم الثابت ---

    // دالة للحصول على عنصر من المصفوفة ذات الحجم الثابت بواسطة الفهرس
    function getFixedElement(uint _index) public view returns (uint) {
        require(_index < fixedSizeArray.length, "Index out of bounds for fixed array");
        return fixedSizeArray[_index]; // الوصول إلى العنصر باستخدام الفهرس
    }

    // --- وظائف المصفوفة الديناميكية (string) ---

    // دالة لإضافة سلسلة نصية جديدة إلى المصفوفة الديناميكية
    function addString(string memory _name) public {
        dynamicStringArray.push(_name); // 'push' تضيف العنصر في نهاية المصفوفة
    }

    // دالة للحصول على سلسلة نصية من المصفوفة الديناميكية بواسطة الفهرس
    function getString(uint _index) public view returns (string memory) {
        require(_index < dynamicStringArray.length, "Index out of bounds for string array");
        return dynamicStringArray[_index]; // الوصول إلى العنصر باستخدام الفهرس
    }

    // دالة للحصول على جميع السلاسل النصية من المصفوفة الديناميكية
    function getAllStrings() public view returns (string[] memory) {
        return dynamicStringArray; // إرجاع المصفوفة بأكملها
    }

    // --- وظائف المصفوفة الديناميكية (uint) ---

    // دالة لإضافة رقم جديد إلى المصفوفة الديناميكية
    function addUint(uint _value) public {
        dynamicUintArray.push(_value);
    }

    // دالة للحصول على جميع الأرقام من المصفوفة الديناميكية
    function getAllUints() public view returns (uint[] memory) {
        return dynamicUintArray;
    }

ملاحظة تقنية: استخدام require للتحقق من أن الفهرس ضمن النطاق يمنع الأخطاء الأمنية مثل "out-of-bounds access" ويحافظ على سلامة العقد.

3. تحديث وحذف العناصر

تُعد القدرة على تحديث العناصر وحذفها أساسية لإدارة البيانات:

  • التحديث: يتم تحديث العناصر في كلا النوعين من المصفوفات عن طريق تعيين قيمة جديدة للفهرس المطلوب مباشرة.
  • الحذف: في Solidity، كلمة delete الرئيسية تعيد العنصر إلى قيمته الافتراضية (صفر للأرقام، سلسلة فارغة للسلاسل). للمصفوفات الديناميكية، يمكن استخدام pop() لإزالة العنصر الأخير وتقليص حجم المصفوفة فعلياً.

لنضف وظائف التحديث والحذف إلى عقدنا:

// ... داخل عقد ArrayManager

    // --- وظائف المصفوفة ذات الحجم الثابت ---

    // دالة لتحديث عنصر في المصفوفة ذات الحجم الثابت
    function updateFixedElement(uint _index, uint _newValue) public {
        require(_index < fixedSizeArray.length, "Index out of bounds for fixed array");
        fixedSizeArray[_index] = _newValue; // تحديث العنصر مباشرة
    }

    // --- وظائف المصفوفة الديناميكية (string) ---

    // دالة لتحديث سلسلة نصية في المصفوفة الديناميكية
    function updateString(uint _index, string memory _newName) public {
        require(_index < dynamicStringArray.length, "Index out of bounds for string array");
        dynamicStringArray[_index] = _newName; // تحديث العنصر مباشرة
    }

    // دالة لحذف سلسلة نصية من المصفوفة الديناميكية (عبر تعيينها للقيمة الافتراضية)
    // ملاحظة: هذا لا يقلص حجم المصفوفة فعلياً، بل يترك فراغاً بقيمة افتراضية.
    function deleteString(uint _index) public {
        require(_index < dynamicStringArray.length, "Index out of bounds for string array");
        delete dynamicStringArray[_index]; // يحذف العنصر ويعيده للقيمة الافتراضية (سلسلة فارغة)
    }

    // دالة لإزالة العنصر الأخير من المصفوفة الديناميكية وتقليص حجمها
    function removeLastString() public {
        require(dynamicStringArray.length > 0, "Array is empty");
        dynamicStringArray.pop(); // 'pop' تزيل العنصر الأخير وتقلص حجم المصفوفة
    }

    // --- وظائف المصفوفة الديناميكية (uint) ---

    // دالة لتحديث رقم في المصفوفة الديناميكية
    function updateUint(uint _index, uint _newValue) public {
        require(_index < dynamicUintArray.length, "Index out of bounds for uint array");
        dynamicUintArray[_index] = _newValue;
    }
}

ملاحظة تقنية: عمليات التعديل والحذف في المصفوفات (خاصة الكبيرة منها) يمكن أن تكون مكلفة من حيث الغاز (Gas Costs) على شبكة الإيثيريوم. يجب تصميم العقود بذكاء لتقليل هذه التكاليف قدر الإمكان.

الكود النهائي الكامل (Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ArrayManager {
    // مصفوفة ذات حجم ثابت لتخزين 5 أرقام من نوع uint
    uint[5] public fixedSizeArray = [1, 2, 3, 4, 5];

    // مصفوفة ديناميكية لتخزين أسماء (سلاسل نصية)
    string[] public dynamicStringArray;

    // مصفوفة ديناميكية لتخزين أرقام
    uint[] public dynamicUintArray;

    constructor() {
        // تهيئة بعض القيم الأولية للمصفوفة الديناميكية عند نشر العقد
        dynamicStringArray.push("Apple");
        dynamicStringArray.push("Banana");
    }

    // --- وظائف المصفوفة ذات الحجم الثابت ---

    // دالة للحصول على عنصر من المصفوفة ذات الحجم الثابت بواسطة الفهرس
    function getFixedElement(uint _index) public view returns (uint) {
        require(_index < fixedSizeArray.length, "Index out of bounds for fixed array");
        return fixedSizeArray[_index];
    }

    // دالة لتحديث عنصر في المصفوفة ذات الحجم الثابت
    function updateFixedElement(uint _index, uint _newValue) public {
        require(_index < fixedSizeArray.length, "Index out of bounds for fixed array");
        fixedSizeArray[_index] = _newValue;
    }

    // --- وظائف المصفوفة الديناميكية (string) ---

    // دالة لإضافة سلسلة نصية جديدة إلى المصفوفة الديناميكية
    function addString(string memory _name) public {
        dynamicStringArray.push(_name);
    }

    // دالة للحصول على سلسلة نصية من المصفوفة الديناميكية بواسطة الفهرس
    function getString(uint _index) public view returns (string memory) {
        require(_index < dynamicStringArray.length, "Index out of bounds for string array");
        return dynamicStringArray[_index];
    }

    // دالة للحصول على جميع السلاسل النصية من المصفوفة الديناميكية
    function getAllStrings() public view returns (string[] memory) {
        return dynamicStringArray;
    }

    // دالة لتحديث سلسلة نصية في المصفوفة الديناميكية
    function updateString(uint _index, string memory _newName) public {
        require(_index < dynamicStringArray.length, "Index out of bounds for string array");
        dynamicStringArray[_index] = _newName;
    }

    // دالة لحذف سلسلة نصية من المصفوفة الديناميكية (عبر تعيينها للقيمة الافتراضية)
    // ملاحظة: هذا لا يقلص حجم المصفوفة فعلياً، بل يترك فراغاً بقيمة افتراضية.
    function deleteString(uint _index) public {
        require(_index < dynamicStringArray.length, "Index out of bounds for string array");
        delete dynamicStringArray[_index];
    }

    // دالة لإزالة العنصر الأخير من المصفوفة الديناميكية وتقليص حجمها
    function removeLastString() public {
        require(dynamicStringArray.length > 0, "Array is empty");
        dynamicStringArray.pop();
    }

    // --- وظائف المصفوفة الديناميكية (uint) ---

    // دالة لإضافة رقم جديد إلى المصفوفة الديناميكية
    function addUint(uint _value) public {
        dynamicUintArray.push(_value);
    }

    // دالة للحصول على جميع الأرقام من المصفوفة الديناميكية
    function getAllUints() public view returns (uint[] memory) {
        return dynamicUintArray;
    }

    // دالة لتحديث رقم في المصفوفة الديناميكية
    function updateUint(uint _index, uint _newValue) public {
        require(_index < dynamicUintArray.length, "Index out of bounds for uint array");
        dynamicUintArray[_index] = _newValue;
    }
}

الكود النهائي الكامل (JavaScript - Ethers.js / Hardhat)

هذا السكربت يوضح كيفية نشر العقد والتفاعل مع وظائفه باستخدام Ethers.js ضمن بيئة Hardhat.

const { ethers } = require("hardhat");

async function main() {
    // 1. نشر العقد
    const ArrayManager = await ethers.getContractFactory("ArrayManager");
    const arrayManager = await ArrayManager.deploy();
    await arrayManager.deployed();
    console.log("ArrayManager deployed to:", arrayManager.address);

    // 2. التفاعل مع المصفوفة الديناميكية (strings)
    console.log("\n--- التفاعل مع المصفوفة الديناميكية (Strings) ---");
    let strings = await arrayManager.getAllStrings();
    console.log("Strings الأولية:", strings); // يجب أن تكون ["Apple", "Banana"] من الـ constructor

    await arrayManager.addString("Cherry");
    console.log("بعد إضافة 'Cherry':", await arrayManager.getAllStrings()); // ["Apple", "Banana", "Cherry"]

    await arrayManager.updateString(0, "Apricot");
    console.log("بعد تحديث العنصر الأول:", await arrayManager.getAllStrings()); // ["Apricot", "Banana", "Cherry"]

    await arrayManager.deleteString(1); // يحذف "Banana" ويترك سلسلة فارغة
    console.log("بعد حذف العنصر الثاني (تعيين للقيمة الافتراضية):", await arrayManager.getAllStrings()); // ["Apricot", "", "Cherry"]

    await arrayManager.removeLastString(); // يزيل "Cherry"
    console.log("بعد إزالة العنصر الأخير:", await arrayManager.getAllStrings()); // ["Apricot", ""]

    // 3. التفاعل مع المصفوفة الديناميكية (uints)
    console.log("\n--- التفاعل مع المصفوفة الديناميكية (Uints) ---");
    console.log("Uints الأولية:", await arrayManager.getAllUints()); // []

    await arrayManager.addUint(100);
    await arrayManager.addUint(200);
    console.log("بعد إضافة أرقام:", await arrayManager.getAllUints()); // [100, 200]

    await arrayManager.updateUint(0, 150);
    console.log("بعد تحديث العنصر الأول:", await arrayManager.getAllUints()); // [150, 200]

    // 4. التفاعل مع المصفوفة ذات الحجم الثابت
    console.log("\n--- التفاعل مع المصفوفة ذات الحجم الثابت ---");
    console.log("Fixed Array Element at 0 (قبل التحديث):", await arrayManager.getFixedElement(0)); // 1

    await arrayManager.updateFixedElement(0, 99);
    console.log("Fixed Array Element at 0 (بعد التحديث):", await arrayManager.getFixedElement(0)); // 99
    // لا يمكن الحصول على كل العناصر مباشرة للمصفوفة العامة ذات الحجم الثابت إلا بتكرار يدوي أو كشف كل عنصر
    // لذا سنكتفي بالوصول إلى عنصر واحد.
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

النتيجة المتوقعة

عند تشغيل سكربت JavaScript باستخدام Hardhat (npx hardhat run scripts/deploy.js --network localhost بعد نشر العقد محلياً)، ستظهر المخرجات التالية في الطرفية:

ArrayManager deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 (قد يختلف العنوان)

--- التفاعل مع المصفوفة الديناميكية (Strings) ---
Strings الأولية: [ 'Apple', 'Banana' ]
بعد إضافة 'Cherry': [ 'Apple', 'Banana', 'Cherry' ]
بعد تحديث العنصر الأول: [ 'Apricot', 'Banana', 'Cherry' ]
بعد حذف العنصر الثاني (تعيين للقيمة الافتراضية): [ 'Apricot', '', 'Cherry' ]
بعد إزالة العنصر الأخير: [ 'Apricot', '' ]

--- التفاعل مع المصفوفة الديناميكية (Uints) ---
Uints الأولية: []
بعد إضافة أرقام: [ ,  ]
بعد تحديث العنصر الأول: [ ,  ]

--- التفاعل مع المصفوفة ذات الحجم الثابت ---
Fixed Array Element at 0 (قبل التحديث): 
Fixed Array Element at 0 (بعد التحديث): 

توضح هذه المخرجات بوضوح كيفية تفاعل العقد مع المصفوفات المختلفة، وإضافة العناصر، وتحديثها، وحذفها، مما يبرهن على فهمك العميق لإدارة البيانات داخل العقود الذكية باستخدام Solidity.