مقدمة عن المصفوفات Arrays والشرائح Slices الخاصة بلغة Go


أمل عبدالله محمد الجنايني

سنتعرف في هذا المقال على إيجابيات وسلبيات تخزين البيانات في Go باستخدام المصفوفات Arrays والشرائح Slices ولماذا يكون أحدها أفضل من الآخر عادةً.

هذا المقال جزء من سلسلة Go التي كتبها Mihalis Tsoukalos:

المصفوفات Arrays

تعد المصفوفات واحدةً من أكثر هياكل البيانات شيوعًا بين لغات البرمجة لسببين رئيسيين: إنّها بسيطة وسهلة الفهم، ويمكنها تخزين أنواع مختلفة من البيانات فيها.

يمكنك تعريف مصفوفة Array في لغة البرمجة Go، تحت اسم anArray مثلًا والتي تُخزِّن أربعة أعداد صحيحة كما يلي:

anArray := [4]int{-1, 2, 0, -4}

يُحدَّد حجم المصفوفة Array Size أولًا، ثم نوعها Array Type، وأخيرًا عناصرها Array Elements.

تُساعدك الدّالة ()len في معرفة طول المصفوفة فحجم المصفوفة السابقة هو 4.

إذا كنت على درايةٍ بلغات برمجة أخرى، فقد حاولت الوصول لجميع عناصر المصفوفة باستخدام حلقة for. ومع ذلك، كما سترى لاحقًا، أنّ الكلمة المفتاحية range الخاصة بلغة Go تُتيح لك الوصول لجميع عناصر المصفوفة أو الشريحة بسلاسة.

وأخيرًا، إليك كيفية تحديد مصفوفة ذات بُعدين two dimentional array:

twoD := [3][3]int{
    {1, 2, 3},
    {6, 7, 8},
    {10, 11, 12}}

الملف المصدر arrays.go يُوضِّح كيفية استخدام مصفوفات Go، ها هو الكود الأكثر أهمية في ملف arrays.go:

for i := 0; i < len(twoD); i++ {
        k := twoD[i]
        for j := 0; j < len(k); j++ {
                fmt.Print(k[j], " ")
        }
        fmt.Println()
}

for _, a := range twoD {
        for _, j := range a {
                fmt.Print(j, " ")
        }
        fmt.Println()
}

يوضح هذا كيف يُمكنك المرور على عناصر المصفوفة باستخدام for loop والكلمة المُفتاحية range. توضّح باقي الكود الخاص بالملف arrays.go كيفية تمرير المصفوفة كمعامل دالّة.

فيما يلي هو ناتج arrays.go:

$ go run arrays.go
Before change(): [-1 2 0 -4]
After change(): [-1 2 0 -4]
1 2 3
6 7 8
10 11 12
1 2 3
6 7 8
10 11 12

يوضح هذا الناتج أنّ التغييرات التي تُجريها على مصفوفة داخل دالة تُفقَد بعد إنتهاء الدالة.

عيوب ومساوئ المصفوفات arrays

لدى مصفوفات Go العديد من المساوئ التي لابد أن تأخذها بعين الإعتبار حينما تستخدمها في مشاريع Go.

أولًا، لا يُمكنك تغيير حجم المصفوفة بعد تعريفها، وهذا يعني أنّ مصفوفات Go ليست ديناميكية. بعبارة أبسط، إذا كنت بحاجة إلى إضافة عنصر إلى مصفوفة مُمتلئة، ستحتاج إلى إنشاء مصفوفة أكبر ونسخ جميع عناصر المصفوفة القديمة إلى الجديدة.

ثانيًا، عندما تقوم بتمرير مصفوفة إلى دالة كمعامل لهذه الدالة، فإنّك في الواقع تُمرر نسخة من المصفوفة، مما يعني أن أي تغييرات تُجريها على المصفوفة داخل الدالة ستُفقد بعد إنتهاء هذه الدالة.

أخيرًا، يمكن أن يكون تمرير مصفوفة كبيرة إلى دالة بطيئًا جدًا، خاصة وأن Go يجب أن تنشئ نسخة من هذه المصفوفة.

الحل لجميع هذه المشاكل هو استخدام الشرائح Slices التي توفرها Go.

الشرائح Slices

تشبه شرائح Go مصفوفات Go لكن بدون أوجه القصور. أولاً، يمكنك إضافة عنصر إلى شريحة موجودة باستخدام الدالة ()append، علاوةً على ذلك، تم تنفيذ شرائح Go داخليًا باستخدام المصفوفات، مما يعني أن Go تستخدم مصفوفةً أساسيًة لكل شريحة.

الشرائح لها خاصية سعة وخاصية طول، وهما ليستا نفس الشيء دائمًا. طول الشريحة هو نفس طول المصفوفة التي تحتوي على نفس عدد العناصر، ويمكن معرفتها باستخدام الدالة ()len. أمّا سعة الشريحة فهي الغرفة التي تم تخصيصها حاليًا للشريحة، ويمكن معرفتها باستخدام الدالة ()cap.

نظرًا لأن الشرائح ديناميكية الحجم، ففي حالة نفاذ شريحة ما (مما يعني أن الطول الحالي للمصفوفة هو نفس السعة أثناء محاولة إضافة عنصر آخر إليها)، يعمل نظام Go تلقائيًا على مضاعفة قدرته الحالية على توفير مساحة لمزيد من العناصر ويضيف العنصر المطلوب إلى المصفوفة.

بالإضافة إلى ذلك، يتم تمرير الشرائح للدوال حسب المرجع (Pass by reference)، مما يعني أن ما يتم نقله فعليًا إلى الدالة هو عنوان الذاكرة لمتغير الشريحة ، ولن تضيع أي تعديلات تجريها على الشريحة داخل إحدى الدوال بعد انتهائها. نتيجةً لذلك، فإن تمرير شريحة كبيرة إلى دالة يكون أسرع بكثير من تمرير مصفوفة بنفس عدد العناصر إلى نفس الدالة. وذلك لأن Go لن تضطر إلى عمل نسخة من الشريحة، إذ إنها ستُمرِّر فقط عنوان ذاكرة متغير الشريحة.

يتم توضيح شرائح Go في ملف slice.go، والذي يحتوي على الكود التالي:

package main

import (
        "fmt"
)

func negative(x []int) {
        for i, k := range x {
                x[i] = -k
        }
}

func printSlice(x []int) {
        for _, number := range x {
                fmt.Printf("%d ", number)
        }
        fmt.Println()
}

func main() {
        s := []int{0, 14, 5, 0, 7, 19}
        printSlice(s)
        negative(s)
        printSlice(s)

        fmt.Printf("Before. Cap: %d, length: %d\n", cap(s), len(s))
        s = append(s, -100)
        fmt.Printf("After. Cap: %d, length: %d\n", cap(s), len(s))
        printSlice(s)

        anotherSlice := make([]int, 4)
        fmt.Printf("A new slice with 4 elements: ")
        printSlice(anotherSlice)
}

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

بالإضافة إلى ذلك، تتيح لك الدالة ()append إضافة عنصر إلى شريحة موجودة؛ لاحظ أنه حتى إذا كانت سعة الشريحة تسمح لك بإضافة عنصر إلى هذه الشريحة فلن يتم تعديل طولها ما لم تستخدم ()append.

الدالة ()printSlice هي دالة مُساعدة، تُستخدم لطباعة عناصر معامل الشريحة الخاص بها، في حين أنّ الدالة ()negative تقوم بعمل معالجة لعناصر مُعامل الشريحة الخاص بها.

ها هو ناتج slice.go:

$ go run slice.go
0 14 5 0 7 19
0 -14 -5 0 -7 -19
Before. Cap: 6, length: 6
After. Cap: 12, length: 7
0 -14 -5 0 -7 -19 -100
A new slice with 4 elements: 0 0 0 0

يُرجى ملاحظة أنه عند إنشاء شريحة جديدة وتخصيص مساحة ذاكرة لعدد معين من العناصر، فستعمل Go تلقائيًا على تهيئة جميع العناصر بقيمة الصفر من نوعها، والتي في هذه الحالة هي 0.

الشرائح كمرجع للمصفوفات

تُتيح لك Go الاشارة إلى مصفوفة موجودة بشريحة باستخدام الترميز [:]. في هذه الحالة، ستنعكس أي تغييرات تجريها على دالة شريحة إلى المصفوفة - وهذا موضّح في refArray.go. يرجى تذكُّر أن الترميز [:] لا يُنشئ نسخة من المصفوفة، بل فقط يُشير إليها.

الجزء الأكثر إثارة للاهتمام في refArray.go هو:

func main() {
        anArray := [5]int{-1, 2, -3, 4, -5}
        refAnArray := anArray[:]

        fmt.Println("Array:", anArray)
        printSlice(refAnArray)
        negative(refAnArray)
        fmt.Println("Array:", anArray)
}

ناتج refArray.go هو:

$ go run refArray.go
Array: [-1 2 -3 4 -5]
-1 2 -3 4 -5
Array: [1 -2 3 -4 5]

لذلك، تغيرت عناصر مجموعة anArray بسبب الإشارة إلى الشريحة.

الملخص

على الرغم من أن Go تدعم المصفوفات والشرائح، إلا أنه من الواضح إلى الآن أنك ستستخدم الشرائح على الأرجح لأنها أكثر تنوعًا وقوة من مصفوفات Go. لا يوجد سوى عدد قليل من الأحداث التي ستحتاج فيها إلى استخدام مصفوفة بدلاً من شريحة. الحدث الأكثر وضوحًا هو عندما تكون متأكدًا تمامًا من أنك ستحتاج إلى تخزين عدد محدد من العناصر.

يمكنك العثور على كود Go الخاص بـ arrays.go و slice.go و refArray.go على GitHub.

ترجمة -وبتصرف- للمقال An introduction to Go arrays and slices لصاحبه Mihalis Tsoukalos





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن