الكتاب الأساسي

المصفوفات والشرائح والخرائط في Go

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

توجد تفاصيل كثيرة متعلقة بالحاويات في Go. سيسردها هذا المقال واحدة واحدة.

نظرة عامة مبسّطة على أنواع الحاويات وقيمها

تُستخدم كل قيمة من الأنواع الثلاثة لتخزين مجموعة من قيم العناصر. أنواع جميع العناصر المخزنة في قيمة حاوية متطابقة. ويُسمى النوع المتطابق نوع العنصر (element type) الخاص بـ (نوع الحاوية الخاص بـ) قيمة الحاوية.

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

لكل قيمة حاوية خاصية طول (length) تشير إلى عدد العناصر المخزنة في تلك الحاوية. النطاق الصالح للمفاتيح الصحيحة لقيمة مصفوفة أو شريحة هو من الصفر (شاملاً) إلى الطول (غير شامل) للمصفوفة أو الشريحة. ولكل قيمة من نوع خريطة، يمكن أن تكون قيم مفاتيح تلك الخريطة أي قيمة من نوع مفتاح نوع الخريطة.

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

تُخزَّن عناصر المصفوفة أو الشريحة معاً بشكل متجاور في قطعة ذاكرة متصلة. بالنسبة للمصفوفة، فإن قطعة الذاكرة المتصلة تستضيف الجزء المباشر من المصفوفة. وبالنسبة للشريحة، فإن قطعة الذاكرة المتصلة تستضيف الجزء الأساسي غير المباشر من الشريحة. ويعتمد تنفيذ الخرائط في المصرِّف/وقت التشغيل القياسي لـ Go على خوارزمية الجدول المبعثر (hashtable). لذا فإن جميع عناصر الخريطة تُخزَّن أيضاً في قطعة ذاكرة متصلة أساسية، لكنها قد لا تكون متجاورة. وقد توجد ثقوب (فجوات) كثيرة داخل قطعة الذاكرة المتصلة. وخوارزمية شائعة أخرى لتنفيذ الخرائط هي خوارزمية الشجرة الثنائية (binary tree). وأياً كانت الخوارزمية المستخدمة، فإن المفاتيح المرتبطة بعناصر الخريطة تُخزَّن أيضاً في (الأجزاء الأساسية لـ) الخريطة.

يمكننا الوصول إلى عنصر عبر مفتاحه. التعقيدات الزمنية للوصول إلى العناصر في جميع قيم الحاويات هي O(1) كلها، مع أن الوصول إلى عناصر الخرائط أبطأ عموماً عدة مرات من الوصول إلى عناصر المصفوفات والشرائح. لكن للخرائط ميزتان على المصفوفات والشرائح:

  • يمكن أن تكون أنواع مفاتيح الخرائط أي أنواع قابلة للمقارنة.
  • تستهلك الخرائط ذاكرة أقل بكثير من المصفوفات والشرائح إذا كانت معظم العناصر قيماً صفرية.

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

التمثيلات الحرفية لأنواع الحاويات غير المسمّاة

التمثيلات الحرفية لأنواع الحاويات الثلاثة غير المسمّاة:

  • أنواع المصفوفات: [N]T
  • أنواع الشرائح: []T
  • أنواع الخرائط: map[K]T حيث
  • T نوع اعتباطي. يحدد نوع العنصر لنوع الحاوية. ولا يمكن تخزين إلا قيم النوع المحدد كقيم عناصر لقيم نوع الحاوية.
  • يجب أن يكون N ثابتاً صحيحاً غير سالب. يحدد عدد العناصر المخزنة في أي قيمة من نوع مصفوفة، ويمكن تسميته طول نوع المصفوفة. هذا يعني أن طول نوع مصفوفة جزء أصيل من نوع المصفوفة. مثلاً، [5]int و[8]int نوعان مختلفان من المصفوفات.
  • K نوع اعتباطي قابل للمقارنة. يحدد نوع المفتاح لنوع خريطة. معظم الأنواع في Go قابلة للمقارنة، والأنواع غير القابلة للمقارنة مذكورة هنا.

إليك بعض الأمثلة على التمثيلات الحرفية لأنواع الحاويات:


const Size = 32

type Person struct {
	name string
	age  int
}

/* أنواع المصفوفات */

[5]string
[Size]int
// نوع العنصر هو نوع شريحة: []byte
[16][]byte
// نوع العنصر هو نوع بنية: Person
[100]Person

/* أنواع الشرائح */

[]bool
[]int64
// نوع العنصر هو نوع خريطة: map[int]bool
[]map[int]bool
// نوع العنصر هو نوع مؤشر: *int
[]*int

/* أنواع الخرائط */

map[string]int
map[int]bool
// نوع العنصر هو نوع مصفوفة: [6]string
map[int16][6]string
// نوع العنصر هو نوع شريحة: []string
map[bool][]string
// نوع العنصر هو نوع مؤشر: *int8،
// ونوع المفتاح هو نوع بنية.
map[struct{x int}]*int8

إن أحجام جميع أنواع الشرائح متماثلة. وأحجام جميع أنواع الخرائط متماثلة أيضاً. أما حجم نوع المصفوفة فيعتمد على طوله وحجم نوع عنصره. وحجم نوع مصفوفة بطول صفر أو نوع مصفوفة بنوع عنصر بحجم صفر هو صفر.

القيم الحرفية للحاويات

مثل قيم البنى، يمكن أيضاً تمثيل قيم الحاويات بالقيم الحرفية المركّبة (composite literals)، T{...}، حيث يشير T إلى نوع الحاوية (باستثناء القيم الصفرية لأنواع الشرائح والخرائط). وإليك بعض الأمثلة:


// قيمة مصفوفة تحتوي أربع قيم منطقية.
[4]bool{false, true, true, false}

// قيمة شريحة تحتوي ثلاث كلمات.
[]string{"break", "continue", "fallthrough"}

// قيمة خريطة تحتوي بعض أزواج المفتاح-القيمة.
map[string]int{"C": 1972, "Python": 1991, "Go": 2009}

يُسمى كل زوج مفتاح-عنصر بين القوسين لقيمة حرفية مركّبة لخريطة أيضاً مُدخَلاً (entry).

توجد عدة صيغ مختلفة للقيم الحرفية المركّبة للمصفوفات والشرائح:


// القيم الحرفية المركّبة للشرائح التالية
// متكافئة مع بعضها.
[]string{"break", "continue", "fallthrough"}
[]string{0: "break", 1: "continue", 2: "fallthrough"}
[]string{2: "fallthrough", 1: "continue", 0: "break"}
[]string{2: "fallthrough", 0: "break", "continue"}

// القيم الحرفية المركّبة للمصفوفات التالية
// متكافئة مع بعضها.
[4]bool{false, true, true, false}
[4]bool{0: false, 1: true, 2: true, 3: false}
[4]bool{1: true, true}
[4]bool{2: true, 1: true}
[...]bool{false, true, true, false}
[...]bool{3: false, 1: true, true}

في القيمتين الحرفيتين الأخيرتين، تعني ... أننا نريد أن يستنتج المصرِّف الأطوال لقيم المصفوفات المقابلة.

نعلم من الأمثلة أعلاه أن فهارس العناصر (المفاتيح) اختيارية في القيم الحرفية المركّبة للمصفوفات والشرائح. وفي القيمة الحرفية المركّبة لمصفوفة أو شريحة،

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

يجب ألا تكون مفاتيح المُدخلات في القيمة الحرفية لخريطة غائبة، ويمكن أن تكون غير ثوابت.


var a uint = 1
var _ = map[uint]int {a : 123} // لا مشكلة

// السطران التاليان يفشلان في التصريف،
// لأن "a" ليس مفتاحاً/فهرساً ثابتاً.
var _ = []int{a: 100}  // خطأ
var _ = [5]int{a: 100} // خطأ

المفاتيح الثابتة في قيمة حرفية مركّبة واحدة محددة لا يمكن أن تتكرر.

التمثيلات الحرفية للقيم الصفرية لأنواع الحاويات

مثل البنى، يمكن تمثيل القيمة الصفرية لنوع مصفوفة A بالقيمة الحرفية المركّبة A{}. مثلاً، يمكن الإشارة إلى القيمة الصفرية للنوع [100]int بالشكل [100]int{}. وجميع العناصر المخزنة في القيمة الصفرية لنوع مصفوفة هي قيم صفرية لنوع عنصر نوع المصفوفة.

ومثل أنواع المؤشرات، تُمثَّل القيم الصفرية لجميع أنواع الشرائح والخرائط بالمعرّف المُعلَن مسبقاً nil.

بالمناسبة، هناك أنواع أخرى أيضاً تُمثَّل قيمها الصفرية بـ nil، منها أنواع الدوال والقنوات والواجهات التي ستُقدَّم لاحقاً.

عند تعريف متغير مصفوفة دون تحديد قيمة ابتدائية، تكون الذاكرة قد خُصِّصت لعناصر قيمة المصفوفة الصفرية. أما ذاكرة عناصر قيمة شريحة أو خريطة بقيمة nil فلم تُخصَّص بعد.

لاحظ أن []T{} تمثل قيمة شريحة فارغة (بعناصر معدومة) من نوع الشريحة []T، وهي مختلفة عن []T(nil). والوضع نفسه ينطبق على map[K]T{} وmap[K]T(nil).

القيم الحرفية المركّبة غير قابلة لأخذ العناوين لكن يمكن أخذ عناوينها

تعلمنا سابقاً أن عناوين القيم الحرفية المركّبة للبنى يمكن أخذها مباشرة. ولا استثناء هنا للقيم الحرفية المركّبة للحاويات.

مثال:


package main

import "fmt"

func main() {
	pm := &map[string]int{"C": 1972, "Go": 2009}
	ps := &[]string{"break", "continue"}
	pa := &[...]bool{false, true, true, false}
	fmt.Printf("%T\n", pm) // *map[string]int
	fmt.Printf("%T\n", ps) // *[]string
	fmt.Printf("%T\n", pa) // *[4]bool
}

يمكن تبسيط القيم الحرفية المركّبة المتداخلة

إذا كانت قيمة حرفية مركّبة تحتوي على قيم حرفية مركّبة أخرى متداخلة كثيرة، فيمكن تبسيط تلك القيم الحرفية المركّبة المتداخلة إلى الصيغة {...}.

مثلاً، قيمة الشريحة الحرفية


// قيمة شريحة من نوع نوع عنصره
// *[4]byte. نوع العنصر هو نوع مؤشر
// نوعه الأساسي [4]byte. والنوع الأساسي
// هو نوع مصفوفة نوع عنصره "byte".
var heads = []*[4]byte{
	&[4]byte{'P', 'N', 'G', ' '},
	&[4]byte{'G', 'I', 'F', ' '},
	&[4]byte{'J', 'P', 'E', 'G'},
}

يمكن تبسيطها إلى


var heads = []*[4]byte{
	{'P', 'N', 'G', ' '},
	{'G', 'I', 'F', ' '},
	{'J', 'P', 'E', 'G'},
}

وقيمة المصفوفة الحرفية في المثال التالي


type language struct {
	name string
	year int
}

var _ = [...]language{
	language{"C", 1972},
	language{"Python", 1991},
	language{"Go", 2009},
}

يمكن تبسيطها إلى


var _ = [...]language{
	{"C", 1972},
	{"Python", 1991},
	{"Go", 2009},
}

وقيمة الخريطة الحرفية في المثال التالي


type LangCategory struct {
	dynamic bool
	strong  bool
}

// قيمة من نوع خريطة نوع مفتاحها
// نوع بنية ونوع عنصرها
// نوع خريطة آخر "map[string]int".
var _ = map[LangCategory]map[string]int{
	LangCategory{true, true}: map[string]int{
		"Python": 1991,
		"Erlang": 1986,
	},
	LangCategory{true, false}: map[string]int{
		"JavaScript": 1995,
	},
	LangCategory{false, true}: map[string]int{
		"Go":   2009,
		"Rust": 2010,
	},
	LangCategory{false, false}: map[string]int{
		"C": 1972,
	},
}

يمكن تبسيطها إلى


var _ = map[LangCategory]map[string]int{
	{true, true}: {
		"Python": 1991,
		"Erlang": 1986,
	},
	{true, false}: {
		"JavaScript": 1995,
	},
	{false, true}: {
		"Go":   2009,
		"Rust": 2010,
	},
	{false, false}: {
		"C": 1972,
	},
}

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

مقارنة قيم الحاويات

كما ذُكر في مقال نظرة عامة على نظام أنواع Go، فإن نوعي الخرائط والشرائح نوعان غير قابلين للمقارنة. لذا لا يمكن استخدام نوعي الخرائط والشرائح كنوعي مفاتيح للخرائط.

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

معظم أنواع المصفوفات قابلة للمقارنة، باستثناء تلك التي أنواع عناصرها أنواع غير قابلة للمقارنة.

عند مقارنة قيمتي مصفوفتين، ستُقارَن كل زوج من العناصر المتناظرة. ويمكننا اعتبار أن أزواج العناصر تُقارَن بترتيب فهارسها. وتكون قيمتا المصفوفتين متساويتين فقط إذا كانت جميع عناصرهما المتناظرة متساوية. وتتوقف المقارنة مبكراً عند العثور على زوج عناصر غير متساوٍ أو حدوث panic.

مثال:


package main

import "fmt"

func main() {
	var a [16]byte
	var s []int
	var m map[string]int

	fmt.Println(a == a)   // true
	fmt.Println(m == nil) // true
	fmt.Println(s == nil) // true
	fmt.Println(nil == map[string]int{}) // false
	fmt.Println(nil == []int{})          // false

	// الأسطر التالية تفشل في التصريف.
	/*
	_ = m == m
	_ = s == s
	_ = m == map[string]int(nil)
	_ = s == []int(nil)
	var x [16][]int
	_ = x == x
	var y [16]map[int]bool
	_ = y == y
	*/
}

التحقق من أطوال وسعات قيم الحاويات

إلى جانب خاصية الطول، لكل قيمة حاوية أيضاً خاصية سعة (capacity). وسعة المصفوفة تساوي دائماً طول المصفوفة. ويمكن اعتبار سعة الخريطة غير الفارغة (non-nil) غير محدودة. لذلك، في الممارسة العملية، سعات قيم الشرائح فقط هي ذات معنى. وسعة الشريحة دائماً تساوي طول الشريحة أو أكبر منه. وسيُشرح معنى سعات الشرائح في القسم الذي يلي القسم التالي.

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

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

مثال:


package main

import "fmt"

func main() {
	var a [5]int
	fmt.Println(len(a), cap(a)) // 5 5
	var s []int
	fmt.Println(len(s), cap(s)) // 0 0
	s, s2 := []int{2, 3, 5}, []bool{}
	fmt.Println(len(s), cap(s))   // 3 3
	fmt.Println(len(s2), cap(s2)) // 0 0
	var m map[int]bool
	fmt.Println(len(m)) // 0
	m, m2 := map[int]bool{1: true, 0: false}, map[int]int{}
	fmt.Println(len(m), len(m2)) // 2 0
}

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

استرجاع عناصر الحاويات وتعديلها

يُمثَّل العنصر المرتبط بالمفتاح k والمخزَّن في قيمة حاوية v بصيغة فهرسة العناصر v[k].

في استخدام v[k]، بافتراض أن v مصفوفة أو شريحة،

  • إذا كان k ثابتاً، فيجب أن يحقق المتطلبات الموضحة أعلاه الخاصة بالفهارس في القيم الحرفية المركّبة للحاويات. إضافةً إلى ذلك، إذا كانت v مصفوفة، فيجب أن يكون k أصغر من طول المصفوفة.
  • إذا كانت k قيمة غير ثابتة، فيجب أن تكون قيمة من أي نوع صحيح أساسي. إضافةً إلى ذلك، يجب أن تكون أكبر من أو تساوي الصفر وأصغر من len(v)، وإلا فسيحدث panic في وقت التشغيل.
  • إذا كانت v شريحة بقيمة nil، فسيحدث panic في وقت التشغيل.

وفي استخدام v[k]، بافتراض أن v خريطة، فيجب أن يكون k قابلاً للإسناد إلى قيم نوع عنصر نوع الخريطة، و

  • إذا كانت k قيمة واجهة نوعها الديناميكي غير قابل للمقارنة، فسيحدث panic في وقت التشغيل.
  • إذا استُخدم v[k] كقيمة هدف في إسناد وكانت v خريطة بقيمة nil، فسيحدث panic في وقت التشغيل.
  • إذا استُخدم v[k] لاسترجاع قيمة العنصر المقابل للمفتاح k في الخريطة v، فلن يحدث أي panic، حتى لو كانت v خريطة بقيمة nil. (بافتراض أن تقييم k لن يُحدث panic.)
  • إذا استُخدم v[k] لاسترجاع قيمة العنصر المقابل للمفتاح k في الخريطة v، ولم تكن الخريطة v تحتوي مُدخَلاً بالمفتاح k، فإن v[k] ينتج قيمة صفرية من نوع عنصر نوع الخريطة المقابل لـ v. وعموماً، يُعامل v[k] كتعبير أحادي القيمة. لكن عندما يُستخدم v[k] كتعبير المصدر الوحيد في إسناد، يمكن معاملته كتعبير متعدد القيم وينتج قيمة منطقية ثانية اختيارية غير مُنمّطة، تشير إلى ما إذا كانت الخريطة v تحتوي مُدخَلاً بالمفتاح k أم لا.

مثال على الوصول إلى عناصر الحاويات وتعديلها:


package main

import "fmt"

func main() {
	a := [3]int{-1, 0, 1}
	s := []bool{true, false}
	m := map[string]int{"abc": 123, "xyz": 789}
	fmt.Println (a[2], s[1], m["abc"])    // استرجاع
	a[2], s[1], m["abc"] = 999, true, 567 // تعديل
	fmt.Println (a[2], s[1], m["abc"])    // استرجاع

	n, present := m["hello"]
	fmt.Println(n, present, m["hello"]) // 0 false 0
	n, present = m["abc"]
	fmt.Println(n, present, m["abc"]) // 567 true 567
	m = nil
	fmt.Println(m["abc"]) // 0

	// السطران التاليان يفشلان في التصريف.
	/*
	_ = a[3]  // الفهرس 3 خارج الحدود
	_ = s[-1] // يجب أن يكون الفهرس غير سالب
	*/

	// كل من الأسطر التالية يمكن أن يسبب panic.
	_ = a[n]         // panic: index out of range
	_ = s[n]         // panic: index out of range
	m["hello"] = 555 // panic: assign to entry in nil map
}

تذكّر تعريف البنية الداخلية لأنواع الشرائح

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


type _slice struct {
	elements unsafe.Pointer // يشير إلى العناصر الأساسية
	len      int            // الطول
	cap      int            // السعة
}

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

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

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

وبالنسبة للشريحة المصوَّرة أعلاه، لا تنتمي العناصر من الفهرس len إلى الفهرس cap (غير شامل) إلى عناصر الشريحة. إنها مجرد خانات عناصر زائدة عن الحاجة بالنسبة للشريحة المصوَّرة، لكنها قد تكون عناصر فعّالة لشرائح أخرى أو لمصفوفة أخرى.

سيشرح القسم التالي كيفية إلحاق عناصر بشريحة أساسية (base slice) وإنتاج شريحة جديدة باستخدام الدالة المدمجة append. وقد تشترك الشريحة الناتجة عن استدعاء الدالة append في العناصر الأولى مع الشريحة الأساسية أو لا تشترك، اعتماداً على سعة الشريحة الأساسية (وطولها) وعدد العناصر الملحَقة.

عند استخدام الشريحة كشريحة أساسية في استدعاء الدالة append،

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

سيعرض القسم الذي يلي القسم التالي صورة تصف الحالتين المحتملتين في إلحاق عناصر الشريحة.

وتوجد طرق أخرى تؤدي إلى استضافة عناصر شريحتين على قطعة الذاكرة الأساسية نفسها، مثل الإسنادات وعمليات الشرائح الفرعية (subslice) التي ستُقدَّم أدناه.

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

إسنادات الحاويات

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

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

وعند إسناد مصفوفة إلى مصفوفة أخرى، تُنسخ جميع العناصر من المصفوفة المصدر إلى المصفوفة الهدف. ولا تشترك المصفوفتان في أي عناصر.

مثال:


package main

import "fmt"

func main() {
	m0 := map[int]int{0:7, 1:8, 2:9}
	m1 := m0
	m1[0] = 2
	fmt.Println(m0, m1) // map[0:2 1:8 2:9] map[0:2 1:8 2:9]

	s0 := []int{7, 8, 9}
	s1 := s0
	s1[0] = 2
	fmt.Println(s0, s1) // [2 8 9] [2 8 9]

	a0 := [...]int{7, 8, 9}
	a1 := a0
	a1[0] = 2
	fmt.Println(a0, a1) // [7 8 9] [2 8 9]
}

إلحاق عناصر الحاويات وحذفها

صيغة إلحاق زوج مفتاح-عنصر (مُدخَل) بخريطة هي نفسها صيغة تعديل عنصر خريطة. مثلاً، لقيمة خريطة m غير فارغة (non-nil)، السطر التالي


m[k] = e

يضع زوج المفتاح-العنصر (k, e) في الخريطة m إذا لم تكن m تحتوي مُدخَلاً بالمفتاح k، أو يعدّل قيمة العنصر المرتبط بـ k إذا كانت m تحتوي مُدخَلاً بالمفتاح k.

وتوجد دالة مدمجة delete تُستخدم لحذف مُدخَل من خريطة. مثلاً، السطر التالي سيحذف المُدخَل ذا المفتاح k من الخريطة m. وإذا لم تكن الخريطة m تحتوي مُدخَلاً بالمفتاح k، فالعملية لا تفعل شيئاً ولن يحدث أي panic، حتى لو كانت m خريطة بقيمة nil.


delete(m, k)

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


package main

import "fmt"

func main() {
	m := map[string]int{"Go": 2007}
	m["C"] = 1972     // إلحاق
	m["Java"] = 1995  // إلحاق
	fmt.Println(m)    // map[C:1972 Go:2007 Java:1995]
	m["Go"] = 2009    // تعديل
	delete(m, "Java") // حذف
	fmt.Println(m)    // map[C:1972 Go:2009]
}

لاحظ أنه قبل Go 1.12، كان ترتيب طباعة مُدخلات الخريطة غير محدد.

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

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

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

مثال يوضح كيفية استخدام الدالة append:


package main

import "fmt"

func main() {
	s0 := []int{2, 3, 5}
	fmt.Println(s0, cap(s0)) // [2 3 5] 3
	s1 := append(s0, 7)      // إلحاق عنصر واحد
	fmt.Println(s1, cap(s1)) // [2 3 5 7] 6
	s2 := append(s1, 11, 13) // إلحاق عنصرين
	fmt.Println(s2, cap(s2)) // [2 3 5 7 11 13] 6
	s3 := append(s0)         // <=> s3 := s0
	fmt.Println(s3, cap(s3)) // [2 3 5] 3
	s4 := append(s0, s0...)  // مضاعفة s0 كـ s4
	fmt.Println(s4, cap(s4)) // [2 3 5 2 3 5] 6

	s0[0], s1[0] = 99, 789
	fmt.Println(s2[0], s3[0], s4[0]) // 789 99 2
}

لاحظ أن الدالة المدمجة append هي دالة متغيرة الوسائط. لها وسيطان، والثاني هو وسيط متغير.

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

في المثال أعلاه، السطر 14 مكافئ لـ


	s4 := append(s0, s0[0], s0[1], s0[2])

والسطر 8 مكافئ لـ


	s1 := append(s0, []int{7}...)

والسطر 10 مكافئ لـ


	s2 := append(s1, []int{11, 13}...)

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

في البرنامج أعلاه،

  • سيخصص استدعاء append في السطر 8 قطعة ذاكرة أساسية جديدة للشريحة s1، لأن الشريحة s0 ليس لديها خانات عناصر زائدة كافية لتخزين العنصر الملحَق الجديد. والوضع نفسه ينطبق على استدعاء append في السطر 14.
  • ولن يخصص استدعاء append في السطر 10 قطعة ذاكرة أساسية جديدة للشريحة s2، لأن الشريحة s1 لديها خانات عناصر زائدة كافية لتخزين العناصر الملحَقة الجديدة.

لذا، تشترك s1 وs2 في بعض العناصر، وتشترك s0 وs3 في جميع العناصر، ولا تشترك s4 في أي عناصر مع غيرها. وتصوّر الصورة التالية حالات هذه الشرائح عند نهاية البرنامج أعلاه.

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

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


package main

import "fmt"

func main() {
	var s = append([]string(nil), "array", "slice")
	fmt.Println(s)      // [array slice]
	fmt.Println(cap(s)) // 2
	s = append(s, "map")
	fmt.Println(s)      // [array slice map]
	fmt.Println(cap(s)) // 4
	s = append(s, "channel")
	fmt.Println(s)      // [array slice map channel]
	fmt.Println(cap(s)) // 4
}

لا يمكن أن تكون المعطاة الأولى لاستدعاء الدالة append قيمة nil غير مُنمّطة (حتى Go 1.25).

إنشاء الشرائح والخرائط بالدالة المدمجة make

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

بالمناسبة، يمكن أيضاً استخدام الدالة المدمجة make لإنشاء القنوات، وهو ما سيُشرح في مقال القنوات في Go لاحقاً.

بافتراض أن M نوع خريطة وأن n عدد صحيح، يمكننا استخدام الصيغتين التاليتين لإنشاء خرائط جديدة من النوع M.


make(M, n)
make(M)

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

ملاحظة: قد تكون المعطاة الثانية n سالبة أو صفراً، وفي هذه الحالة ستُتجاهَل.

بافتراض أن S نوع شريحة، وأن length وcapacity عددان صحيحان غير سالبين، وأن length ليس أكبر من capacity، يمكننا استخدام الصيغتين التاليتين لإنشاء شرائح جديدة من النوع S. (لا يُشترط أن يكون نوعا length وcapacity متطابقين.)


make(S, length, capacity)
make(S, length)

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

وتُهيَّأ جميع العناصر في الشريحة الناتجة عن استدعاء الدالة make على القيمة الصفرية (لنوع عنصر الشريحة).

مثال على كيفية استخدام الدالة المدمجة make لإنشاء الخرائط والشرائح:


package main

import "fmt"

func main() {
	// إنشاء خرائط جديدة.
	fmt.Println(make(map[string]int)) // map[]
	m := make(map[string]int, 3)
	fmt.Println(m, len(m)) // map[] 0
	m["C"] = 1972
	m["Go"] = 2009
	fmt.Println(m, len(m)) // map[C:1972 Go:2009] 2

	// إنشاء شرائح جديدة.
	s := make([]int, 3, 5)
	fmt.Println(s, len(s), cap(s)) // [0 0 0] 3 5
	s = make([]int, 2)
	fmt.Println(s, len(s), cap(s)) // [0 0] 2 2
}

تخصيص الحاويات بالدالة المدمجة new

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

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

مثال:


package main

import "fmt"

func main() {
	m := *new(map[string]int)   // <=> var m map[string]int
	fmt.Println(m == nil)       // true
	s := *new([]int)            // <=> var s []int
	fmt.Println(s == nil)       // true
	a := *new([5]bool)          // <=> var a [5]bool
	fmt.Println(a == [5]bool{}) // true
}

قابلية أخذ عناوين عناصر الحاويات

فيما يلي بعض الحقائق حول قابلية أخذ عناوين عناصر الحاويات.

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

مثال:


package main

import "fmt"

func main() {
	a := [5]int{2, 3, 5, 7}
	s := make([]bool, 2)
	pa2, ps1 := &a[2], &s[1]
	fmt.Println(*pa2, *ps1) // 5 false
	a[2], s[1] = 99, true
	fmt.Println(*pa2, *ps1) // 99 true
	ps0 := &[]string{"Go", "C"}[0]
	fmt.Println(*ps0) // Go

	m := map[int]bool{1: true}
	_ = m
	// الأسطر التالية تفشل في التصريف.
	/*
	_ = &[3]int{2, 3, 5}[0]
	_ = &map[int]bool{1: true}[1]
	_ = &m[1]
	*/
}

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

تعلمنا من المقال السابق، أجزاء القيمة، أن كل قيمة بنية وقيمة مصفوفة تتكون من جزء مباشر واحد فقط. لذا

  • إذا كان نوع عنصر خريطة نوع بنية، فلا يمكننا تعديل كل حقل من حقول عنصر (وهو بنية) في الخريطة بشكل فردي.
  • وإذا كان نوع عنصر خريطة نوع مصفوفة، فلا يمكننا تعديل كل عنصر من عناصر عنصر (وهو مصفوفة) في الخريطة بشكل فردي.

مثال:


package main

import "fmt"

func main() {
	type T struct{age int}
	mt := map[string]T{}
	mt["John"] = T{age: 29} // تعديله ككل
	ma := map[int][5]int{}
	ma[1] = [5]int{1: 789} // تعديله ككل

	// السطران التاليان يفشلان في التصريف،
	// لأن عناصر الخريطة يمكن تعديلها جزئياً.
	/*
	ma[1][1] = 123      // خطأ
	mt["John"].age = 30 // خطأ
	*/

	// عمليات الوصول مقبولة.
	fmt.Println(ma[1][1])       // 789
	fmt.Println(mt["John"].age) // 29
}

لجعل أي تعديل مرغوب يعمل في المثال أعلاه، يجب أولاً حفظ عنصر الخريطة المقابل في متغير مؤقت، ثم يُعدَّل المتغير المؤقت حسب الحاجة، وفي النهاية تُكتب قيمة المتغير المؤقت فوق عنصر الخريطة المقابل. مثلاً،


package main

import "fmt"

func main() {
	type T struct{age int}
	mt := map[string]T{}
	mt["John"] = T{age: 29}
	ma := map[int][5]int{}
	ma[1] = [5]int{1: 789}

	t := mt["John"] // نسخة مؤقتة
	t.age = 30
	mt["John"] = t // الكتابة فوقه مرة أخرى

	a := ma[1] // نسخة مؤقتة
	a[1] = 123
	ma[1] = a // الكتابة فوقه مرة أخرى

	fmt.Println(ma[1][1], mt["John"].age) // 123 30
}

ملاحظة: القيد المذكور للتو قد يُرفع لاحقاً.

اشتقاق الشرائح من المصفوفات والشرائح

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

وتوجد صيغتان لكتابة الشرائح الفرعية (حيث baseContainer مصفوفة أو شريحة):


baseContainer[low : high]       // الصيغة ذات الفهرسَين
baseContainer[low : high : max] // الصيغة ذات الفهارس الثلاثة

الصيغة ذات الفهرسَين مكافئة لـ


baseContainer[low : high : cap(baseContainer)]

إذن الصيغة ذات الفهرسَين حالة خاصة من الصيغة ذات الفهارس الثلاثة. والصيغة ذات الفهرسَين أكثر استخداماً بكثير من الصيغة ذات الفهارس الثلاثة في الممارسة العملية.

لاحظ أن الصيغة ذات الفهارس الثلاثة مدعومة فقط منذ Go 1.2.

وفي تعبير الشريحة الفرعية، يجب أن تحقق الفهارس low وhigh وmax علاقات المتطلبات التالية.


// الصيغة ذات الفهرسَين
0 <= low <= high <= cap(baseContainer)

// الصيغة ذات الفهارس الثلاثة
0 <= low <= high <= max <= cap(baseContainer)

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

لاحظ أن

  • يمكن أن يكون الفهرسان low وhigh أكبر من len(baseContainer) معاً، ما دامت العلاقات أعلاه كلها محققة. لكن يجب ألا يكون الفهرسان أكبر من cap(baseContainer).
  • لن يسبب تعبير الشريحة الفرعية panic إذا كانت baseContainer شريحة بقيمة nil وكانت جميع الفهارس المستخدمة في التعبير صفراً. والشريحة الناتجة المشتقة من شريحة بقيمة nil تبقى شريحة بقيمة nil.

وطول الشريحة المشتقة الناتجة يساوي high - low، وسعة الشريحة المشتقة الناتجة تساوي max - low. وقد يكون طول الشريحة المشتقة أكبر من الحاوية الأساسية، لكن السعة لن تكون أبداً أكبر من الحاوية الأساسية.

وفي الممارسة العملية، وللتبسيط، غالباً ما نحذف بعض الفهارس في صيغ الشرائح الفرعية. وقواعد الحذف هي:

  • إذا كان الفهرس low يساوي صفراً، فيمكن حذفه، سواء في الصيغة ذات الفهرسَين أو الصيغة ذات الفهارس الثلاثة.
  • إذا كان high يساوي len(baseContainer)، فيمكن حذفه، لكن في الصيغة ذات الفهرسَين فقط.
  • ولا يمكن حذف max أبداً في الصيغ ذات الفهارس الثلاثة.

مثلاً، التعبيرات التالية متكافئة.


baseContainer[0 : len(baseContainer)]
baseContainer[: len(baseContainer)]
baseContainer[0 :]
baseContainer[:]
baseContainer[0 : len(baseContainer) : cap(baseContainer)]
baseContainer[: len(baseContainer) : cap(baseContainer)]

مثال على استخدام صيغ الشرائح الفرعية:


package main

import "fmt"

func main() {
	a := [...]int{0, 1, 2, 3, 4, 5, 6}
	s0 := a[:]     // <=> s0 := a[0:7:7]
	s1 := s0[:]    // <=> s1 := s0
	s2 := s1[1:3]  // <=> s2 := a[1:3]
	s3 := s1[3:]   // <=> s3 := s1[3:7]
	s4 := s0[3:5]  // <=> s4 := s0[3:5:7]
	s5 := s4[:2:2] // <=> s5 := s0[3:5:5]
	s6 := append(s4, 77)
	s7 := append(s5, 88)
	s8 := append(s7, 66)
	s3[1] = 99
	fmt.Println(len(s2), cap(s2), s2) // 2 6 [1 2]
	fmt.Println(len(s3), cap(s3), s3) // 4 4 [3 99 77 6]
	fmt.Println(len(s4), cap(s4), s4) // 2 4 [3 99]
	fmt.Println(len(s5), cap(s5), s5) // 2 2 [3 99]
	fmt.Println(len(s6), cap(s6), s6) // 3 4 [3 99 77]
	fmt.Println(len(s7), cap(s7), s7) // 3 4 [3 4 88]
	fmt.Println(len(s8), cap(s8), s8) // 4 4 [3 4 88 66]
}

وتصوّر الصورة التالية تخطيطات الذاكرة النهائية لقيم المصفوفة والشرائح المستخدمة في المثال أعلاه.

نعلم من الصورة أن عناصر الشريحتين s7 وs8 مستضافة على قطعة ذاكرة أساسية مختلفة عن الحاويات الأخرى. أما عناصر الشرائح الأخرى فمستضافة على قطعة الذاكرة نفسها التي تستضيف المصفوفة a.

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


func f() []int {
	s := make([]int, 10, 100)
	return s[50:60]
}

لاحظ أنه في الدالة أعلاه، الفهرس الأدنى (50) أكبر من طول s (10)، وهذا مسموح به.

تحويل الشريحة إلى مؤشر مصفوفة

منذ Go 1.17، يمكن تحويل شريحة إلى مؤشر مصفوفة. وفي هذا التحويل، إذا كان طول نوع المصفوفة الأساسي لنوع المؤشر أكبر من طول الشريحة، فسيحدث panic.

مثال:


package main

type S []int
type A [2]int
type P *A

func main() {
	var x []int
	var y = make([]int, 0)
	var x0 = (*[0]int)(x) // لا مشكلة، x0 == nil
	var y0 = (*[0]int)(y) // لا مشكلة، y0 != nil
	_, _ = x0, y0

	var z = make([]int, 3, 5)
	var _ = (*[3]int)(z) // لا مشكلة
	var _ = (*[2]int)(z) // لا مشكلة
	var _ = (*A)(z)      // لا مشكلة
	var _ = P(z)         // لا مشكلة

	var w = S(z)
	var _ = (*[3]int)(w) // لا مشكلة
	var _ = (*[2]int)(w) // لا مشكلة
	var _ = (*A)(w)      // لا مشكلة
	var _ = P(w)         // لا مشكلة

	var _ = (*[4]int)(z) // سينتج panic
}

تحويل الشريحة إلى مصفوفة

منذ Go 1.20، يمكن تحويل شريحة إلى مصفوفة. وفي هذا التحويل، إذا كان طول نوع المصفوفة أكبر من طول الشريحة، فسيحدث panic. ولا تشترك الشريحة والمصفوفة الناتجة في أي عنصر.

مثال:


package main

import "fmt"

func main() {
	var s = []int{0, 1, 2, 3}
	var a = [3]int(s[1:])
	s[2] = 9
	fmt.Println(s) // [0 1 9 3]
	fmt.Println(a) // [1 2 3]

	_ = [3]int(s[:2]) // panic
}

نسخ عناصر الشرائح بالدالة المدمجة copy

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

وبمساعدة صيغة الشرائح الفرعية، يمكننا استخدام الدالة copy لنسخ العناصر بين مصفوفتين أو بين مصفوفة وشريحة.

مثال:


package main

import "fmt"

func main() {
	type Ta []int
	type Tb []int
	dest := Ta{1, 2, 3}
	src := Tb{5, 6, 7, 8, 9}
	n := copy(dest, src)
	fmt.Println(n, dest) // 3 [5 6 7]
	n = copy(dest[1:], dest)
	fmt.Println(n, dest) // 2 [5 5 6]

	a := [4]int{} // مصفوفة
	n = copy(a[:], src)
	fmt.Println(n, a) // 4 [5 6 7 8]
	n = copy(a[:], a[2:])
	fmt.Println(n, a) // 2 [7 8 7 8]
}

لاحظ أنه، كحالة خاصة، يمكن استخدام الدالة المدمجة copy لـنسخ البايتات من نص إلى شريحة بايتات.

ولا يمكن أن تكون أي من معطاتَي استدعاء الدالة copy قيمة nil غير مُنمّطة (حتى Go 1.25).

التكرار على عناصر الحاويات

في Go، يمكن التكرار على مفاتيح وعناصر قيمة حاوية بالصيغة التالية:


for key, element = range aContainer {
	// استخدم key وelement ...
}

حيث for وrange كلمتان مفتاحيتان، وتسمى key وelement متغيري التكرار. وإذا كانت aContainer شريحة أو مصفوفة (أو مؤشر مصفوفة، انظر أدناه)، فيجب أن يكون نوع key هو النوع المدمج int.

ويمكن أن تكون علامة الإسناد = علامة تعريف متغير قصير :=، وفي هذه الحالة يكون متغيرا التكرار متغيرين جديدين معرَّفين ولا يظهران إلا داخل جسم كتلة الشيفرة for-range، وإذا كانت aContainer شريحة أو مصفوفة (أو مؤشر مصفوفة)، فيُستنتج نوع key على أنه int.

ومثل كتلة حلقة for التقليدية، تُنشئ كل كتلة حلقة for-range كتلتي شيفرة، إحداهما ضمنية والأخرى صريحة تتشكل باستخدام {}. والكتلة الصريحة متداخلة في الكتلة الضمنية.

ومثل كتل حلقات for، يمكن أيضاً استخدام جملتي break وcontinue في كتل حلقات for-range،

مثال:


package main

import "fmt"

func main() {
	m := map[string]int{"C": 1972, "C++": 1983, "Go": 2009}
	for lang, year := range m {
		fmt.Printf("%v: %v \n", lang, year)
	}

	a := [...]int{2, 3, 5, 7, 11}
	for i, prime := range a {
		fmt.Printf("%v: %v \n", i, prime)
	}

	s := []string{"go", "defer", "goto", "var"}
	for i, keyword := range s {
		fmt.Printf("%v: %v \n", i, keyword)
	}
}

ولصيغة كتلة الشيفرة for-range عدة أشكال مختلفة:


// تجاهل متغير التكرار key.
for _, element = range aContainer {
	// ...
}

// تجاهل متغير التكرار element.
for key, _ = range aContainer {
	element = aContainer[key]
	// ...
}

// متغير التكرار element محذوف.
// هذا الشكل مكافئ للأخير.
for key = range aContainer {
	element = aContainer[key]
	// ...
}

// تجاهل متغيري التكرار key وelement معاً.
for _, _ = range aContainer {
	// هذا الشكل ليس مفيداً كثيراً.
}

// متغيرا التكرار key وelement كلاهما
// محذوفان. هذا الشكل مكافئ للأخير.
for range aContainer {
	// هذا الشكل ليس مفيداً كثيراً.
}

يُسمح بالتكرار على خرائط بقيمة nil أو شرائح بقيمة nil. ومثل هذه التكرارات لا تفعل شيئاً.

وبعض التفاصيل حول التكرار على الخرائط مذكورة هنا.

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

إذا كان مضموناً عدم وجود goroutines أخرى تتلاعب بالخريطة m، فالشيفرة التالية مضمونة لمسح جميع المُدخلات (عدا تلك التي مفاتيحها NaN) المخزنة في الخريطة m:


for key := range m {
	delete(m, key)
}

(ملاحظة: قدّمت Go 1.21 دالة clear المدمجة، التي يمكن استخدامها لمسح جميع مُدخلات خريطة، بما فيها تلك التي مفاتيحها NaN.)

وبالتأكيد، يمكن أيضاً التكرار على عناصر المصفوفات والشرائح باستخدام كتلة حلقة for التقليدية:


for i := 0; i < len(anArrayOrSlice); i++ {
	// ... استخدم anArrayOrSlice[i]
}

وبالنسبة لكتلة حلقة for-range (سواء كان قبل range علامة = أو :=)


for key, element = range aContainer {...}

هناك حقيقتان مهمتان.

  1. الحاوية المكرَّر عليها هي نسخة من aContainer. لاحظ أن الجزء المباشر فقط من aContainer هو الذي يُنسخ. ونسخة الحاوية مجهولة، لذا لا توجد طرق لتعديلها. - إذا كانت aContainer مصفوفة، فلن تنعكس التعديلات التي تُجرى على عناصر المصفوفة أثناء التكرار على متغيري التكرار. والسبب أن نسخة المصفوفة لا تشترك في العناصر مع المصفوفة.
  • وإذا كانت aContainer شريحة أو خريطة، فستنعكس التعديلات التي تُجرى على عناصر الشريحة أو الخريطة أثناء التكرار على متغيري التكرار. والسبب أن نسخة الشريحة (أو الخريطة) تشترك في جميع العناصر (المُدخلات) مع الشريحة (أو الخريطة).
  1. سيُسنَد زوج مفتاح-عنصر من نسخة aContainer (ويُنسخ) إلى زوج متغيري التكرار في كل خطوة تكرار، لذا لن تنعكس التعديلات التي تُجرى على الأجزاء المباشرة لمتغيري التكرار على العناصر (والمفاتيح بالنسبة للخرائط) المخزنة في aContainer. (لهذه الحقيقة، وبما أن استخدام كتل حلقات for-range هو الطريقة الوحيدة للتكرار على مفاتيح وعناصر الخرائط، يُنصح بعدم استخدام أنواع كبيرة الحجم كنوعي مفاتيح وعناصر الخرائط، لتجنب أعباء النسخ الكبيرة.)

مثال يثبت الحقيقتين الأولى والثانية.


package main

import "fmt"

func main() {
	type Person struct {
		name string
		age  int
	}
	persons := [2]Person {{"Alice", 28}, {"Bob", 25}}
	for i, p := range persons {
		fmt.Println(i, p)

		// هذا التعديل ليس له تأثير على
		// التكرار، لأن المصفوفة المكرَّر عليها
		// نسخة من مصفوفة persons.
		persons[1].name = "Jack"

		// هذا التعديل ليس له تأثير على
		// مصفوفة persons، لأن p مجرد
		// نسخة من نسخة لعنصر من عناصر persons.
		p.age = 31
	}
	fmt.Println("persons:", &persons)
}

المخرجات:


0 {Alice 28}
1 {Bob 25}
persons: &[{Alice 28} {Jack 25}]

إذا غيّرنا المصفوفة أعلاه إلى شريحة، فإن التعديل على الشريحة أثناء التكرار يكون له تأثير على التكرار، لكن التعديل على متغير التكرار ليس له تأثير على الشريحة.


...

	// شريحة.
	persons := []Person {{"Alice", 28}, {"Bob", 25}}
	for i, p := range persons {
		fmt.Println(i, p)

		// الآن هذا التعديل له تأثير
		// على التكرار.
		persons[1].name = "Jack"

		// هذا التعديل لا يزال ليس له
		// أي تأثير حقيقي.
		p.age = 31
	}
	fmt.Println("persons:", &persons)
}

وتصبح المخرجات:


0 {Alice 28}
1 {Jack 25}
persons: &[{Alice 28} {Jack 25}]

ويثبت المثال التالي الحقيقة الثانية.


package main

import "fmt"

func main() {
	m := map[int]struct{ dynamic, strong bool } {
		0: {true, false},
		1: {false, true},
		2: {false, false},
	}

	for _, v := range m {
		// هذا السطر التالي ليس له تأثير على الخريطة m.
		v.dynamic, v.strong = true, true
	}

	fmt.Println(m[0]) // {true false}
	fmt.Println(m[1]) // {false true}
	fmt.Println(m[2]) // {false false}
}

كلفة إسناد شريحة أو خريطة صغيرة، لكن كلفة إسناد مصفوفة كبيرة إذا كان حجم نوع المصفوفة كبيراً. لذا، ليس من الجيد عموماً التكرار بـ range على مصفوفة كبيرة. ويمكننا التكرار بـ range على شريحة مشتقة من المصفوفة، أو التكرار بـ range على مؤشر إلى المصفوفة (انظر القسم التالي للتفاصيل).

وبالنسبة لمصفوفة أو شريحة، إذا كان حجم نوع عنصرها كبيراً، فليس من الجيد عموماً أيضاً استخدام متغير التكرار الثاني لتخزين العنصر المكرَّر عليه في كل خطوة حلقة. ولمثل هذه المصفوفات والشرائح، ينبغي استخدام شكل حلقة for-range بمتغير تكرار واحد أو حلقة for التقليدية للتكرار على عناصرها. وفي المثال التالي، الحلقة في الدالة fa أقل كفاءة بكثير من الحلقة في الدالة fb.


type Buffer struct {
	start, end int
	data       [1024]byte
}

func fa(buffers []Buffer) int {
	numUnreads := 0
	for _, buf := range buffers {
		numUnreads += buf.end - buf.start
	}
	return numUnreads
}

func fb(buffers []Buffer) int {
	numUnreads := 0
	for i := range buffers {
		numUnreads += buffers[i].end - buffers[i].start
	}
	return numUnreads
}

قبل Go 1.22، عندما تُنفَّذ كتلة حلقة for-range (لاحظ أن العلامة قبل range هي :=)


for key, element := range aContainer {...}

تُسنَد جميع أزواج المفتاح-العنصر إلى زوج متغيرَي التكرار نفسه. لكن منذ Go 1.22، سيُسنَد كل زوج مفتاح-عنصر إلى زوج متغيرَي تكرار مميز (أي ستُنشأ نسخة مميزة لكل متغير تكرار في كل تكرار حلقة).

ويوضح المثال التالي اختلافات السلوك بين إصدارات Go 1.21- و1.22+.


// forrange1.go
package main

import "fmt"

func main() {
	for i, n := range []int{0, 1, 2} {
		defer func() {
			fmt.Println(i, n)
		}()
	}
}

وباستخدام إصدارات مختلفة من سلسلة أدوات Go لتشغيل الشيفرة (gotv أداة تُستخدم لإدارة واستخدام تثبيتات متعددة متعايشة من إصدارات سلسلة أدوات Go الرسمية)، سنحصل على مخرجات مختلفة:


$ gotv 1.21. run forrange1.go
[Run]: $HOME/.cache/gotv/tag_go1.21.8/bin/go run forrange1.go
2 2
2 2
2 2
$ gotv 1.22. run forrange1.go
[Run]: $HOME/.cache/gotv/tag_go1.22.1/bin/go run forrange1.go
2 2
1 1
0 0

مثال آخر:


// forrange2.go
package main

import "fmt"

func main() {
	var m = map[*int]uint32{}
	for i, n := range []int{1, 2, 3} {
		m[&i]++
		m[&n]++
	}
	fmt.Println(len(m))
}

وباستخدام إصدارات مختلفة من سلسلة أدوات Go، سنحصل على المخرجات التالية:


$ gotv 1.21. run forrange2.go
[Run]: $HOME/.cache/gotv/tag_go1.21.8/bin/go run forrange2.go
2
$ gotv 1.22. run forrange2.go
[Run]: $HOME/.cache/gotv/tag_go1.22.1/bin/go run forrange2.go
6

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

استخدام مؤشرات المصفوفات كمصفوفات

في سيناريوهات كثيرة، يمكننا استخدام مؤشر إلى مصفوفة كما لو كان مصفوفة.

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


package main

import "fmt"

func main() {
	var a [100]int

	// نسخ مؤشر غير مكلف.
	for i, n := range &a {
		fmt.Println(i, n)
	}

	// نسخ شريحة غير مكلف.
	for i, n := range a[:] {
		fmt.Println(i, n)
	}
}

إذا لم يُتجاهَل متغير التكرار الثاني في حلقة for-range ولم يُحذف، فإن استخدام range على مؤشر مصفوفة بقيمة nil سينتج panic. وفي المثال التالي، ستطبع كل من كتلتي الحلقة الأولى والثانية خمسة فهارس، أما الأخيرة فستنتج panic.


package main

import "fmt"

func main() {
	var p *[5]int // nil

	for i, _ := range p { // لا مشكلة
		fmt.Println(i)
	}

	for i := range p { // لا مشكلة
		fmt.Println(i)
	}

	for i, n := range p { // panic
		fmt.Println(i, n)
	}
}

ويمكن أيضاً استخدام مؤشرات المصفوفات لفهرسة عناصر المصفوفة. وفهرسة عناصر المصفوفة عبر مؤشر مصفوفة بقيمة nil تنتج panic.


package main

import "fmt"

func main() {
	a := [5]int{2, 3, 5, 7, 11}
	p := &a
	p[0], p[1] = 17, 19
	fmt.Println(a) // [17 19 5 7 11]
	p = nil
	_ = p[0] // panic
}

ويمكننا أيضاً اشتقاق شرائح من مؤشرات المصفوفات. واشتقاق الشرائح من مؤشر مصفوفة بقيمة nil ينتج panic.


package main

import "fmt"

func main() {
	pa := &[5]int{2, 3, 5, 7, 11}
	s := pa[1:3]
	fmt.Println(s) // [3 5]
	pa = nil
	s = pa[0:0] // panic
	// لو نُفِّذ هذا السطر، فسينتج panic أيضاً.
	_ = (*[0]byte)(nil)[:]
}

ويمكننا أيضاً تمرير مؤشرات المصفوفات كمعطيات للدالتين المدمجتين len وcap. ولن تنتج معطيات مؤشر المصفوفة بقيمة nil لهاتين الدالتين أي panic.


var pa *[5]int // == nil
fmt.Println(len(pa), cap(pa)) // 5 5

استخدام الدالة المدمجة clear لمسح مُدخلات الخريطة وإعادة ضبط عناصر الشريحة

قدّمت Go 1.21 الدالة المدمجة clear. وتُستخدم هذه الدالة لمسح مُدخلات الخريطة وإعادة ضبط عناصر الشريحة.

مثال:


package main

import "fmt"

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

a := [4]int{5, 6, 7, 8}
clear(a[1:3])
fmt.Println(a) // [5 0 0 8]

m := map[float64]float64{}
x := 0.0
m[x] = x
x /= x // x أصبح الآن NaN
m[x] = x
fmt.Println(len(m)) // 2
for k := range m {
	delete(m, k)
}
fmt.Println(len(m)) // 1
clear(m)
fmt.Println(len(m)) // 0
}

ويمكننا أن نجد أن الدالة clear تستطيع حتى حذف مُدخلات الخريطة التي مفاتيحها NaN.

تحسين memclr

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

وجُمعت هذه التحسينات (وكثير غيرها) في كتاب Go Optimizations 101.

قد تُقيَّم استدعاءات الدالتين المدمجتين len وcap في وقت التصريف

إذا كانت المعطاة الممرَّرة إلى استدعاء الدالة المدمجة len أو cap قيمة مصفوفة أو مؤشر مصفوفة، فيُقيَّم الاستدعاء في وقت التصريف وتكون نتيجة الاستدعاء ثابتاً مُنمّطاً نوعه النوع المدمج int. ويمكن ربط النتيجة بثوابت مسمّاة.

مثال:


package main

import "fmt"

var a [5]int
var p *[7]string

// كلا N وM ثابتان مُنمّطان.
const N = len(a)
const M = cap(p)

func main() {
	fmt.Println(N) // 5
	fmt.Println(M) // 7
}

وتوجد سيناريوهات أخرى تُقيَّم فيها استدعاءات الدالتين المدمجتين len وcap في وقت التصريف. اقرأ كتاب Go Details & Tips 101 لمثل هذه الحالات.

تعديل خاصيتَي الطول والسعة لقيمة شريحة بشكل فردي

ذُكر أعلاه أنه لا يمكن عموماً تعديل طول قيمة شريحة وسعتها بشكل فردي. ولا يمكن الكتابة فوق قيمة شريحة إلا ككل بإسناد قيمة شريحة أخرى إليها. لكن يمكننا تعديل طول شريحة وسعتها بشكل فردي باستخدام الانعكاس. وسيُشرح الانعكاس بالتفصيل في مقال لاحق.

مثال:


package main

import (
	"fmt"
	"reflect"
)

func main() {
	s := make([]int, 2, 6)
	fmt.Println(len(s), cap(s)) // 2 6

	reflect.ValueOf(&s).Elem().SetLen(3)
	fmt.Println(len(s), cap(s)) // 3 6

	reflect.ValueOf(&s).Elem().SetCap(5)
	fmt.Println(len(s), cap(s)) // 3 5
}

يجب ألا تكون المعطاة الثانية الممرَّرة إلى الدالة reflect.SetLen أكبر من السعة الحالية للشريحة المعطاة s. ويجب ألا تكون المعطاة الثانية الممرَّرة إلى الدالة reflect.SetCap أصغر من الطول الحالي للشريحة المعطاة s ولا أكبر من السعة الحالية للشريحة المعطاة s. وإلا فسيحدث panic.

وطريقة الانعكاس غير كفؤة جداً، فهي أبطأ من إسناد شريحة.

مزيد من معالجات الشرائح

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

اقرأ فصل «Arrays and Slices» في كتاب Go Optimizations 101 لمعرفة كيفية تنفيذ مثل هذه العمليات بكفاءة.

استخدام الخرائط لمحاكاة المجموعات

لا تدعم Go أنواع المجموعات المدمجة. لكن من السهل استخدام نوع خريطة لمحاكاة نوع مجموعة. وفي الممارسة العملية، غالباً ما نستخدم نوع الخريطة map[K]struct{} لمحاكاة نوع مجموعة نوع عنصره K. وحجم نوع عنصر الخريطة struct{} هو صفر، وعناصر قيم مثل أنواع الخرائط هذه لا تشغل مساحة ذاكرة.

العمليات المتعلقة بالحاويات ليست متزامنة داخلياً

لاحظ أن جميع عمليات الحاويات ليست متزامنة داخلياً. وبدون استخدام أي تقنية لمزامنة البيانات، لا مشكلة في أن تقرأ عدة goroutines حاوية بشكل متزامن، لكن لا يصح أن تتلاعب عدة goroutines بحاوية بشكل متزامن وكانت واحدة منها على الأقل تعدّل الحاوية. والحالة الأخيرة ستسبب تسابقات بيانات (data races)، بل قد تجعل الـ goroutines تنتج panic. ويجب أن نزامن عمليات الحاويات يدوياً. اقرأ المقالات عن مزامنات البيانات للتفاصيل.