"إذا أراد العامل أن يؤدي عمله بشكل جيد، فعليه أولاً أن يشحذ أدواته." - كونفوشيوس، "مختارات كونفوشيوس. لو لينجونج"
الصفحة الأمامية > برمجة > كيف تعمل المصفوفات وتصبح صعبة مع For-Range

كيف تعمل المصفوفات وتصبح صعبة مع For-Range

تم النشر بتاريخ 2024-08-22
تصفح:662

هذا مقتطف من التدوينة؛ المنشور الكامل متاح هنا: كيف تعمل المصفوفات وتصبح صعبة مع For-Range.

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

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

سنغطي الشرائح في الجزء التالي، وسأضعها هنا عندما تصبح جاهزة.

ما هي المصفوفة؟

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

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

func main() {
    arr := [5]byte{0, 1, 2, 3, 4}
    println("arr", &arr)

    for i := range arr {
        println(i, &arr[i])
    }
}

// Output:
// arr 0x1400005072b
// 0 0x1400005072b
// 1 0x1400005072c
// 2 0x1400005072d
// 3 0x1400005072e
// 4 0x1400005072f

هناك بعض الأشياء التي يجب ملاحظتها هنا:

  • عنوان المصفوفة هو نفس عنوان العنصر الأول.
  • عنوان كل عنصر هو بايت واحد عن بعضها البعض لأن نوع العنصر لدينا هو بايت.

How Go Arrays Work and Get Tricky with For-Range

مصفوفة [5]بايت{0، 1، 2، 3، 4} في الذاكرة

أنظر إلى الصورة بعناية.

مجموعتنا تنمو من عنوان أعلى إلى عنوان أقل، أليس كذلك؟ توضح هذه الصورة بالضبط كيف تبدو المصفوفة في المكدس، من arr[4] إلى arr[0].

هل هذا يعني أنه يمكننا الوصول إلى أي عنصر في المصفوفة من خلال معرفة عنوان العنصر الأول (أو المصفوفة) وحجم العنصر؟ لنجرب ذلك باستخدام مصفوفة int وحزمة غير آمنة:

func main() {
    a := [3]int{99, 100, 101}

    p := unsafe.Pointer(&a[0])

    a1 := unsafe.Pointer(uintptr(p)   8)
    a2 := unsafe.Pointer(uintptr(p)   16)

    fmt.Println(*(*int)(p))
    fmt.Println(*(*int)(a1))
    fmt.Println(*(*int)(a2))
}

// Output:
// 99
// 100
// 101

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

How Go Arrays Work and Get Tricky with For-Range

المصفوفة [3]int{99, 100, 101} في الذاكرة

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

الآن، المصفوفة من النوع T ليست نوعًا بحد ذاتها، ولكن المصفوفة ذات بحجم ونوع معين T، تعتبر نوعًا. هذا ما أعنيه:

func main() {
    a := [5]byte{}
    b := [4]byte{}

    fmt.Printf("%T\n", a) // [5]uint8
    fmt.Printf("%T\n", b) // [4]uint8

    // cannot use b (variable of type [4]byte) as [5]byte value in assignment
    a = b 
}

على الرغم من أن كلاً من a وb عبارة عن مصفوفتين من البايتات، إلا أن مترجم Go يراها كأنواع مختلفة تمامًا، إلا أن تنسيق %T يجعل هذه النقطة واضحة.

إليك كيف يرى مترجم Go ذلك داخليًا (src/cmd/compile/internal/types2/array.go):

// An Array represents an array type.
type Array struct {
    len  int64
    elem Type
}

// NewArray returns a new array type for the given element type and length.
// A negative length indicates an unknown length.
func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }

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

مصفوفة حرفية

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

var arr1 [10]int // [0 0 0 0 0 0 0 0 0 0]

// With value, infer-length
arr2 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// With index, infer-length
arr3 := [...]int{11: 3} // [0 0 0 0 0 0 0 0 0 0 0 3]

// Combined index and value
arr4 := [5]int{1, 4: 5} // [1 0 0 0 5]
arr5 := [5]int{2: 3, 4, 4: 5} // [0 0 3 4 5]

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

الآن، إليك شيء مثير للاهتمام: عندما نقوم بإنشاء مصفوفة تحتوي على أقل من 4 عناصر، يقوم Go بإنشاء تعليمات لوضع القيم في المصفوفة واحدة تلو الأخرى.

لذا عندما نفعل arr := [3]int{1, 2, 3, 4}، ما يحدث بالفعل هو:

arr := [4]int{}
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4

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

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

"ماذا عن المصفوفات التي تحتوي على أكثر من 4 عناصر؟"

يقوم المترجم بإنشاء تمثيل ثابت للمصفوفة في الملف الثنائي، وهو ما يُعرف باسم استراتيجية "التهيئة الثابتة".

وهذا يعني أنه يتم تخزين قيم عناصر المصفوفة في قسم للقراءة فقط من الملف الثنائي. يتم إنشاء هذه البيانات الثابتة في وقت الترجمة، بحيث يتم تضمين القيم مباشرة في الملف الثنائي. إذا كنت مهتمًا بكيفية ظهور [5]int{1,2,3,4,5} في تجميع Go:

main..stmp_1 SRODATA static size=40
    0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00  ................
    0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ................
    0x0020 05 00 00 00 00 00 00 00                          ........

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

يتم تخزين بياناتنا في stmp_1، وهي بيانات ثابتة للقراءة فقط بحجم 40 بايت (8 بايت لكل عنصر)، ويتم ترميز عنوان هذه البيانات في الملف الثنائي.

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

const readonly = [5]int{1, 2, 3, 4, 5}

arr := readonly

"ماذا عن المصفوفة التي تحتوي على 5 عناصر ولكن تمت تهيئة 3 منها فقط؟"

سؤال جيد، هذا الحرفي [5]int{1,2,3} يندرج ضمن الفئة الأولى، حيث يضع Go القيمة في المصفوفة واحدًا تلو الآخر.

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

ولكن قد تتساءل عن حجم الحجم "الكبير جدًا".

اعتبارًا من Go 1.23، إذا كان حجم المتغير، وليس المصفوفة فقط، يتجاوز القيمة الثابتة MaxStackVarSize، والتي تبلغ حاليًا 10 ميغابايت، فسيتم اعتباره كبيرًا جدًا بحيث لا يمكن تخصيص المكدس وسيتم الهروب إلى الكومة.

func main() {
    a := [10 * 1024 * 1024]byte{}
    println(&a)

    b := [10*1024*1024   1]byte{}
    println(&b)
}

في هذا السيناريو، سينتقل b إلى الكومة بينما لن ينتقل.

عمليات المصفوفة

يتم ترميز طول المصفوفة في النوع نفسه. على الرغم من أن المصفوفات لا تحتوي على خاصية cap، فلا يزال بإمكاننا الحصول عليها:

func main() {
    a := [5]int{1, 2, 3}
    println(len(a)) // 5
    println(cap(a)) // 5
}

السعة تساوي الطول بلا شك، ولكن الشيء الأكثر أهمية هو أننا نعرف ذلك في وقت الترجمة، أليس كذلك؟

لذا فإن len(a) لا معنى له بالنسبة للمترجم لأنه ليس خاصية وقت التشغيل، يعرف مترجم Go القيمة في وقت الترجمة.

...

هذا مقتطف من التدوينة؛ المنشور الكامل متاح هنا: كيف تعمل المصفوفات وتصبح صعبة مع For-Range.

بيان الافراج تم إعادة نشر هذه المقالة على: https://dev.to/func25/how-go-arrays-work-and-get-tricky-with-for-range-3i9i?1 إذا كان هناك أي انتهاك، يرجى الاتصال بـ Study_golang@163 .com لحذفه
أحدث البرنامج التعليمي أكثر>

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

Copyright© 2022 湘ICP备2022001581号-3