Skip to content

الشرائح

في Go تبدو المصفوفات والشرائح متشابهة تقريباً، لكن وظائفهما مختلفة بشكل كبير، المصفوفة هي بنية بيانات بطول ثابت، لا يمكن تغيير طولها بعد تحديده، بينما الشريحة بطول غير ثابت، وتتوسع تلقائياً عندما لا تكفي سعتها.

المصفوفات

إذا كان طول البيانات المراد تخزينها معروفاً مسبقاً، ولا توجد حاجة للتوسع في الاستخدام اللاحق، يمكن النظر في استخدام المصفوفات، المصفوفات في Go هي نوع قيم وليست مراجع، وليست مؤشرات للعنصر الأول.

TIP

المصفوفة كنوع قيم، عند تمرير المصفوفة كمعامل لدالة، وبما أن دوال Go تمرر بالقيمة، سيتم نسخ المصفوفة بأكملها.

التهيئة

عند تعريف مصفوفة يجب أن يكون الطول ثابتاً، لا يمكن أن يكون متغيراً، لا يمكنك تعريف متغير ثم استخدامه كقيمة طول للمصفوفة:

go
// مثال صحيح
var a [5]int

// مثال خاطئ
l := 1
var b [l]int

لنُهيئ أولاً مصفوفة أعداد صحيحة بطول 5:

go
var nums [5]int

يمكن أيضاً التهيئة بعناصر:

go
nums := [5]int{1, 2, 3}

يمكن جعل المُترجم يستنتج الطول تلقائياً:

go
nums := [...]int{1, 2, 3, 4, 5} //يكافئ nums := [5]int{1, 2, 3, 4, 5}، علامة الحذف يجب أن تكون موجودة، وإلا سيُنتج شريحة وليس مصفوفة

يمكن أيضاً الحصول على مؤشر عبر دالة new:

go
nums := new([5]int)

جميع الطرق أعلاه ستخصص لـ nums مساحة ذاكرة بحجم ثابت، والفرق فقط أن الطريقة الأخيرة تعطي مؤشراً.

عند تهيئة المصفوفة، يجب الانتباه إلى أن الطول يجب أن يكون تعبيراً ثابتاً، وإلا لن يتم الترجمة، التعبير الثابت يعني أن النتيجة النهائية للتعبير ثابت، مثال خاطئ:

go
length := 5 // هذا متغير
var nums [length]int

length متغير، لذا لا يمكن استخدامه لتهيئة طول المصفوفة، المثال الصحيح:

go
const length = 5
var nums [length]int // ثابت
var nums2 [length + 1]int // تعبير ثابت
var nums3 [(1 + 2 + 3) * 5]int // تعبير ثابت
var nums4 [5]int // الأكثر استخداماً

الاستخدام

باسم المصفوفة والفهرس، يمكن الوصول للعنصر المقابل في المصفوفة:

go
fmt.Println(nums[0])

يمكن أيضاً تعديل عناصر المصفوفة:

go
nums[0] = 1

يمكن أيضاً استخدام الدالة المدمجة len للوصول لعدد عناصر المصفوفة:

go
len(nums)

استخدام الدالة المدمجة cap للوصول لسعة المصفوفة، سعة المصفوفة تساوي طولها، والسعة مهمة للشرائح فقط:

go
cap(nums)

القص

صيغة قص المصفوفة هي `arr[startIndex:endIndex]، نطاق القص مغلق من اليسار ومفتوح من اليمين. وبعد قص المصفوفة، تصبح نوع شريحة. مثال:

go
nums := [5]int{1, 2, 3, 4, 5}

nums[:] // نطاق الشريحة الفرعية [0,5) -> [1 2 3 4 5]
nums[1:] // نطاق الشريحة الفرعية [1,5) -> [2 3 4 5]
nums[:5] // نطاق الشريحة الفرعية [0,5) -> [1 2 3 4 5]
nums[2:3] // نطاق الشريحة الفرعية [2,3) -> [3]
nums[1:3] // نطاق الشريحة الفرعية [1,3) -> [2 3]
go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  fmt.Printf("%T\n", arr)
  fmt.Printf("%T\n", arr[1:2])
}

المخرجات:

[5]int
[]int

لتحويل المصفوفة لنوع شريحة، القص بدون معاملات كافٍ، الشريحة المحولة والمصفوفة الأصلية تشيران لنفس الذاكرة، وتعديل الشريحة سيؤدي لتغير محتوى المصفوفة الأصلية:

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := arr[:]
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

المخرجات:

array: [0 2 3 4 5]
slice: [0 2 3 4 5]

إذا كنت تريد تعديل الشريحة المحولة، يُنصح باستخدام الطريقة التالية للتحويل:

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := slices.Clone(arr[:])
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

المخرجات:

array: [1 2 3 4 5]
slice: [0 2 3 4 5]

الشرائح

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

التهيئة

هناك عدة طرق لتهيئة الشرائح:

go
var nums []int // قيمة
nums := []int{1, 2, 3} // قيمة
nums := make([]int, 0, 0) // قيمة
nums := new([]int) // مؤشر

يمكن ملاحظة أن الفرق بين الشريحة والمصفوفة في الشكل هو فقط عدم وجود طول تهيئة. عادةً، يُنصح باستخدام make لإنشاء شريحة فارغة، لكن بالنسبة للشرائح، تقبل دالة make ثلاثة معاملات: النوع والطول والسعة. مثال لشرح الفرق بين الطول والسعة: لنفترض أن هناك دلو ماء، الماء ليس ممتلئاً، ارتفاع الدلو هو سعته، يمثل إجمالي كمية الماء التي يمكن أن يحتويها، وارتفاع الماء في الدلو يمثل الطول، ارتفاع الماء يجب أن يكون أقل من أو يساوي ارتفاع الدلو، وإلا سيفيض الماء. لذا، طول الشريحة يمثل عدد العناصر في الشريحة، وسعة الشريحة تمثل إجمالي عدد العناصر التي يمكن أن تحتويها الشريحة، والفرق الأكبر بين الشرائح والمصفوفات هو أن سعة الشريحة ستتوسع تلقائياً، بينما المصفوفات لا، لمزيد من التفاصيل انتقل لـ دليل المرجع - الطول والسعة.

TIP

التنفيذ الأساسي للشريحة لا يزال مصفوفة، وهي نوع مرجعي، يمكن فهمها ببساطة كمؤشر لمصفوفة أساسية (في الأساس الشريحة في Go هي هيكل يحتوي على مؤشر للمصفوفة الأساسية وقيمة الطول وقيمة السعة). لذا عند تمرير الشريحة كمعامل دالة لا يتم نسخ المصفوفة الأساسية، وتعديل الشريحة المُمررة داخل الدالة سيظهر في الشريحة الأصلية.

الشريحة المُعرَّفة عبر var nums []int قيمتها الافتراضية nil، لذا لن يتم تخصيص ذاكرة لها، وعند استخدام make للتهيئة، يُنصح بتخصيص سعة كافية مسبقاً، مما يقلل من استهلاك الذاكرة للتوسعات اللاحقة.

الاستخدام

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

يمكن للشرائح تنفيذ العديد من العمليات عبر دالة append، توقيع الدالة كالتالي، slice هي الشريحة المستهدف إضافة عناصر إليها، elems هي العناصر المراد إضافتها، والقيمة المُرجعة هي الشريحة بعد الإضافة:

go
func append(slice []Type, elems ...Type) []Type

أولاً إنشاء شريحة فارغة بطول 0 وسعة 0، ثم إدراج بعض العناصر في النهاية، وأخيراً إخراج الطول والسعة:

go
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 يمكن ملاحظة أن الطول والسعة غير متطابقين.

حجم المخزن المؤقت المُحجوز للشريحة الجديدة له نمط معين. قبل تحديث golang1.18، معظم المقالات على الإنترنت كانت تصف استراتيجية توسع الشريحة هكذا: عندما تكون سعة الشريحة الأصلية أقل من 1024، تصبح سعة الشريحة الجديدة ضعف السعة الأصلية؛ وعندما تتجاوز سعة الشريحة الأصلية 1024، تصبح سعة الشريحة الجديدة 1.25 ضعف السعة الأصلية. بعد تحديث الإصدار 1.18، تغيرت استراتيجية توسع الشريحة لتصبح: عندما تكون سعة الشريحة الأصلية (oldcap) أقل من 256، تصبح سعة الشريحة الجديدة (newcap) ضعف السعة الأصلية؛ وعندما تتجاوز سعة الشريحة الأصلية 256، تصبح سعة الشريحة الجديدة newcap = oldcap+(oldcap+3*256)/4

إدراج عناصر

إدراج عناصر الشريحة يحتاج أيضاً لاستخدام دالة append، لنفرض الشريحة التالية:

go
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

إدراج عناصر من البداية:

go
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

إدراج عناصر من فهرس وسط i:

go
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3، [1 2 3 4 999 999 5 6 7 8 9 10]

إدراج عناصر من النهاية، وهذا هو الاستخدام الأصلي لـ append:

go
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

حذف عناصر

حذف عناصر الشريحة يحتاج لاستخدام دالة append، لنفرض الشريحة التالية:

go
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

حذف n عنصر من البداية:

go
nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]

حذف n عنصر من النهاية:

go
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]

حذف n عنصر بدءاً من فهرس وسط i:

go
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2، n=3، [1 2 6 7 8 9 10]

حذف جميع العناصر:

go
nums = nums[:0]
fmt.Println(nums) // []

النسخ

عند نسخ الشريحة يجب التأكد من أن الشريحة المستهدفة لديها طول كافٍ، مثلاً:

go
func main() {
  dest := make([]int, 0)
  src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  fmt.Println(src, dest)
  fmt.Println(copy(dest, src))
  fmt.Println(src, dest)
}
[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []

بتعديل الطول لـ 10، المخرجات:

[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]

الاجتياح

اجتياح الشريحة مطابق تماماً للمصفوفات، حلقة for:

go
func main() {
   slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
   for i := 0; i < len(slice); i++ {
      fmt.Println(slice[i])
   }
}

حلقة for range:

go
func main() {
  slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
  for index, val := range slice {
    fmt.Println(index, val)
  }
}

الشرائح متعددة الأبعاد

لنر المثال التالي، الوثائق الرسمية أيضاً تشرح: Effective Go - شرائح ثنائية الأبعاد

go
var nums [5][5]int
for _, num := range nums {
   fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
   fmt.Println(slice)
}

نتيجة الإخراج:

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[]
[]
[]
[]
[]

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

go
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
   slices[i] = make([]int, 5)
}

نتيجة الإخراج النهائية:

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

التعبير الموسع

TIP

فقط الشرائح يمكنها استخدام التعبير الموسع

كلاً من الشرائح والمصفوفات يمكنهما استخدام التعبير البسيط للقص، لكن التعبير الموسع يمكن للشرائح فقط استخدامه، أُضيفت هذه الميزة في إصدار Go1.2، بشكل أساسي لحل مشكلة القراءة والكتابة للشرائح التي تشترك في المصفوفة الأساسية، الصيغة الرئيسية كالتالي، يجب تحقيق العلاقة low<= high <= max <= cap، وسعة الشريحة المقصوصة بالتعبير الموسع هي max-low:

go
slice[low:high:max]

low و high لا يزال لهما نفس المعنى السابق، و max المُضاف يشير للسعة القصوى، مثلاً في المثال التالي تم حذف max، إذن سعة s2 هي cap(s1)-low:

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6

هذا سيؤدي لمشكلة واضحة، s1 و s2 يشتركان في نفس المصفوفة الأساسية، عند القراءة والكتابة على s2، قد يؤثر ذلك على بيانات s1، الكود التالي يمثل هذه الحالة:

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4]                          // cap = 9 - 3 = 6
s2 = append(s2, 1)   // إضافة عنصر جديد، وبما أن السعة 6 فلا يوجد توسع، يتم تعديل المصفوفة الأساسية مباشرة
fmt.Println(s2)
fmt.Println(s1)

المخرجات النهائية:

[4 1]
[1 2 3 4 1 6 7 8 9]

يمكن ملاحظة أنه رغم أننا أضفنا عنصراً لـ s2، تم تعديل s1 أيضاً، التعبير الموسع وُجد لحل هذه المشكلة، بتعديل بسيط يمكن حل المشكلة:

go
func main() {
   s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
   s2 := s1[3:4:4]                        // cap = 4 - 3 = 1
   s2 = append(s2, 1)    // السعة غير كافية، تخصيص مصفوفة أساسية جديدة
   fmt.Println(s2)
   fmt.Println(s1)
}

النتيجة الآن صحيحة:

[4 1]
[1 2 3 4 5 6 7 8 9]

clear

في go1.21 أُضيفت الدالة المدمجة clear، clear ستضع جميع القيم داخل الشريحة للقيمة الصفرية:

go
package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s)
}

المخرجات:

[0 0 0 0]

إذا أردت مسح الشريحة، يمكنك:

go
func main() {
  s := []int{1, 2, 3, 4}
    s = s[:0:0]
  fmt.Println(s)
}

تحديد سعة الشريحة المقصوصة يمنع الكتابة فوق العناصر اللاحقة في الشريحة الأصلية.

Golang تم تحريره بواسطة www.golangdev.cn