المصفوفات في لغة Go
1.1 تعريف وتعليم المصفوفات
المصفوفة هي تسلسل ثابت الحجم من العناصر من نفس النوع. في لغة Go ، يُعتبر طول المصفوفة جزءًا من نوع المصفوفة. هذا يعني أن المصفوفات ذات أطوال مختلفة تُعامل على أنها أنواع مختلفة.
الصيغة الأساسية لتعريف مصفوفة هي كما يلي:
var arr [n]T
هنا ، var
هو الكلمة الرئيسية لتعريف المتغير ، arr
هو اسم المصفوفة ، و n
يمثل طول المصفوفة ، و T
يمثل نوع العناصر في المصفوفة.
على سبيل المثال ، لتعريف مصفوفة تحتوي على 5 أرقام صحيحة:
var myArray [5]int
في هذا المثال ، myArray
هي مصفوفة تستطيع أن تحتوي على 5 أرقام صحيحة من نوع int
.
1.2 تهيئة واستخدام المصفوفات
يمكن تهيئة المصفوفات مباشرة أثناء التعريف أو عن طريق تعيين القيم باستخدام فهارس. هناك طرق متعددة لتهيئة المصفوفة:
التهيئة المباشرة
var myArray = [5]int{10, 20, 30, 40, 50}
كما يمكن السماح للمترجم بالاستنتاج من طول المصفوفة استنادًا إلى عدد القيم المهيئة:
var myArray = [...]int{10, 20, 30, 40, 50}
هنا ، ...
يشير إلى أن المترجم يقوم بحساب طول المصفوفة.
التهيئة باستخدام الفهارس
var myArray [5]int
myArray[0] = 10
myArray[1] = 20
// يتم تهيئة العناصر المتبقية إلى 0 ، حيث أن القيمة الافتراضية للصحيح هي 0
كما أن استخدام المصفوفات بسيط أيضًا ، حيث يمكن الوصول إلى العناصر باستخدام الفهارس:
fmt.Println(myArray[2]) // الوصول إلى العنصر الثالث
1.3 جولة المصفوفات
يتم استخدام طريقتين شائعتين لجولة المصفوفة باستخدام حلقة for
التقليدية واستخدام range
.
الجولة باستخدام حلقة for
for i := 0; i < len(myArray); i++ {
fmt.Println(myArray[i])
}
الجولة باستخدام range
for index, value := range myArray {
fmt.Printf("الفهرس: %d, القيمة: %d\n", index, value)
}
ميزة استخدام range
هي أنه يعيد قيمتين: موضع الفهرس الحالي والقيمة في هذا الموضع.
1.4 خصائص وقيود المصفوفات
في لغة Go ، تعتبر المصفوفات أنواع قيم ، وهذا يعني أنه عندما يتم تمرير مصفوفة كمعلمة إلى دالة ، يتم تمرير نسخة من المصفوفة. لذلك ، إذا كانت هناك حاجة لتعديلات على المصفوفة الأصلية داخل دالة ، فعادةً ما يتم استخدام شرائح أو مؤشرات على المصفوفات.
2 الشرائح في لغة Go
2.1 مفهوم الشرائح
في لغة Go ، الشريحة هي تجريد فوق المصفوفة. حجم مصفوفة غو ثابت ، مما يحد من استخدامها في بعض السيناريوهات. تم تصميم الشرائح في Go لتكون أكثر مرونة ، وتوفير واجهة مريحة ومرنة وقوية لتسلسل هياكل البيانات. الشرائح بحد ذاتها لا تحتفظ بالبيانات. إنها مراجع فقط للمصفوفة الأساسية. يتميز طابعهم الديناميكي أساسًا من خلال النقاط التالية:
- الحجم الديناميكي: على عكس المصفوفات ، يمكن أن يكون طول الشريحة ديناميكيًا ، مما يسمح لها بالنمو أو الانكماش تلقائيًا حسب الحاجة.
-
المرونة: يمكن بسهولة إضافة العناصر إلى الشريحة باستخدام وظيفة
append
المدمجة. - نوع المرجع: تقوم الشرائح بالوصول إلى العناصر في المصفوفة الأساسية بالمرجع ، دون إنشاء نسخ من البيانات.
2.2 تعريف وتهيئة الشرائح
يشبه الصيغة لتعريف شريحة تعريف مصفوفة ، ولكن ليس من الضروري تحديد عدد العناصر عند التعريف. على سبيل المثال ، الطريقة لتعريف شريحة من الأرقام الصحيحة هي كما يلي:
var slice []int
يمكنك تهيئة الشريحة باستخدام اللفة الشمل:
slice := []int{1, 2, 3}
سيتم تهيئة المتغير slice
أعلاه كشريحة تحتوي على ثلاثة أرقام صحيحة.
يمكنك أيضًا تهيئة الشريحة باستخدام دالة make
، والتي تسمح لك بتحديد طول وسعة الشريحة:
slice := make([]int, 5) // إنشاء شريحة من الأرقام الصحيحة بطول وسعة قدره 5
إذا كانت هناك حاجة لسعة أكبر ، يمكنك تمرير السعة كثالث معلمة إلى دالة make
:
slice := make([]int, 5, 10) // إنشاء شريحة من الأرقام الصحيحة بطول 5 وسعة 10
2.3 العلاقة بين الشرائح والمصفوفات
يمكن إنشاء الشرائح عن طريق تحديد قطاع من المصفوفة، مكوّنًا إشارة إلى هذا القطاع. على سبيل المثال، باعتبار المصفوفة التالية:
array := [5]int{10, 20, 30, 40, 50}
يمكننا إنشاء شريحة على النحو التالي:
slice := array[1:4]
ستشير هذه الشريحة slice
إلى العناصر في المصفوفة array
من المؤشر 1 إلى المؤشر 3 (شاملًا للمؤشر 1، ولكن استثنائي للمؤشر 4).
من المهم ملاحظة أن الشريحة لا تنسخ قيم المصفوفة فعليًا؛ بل تشير فقط إلى قطاع مستمر من المصفوفة الأصلية. لذلك، التعديلات على الشريحة ستؤثر أيضًا على المصفوفة الأساسية، والعكس صحيح. فهم هذه العلاقة المرجعية أمر بالغ الأهمية لاستخدام الشرائح بفعالية.
2.4 العمليات الأساسية على الشرائح
2.4.1 الترقيم
تصل الشرائح إلى عناصرها باستخدام فهارس، مشابهة للمصفوفات، مع بدء الترقيم من 0. على سبيل المثال:
slice := []int{10, 20, 30, 40}
// الوصول إلى العناصر الأولى والثالثة
fmt.Println(slice[0], slice[2])
2.4.2 الطول والسعة
تحتوي الشرائح على خاصيتين: الطول (len
) والسعة (cap
). الطول هو عدد العناصر في الشريحة، والسعة هي عدد العناصر من العنصر الأول في الشريحة حتى نهاية المصفوفة الأساسية.
slice := []int{10, 20, 30, 40}
// طباعة الطول والسعة للشريحة
fmt.Println(len(slice), cap(slice))
2.4.3 إضافة العناصر
يتم استخدام الدالة append
لإضافة عناصر إلى شريحة. عندما لا تكون سعة الشريحة كافية لاحتواء العناصر الجديدة، توسّع الدالة append
تلقائيًا سعة الشريحة.
slice := []int{10, 20, 30}
// إضافة عنصر واحد
slice = append(slice, 40)
// إضافة عناصر متعددة
slice = append(slice, 50, 60)
fmt.Println(slice)
من المهم ملاحظة أنه عند استخدام append
لإضافة عناصر، قد يُرجع عملية append
شريحة جديدة. إذا كانت سعة المصفوفة الأساسية غير كافية، سيؤدي إجراء append
إلى جعل الشريحة تشير إلى مصفوفة جديدة أكبر.
2.5 توسيع ونسخ الشرائح
يمكن استخدام الدالة copy
لنسخ عناصر شريحة إلى شريحة أخرى. يجب أن تكون الشريحة الهدف قد حجزت بالفعل مساحة كافية لاحتواء العناصر المنسوخة، ولن تغير العملية السعة للشريحة الهدف.
2.5.1 استخدام دالة copy
يوضح الكود التالي كيفية استخدام copy
:
src := []int{1, 2, 3}
dst := make([]int, 3)
// نسخ العناصر إلى الشريحة الهدف
copied := copy(dst, src)
fmt.Println(dst, copied)
تُرجع دالة copy
عدد العناصر المنسوخة، ولن تتجاوز طول الشريحة الهدف أو طول الشريحة المصدر، أيهما أقل.
2.5.2 الاعتبارات
عند استخدام دالة copy
، إذا تمت إضافة عناصر جديدة للنسخ وكانت الشريحة الهدف لا تحتوي على مساحة كافية، سيتم نسخ العناصر التي يمكن للشريحة الهدف استيعابها فقط.
2.6 الشرائح متعددة الأبعاد
الشريحة متعددة الأبعاد هي شريحة تحتوي على شرائح متعددة. وهي شبيهة بالمصفوفة متعددة الأبعاد، ولكن بسبب الطول المتغير للشرائح، فإن الشرائح متعددة الأبعاد أكثر مرونة.
2.6.1 إنشاء شرائح متعددة الأبعاد
إنشاء شريحة ثنائية الأبعاد (شريحة من شرائح):
twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
twoD[i] = make([]int, 3)
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("الشريحة ثنائية الأبعاد: ", twoD)
2.6.2 استخدام الشرائح متعددة الأبعاد
استخدام شريحة متعددة الأبعاد مشابه لاستخدام شريحة ذات بُعد واحد، يتم الوصول إليها بواسطة الفهرس:
// الوصول إلى عناصر الشريحة ثنائية الأبعاد
val := twoD[1][2]
fmt.Println(val)
3 مقارنة تطبيقات المصفوفات والشرائح
3.1 مقارنة سيناريوهات الاستخدام
تُستخدم المصفوفات والشرائح في Go لتخزين مجموعات من نفس نوع البيانات، لكن لديهما اختلافات مميزة في سيناريوهات الاستخدام.
المصفوفات:
- طول المصفوفة يكون ثابتًا عند الإعلان، مما يجعلها مناسبة لتخزين عدد معروف وثابت من العناصر.
- عندما يتطلب الأمر حاوية ذات حجم ثابت، مثل تمثيل مصفوفة ذات حجم ثابت، المصفوفة الخيار الأفضل.
- يمكن تخصيص المصفوفات على الستاك، مما يوفر أداءً أعلى عندما لا يكون حجم المصفوفة كبيرًا.
الشرائح:
- الشريحة هي تجريد لمصفوفة ديناميكية، بطول متغير، مناسبة لتخزين كمية غير معروفة أو مجموعة من العناصر التي يمكن تغييرها ديناميكياً.
- عندما يُطلب مصفوفة ديناميكية يمكن أن تنمو أو تنكمش كما يلزم، مثل تخزين إدخالات مستخدم غير مؤكدة، الشريحة هي الخيار الأكثر مناسبة.
- تتيح تخطيط الذاكرة للشرائح مراجعة مريحة لجزء أو كامل المصفوفة، وتُستخدم عادةً لمعالجة القوائم الفرعية، وتقسيم محتوى الملف، وسيناريوهات أخرى.
في الختام، المصفوفات مناسبة للسيناريوهات ذات متطلبات حجم ثابت، تعكس خصائص إدارة الذاكرة الثابتة في Go، بينما الشرائح أكثر مرونةً، وتعتبر تمديداً تجريدياً للمصفوفات، مريحة لمعالجة المجموعات الديناميكية.
3.2 النظر في الأداء
عندما نحتاج إلى اختيار بين استخدام مصفوفة أو شريحة، الأداء هو عامل مهم يجب مراعاته.
المصفوفة:
- سرعة وصول سريعة، بسبب الذاكرة المستمرة والفهرسة الثابتة.
- تخصيص الذاكرة على الستاك (إذا كان حجم المصفوفة معروفًا وليس كبيرًا جدًا)، دون الضلوع في تكاليف ذاكرة الهيب.
- لا توجد ذاكرة إضافية لتخزين الطول والسعة، مما يمكن أن يكون مفيداً للبرامج الحساسة للذاكرة.
الشريحة:
- النمو الديناميكي أو التقليص يمكن أن يؤدي إلى تكاليف أداء: النمو قد يتطلب تخصيص ذاكرة جديدة ونسخ العناصر القديمة، بينما التقليص قد يتطلب ضبط النقاط.
- عمليات الشريحة ذاتها سريعة، ولكن الإضافات أو الحذف المتكررة للعناصر قد تؤدي إلى تشتت الذاكرة.
- على الرغم من أن الوصول إلى الشريحة يتضمن تكلفة جانبية صغيرة، إلا أنها عمومًا لا تؤثر بشكل كبير على الأداء ما لم تكن في رمز شديد الحساسية للأداء.
لذا، إذا كان الأداء مراعٍ أمرًا رئيسيًا وكان حجم البيانات معروفًا مسبقًا، فإن استخدام المصفوفة أكثر مناسبة. ومع ذلك، إذا كانت المرونة والراحة مطلوبة، فإن استخدام الشريحة مُستحسن، خاصة لمعالجة مجموعات البيانات الكبيرة.
4 المشاكل الشائعة والحلول
أثناء استخدام المصفوفات والشرائح في لغة Go، قد يواجه المطورون المشاكل الشائعة التالية.
المشكلة 1: خارج نطاق المصفوفة
- يُشير خارج نطاق المصفوفة إلى الوصول إلى فهرس يتجاوز طول المصفوفة. وسيؤدي هذا إلى حدوث خطأ في وقت تشغيل البرنامج.
- الحل: تحقق دائمًا مما إذا كانت قيمة الفهرس داخل النطاق الصالح للمصفوفة قبل الوصول إلى عناصر المصفوفة. يمكن تحقيق هذا من خلال مقارنة الفهرس بطول المصفوفة.
var arr [5]int
index := 10 // نفترض فهرساً خارج النطاق
if index < len(arr) {
fmt.Println(arr[index])
} else {
fmt.Println("الفهرس خارج نطاق المصفوفة.")
}
المشكلة 2: تسرب الذاكرة في الشرائح
- قد تحمل الشرائح مراجعات غير مقصودة لجزء أو كل المصفوفة الأصلية، حتى إذا لم يتم استخدام جزء صغير. وهذا يمكن أن يؤدي إلى تسرب الذاكرة إذا كانت المصفوفة الأصلية كبيرة.
- الحل: إذا كانت الشرائح المؤقتة مطلوبة، فكر في إنشاء شريحة جديدة عن طريق نسخ الجزء المطلوب.
original := make([]int, 1000000)
smallSlice := make([]int, 10)
copy(smallSlice, original[:10]) // نسخ الجزء المطلوب فقط
// بهذه الطريقة، لا تشير smallSlice إلى أجزاء أخرى من original، مما يساعد في استرداد الذاكرة غير الضرورية
المشكلة 3: أخطاء البيانات الناتجة عن إعادة استخدام الشرائح
- نتيجة لمشاركة الشرائح للإشارة إلى نفس المصفوفة الأساسية، قد يكون من الممكن رؤية تأثير التعديلات على البيانات في شرائح مختلفة، مما يؤدي إلى أخطاء غير متوقعة.
- الحل: لتجنب هذا الوضع، من الأفضل إنشاء نسخة جديدة من الشريحة.
sliceA := []int{1, 2, 3, 4, 5}
sliceB := make([]int, len(sliceA))
copy(sliceB, sliceA)
sliceB[0] = 100
fmt.Println(sliceA[0]) // الناتج: 1
fmt.Println(sliceB[0]) // الناتج: 100
ما سبق هو فقط بعض المشاكل والحلول الشائعة التي قد تنشأ عند استخدام المصفوفات والشرائح في لغة Go. قد تكون هناك تفاصيل أكثر للاهتمام بها في التطوير الفعلي، ولكن اتباع هذه المبادئ الأساسية يمكن أن يساعد في تجنب الكثير من الأخطاء الشائعة.