قوانين التأمل في الذهاب

مرحبا يا هبر! أقدم لكم ترجمة مقالة "قوانين التأمل" من مبتكر اللغة.

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

أنواع واجهات


نظرًا لأن التفكير يعتمد على نظام النوع ، فلنقم بتحديث معرفتنا بأنواع في Go.
يتم كتابة Go بشكل ثابت. لكل متغير نوع ثابت واحد فقط ثابت في وقت الترجمة: int, float32, *MyType, []byte ... إذا أعلنا:

 type MyInt int var i int var j MyInt 

ثم i من نوع int و j من نوع MyInt . يحتوي المتغيران i و j على أنواع ثابتة مختلفة ، وعلى الرغم من أنهما لهما النوع الأساسي نفسه ، فلا يمكن تعيينهما لبعضهما البعض دون تحويل.

واحدة من فئات الأنواع المهمة هي الواجهات ، وهي مجموعات ثابتة من الأساليب. يمكن للواجهة تخزين أي قيمة محددة (غير واجهة) طالما أن هذه القيمة تطبق طرق الواجهة. زوج من الأمثلة المعروفة هو io.Reader و io.Writer ، أنواع القارئ والكاتب من حزمة io :

 // Reader -  ,    Read(). type Reader interface { Read(p []byte) (n int, err error) } // Writer -  ,    Write(). type Writer interface { Write(p []byte) (n int, err error) } 

يقال أن أي نوع ينفذ طريقة Read() أو Write() مع هذا التوقيع ينفذ io.Reader أو io.Writer على التوالي. هذا يعني أن متغير من النوع io.Reader يمكن أن يحتوي على أي قيمة من النوع Read ():

 var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) 

من المهم أن نفهم أنه يمكن تعيين r لأي قيمة تنفذ io.Reader . Go مكتوب بشكل ثابت ، والنوع r هو io.Reader .

مثال مهم للغاية لنوع الواجهة هو الواجهة الفارغة:

 interface{} 

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

يجب أن يكون هذا مفهومًا - يرتبط التفكير والواجهات ارتباطًا وثيقًا.

التمثيل الداخلي للواجهة


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

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

 var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty 

r يحتوي بشكل تخطيطي على زوج (, ) --> (tty, *os.File) . لاحظ أن نوع *os.File يطبق أساليب أخرى غير Read() ؛ حتى إذا كانت قيمة الواجهة توفر الوصول فقط إلى أسلوب Read () ، فإن القيمة الموجودة في الداخل تحمل جميع المعلومات حول نوع هذه القيمة. هذا هو السبب في أننا يمكن أن نفعل مثل هذه الأشياء:

 var w io.Writer w = r.(io.Writer) 

التعبير في هذه المهمة هو عبارة نوع؛ تدعي أن العنصر داخل r يطبق أيضًا io.Writer ، وبالتالي يمكننا تعيينه إلى w . بمجرد (tty, *os.File) w على زوج (tty, *os.File) . (tty, *os.File) . هذا هو نفس الزوج كما في r . يحدد النوع الثابت للواجهة أي الطرق التي يمكن استدعاؤها في متغير الواجهة ، على الرغم من أن مجموعة واسعة من الأساليب يمكن أن يكون لها قيمة محددة بالداخل.

استمرارًا ، يمكننا القيام بما يلي:

 var empty interface{} empty = w 

وستحتوي القيمة الفارغة للحقل الفارغ مرة أخرى على نفس الزوج (tty, *os.File) . هذا ملائم: يمكن أن تحتوي الواجهة الفارغة على أي قيمة وجميع المعلومات التي نحتاجها من أي وقت مضى.

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

أحد التفاصيل المهمة هو أن الزوج داخل الواجهة يحتوي دائمًا على نموذج (قيمة ، نوع محدد) ولا يمكن أن يكون له نموذج (قيمة ، واجهة). الواجهات لا تدعم الواجهات كقيم.

الآن نحن على استعداد لدراسة التفكير.

أول قانون انعكاس


  • يمتد الانعكاس من الواجهة إلى انعكاس الكائن.

على المستوى الأساسي ، يعتبر الانعكاس مجرد آلية لفحص زوج من النوع والقيمة المخزنة داخل متغير الواجهة. للبدء ، هناك نوعان نحتاج إلى reflect.Type : reflect.Type ، reflect.Value ، reflect.Value . يوفر هذان النوعان الوصول إلى محتويات متغير الواجهة ويتم إرجاعهما بواسطة الوظائف البسيطة ، وعاكس. TypeOf () و reflect.ValueOf () ، على التوالي. يستخرجون أجزاء من معنى الواجهة. (بالإضافة إلى ذلك ، reflect.Value السهل الحصول على reflect.Type ، reflect.Type ، ولكن دعونا لا نخلط بين مفهومي Value Type في الوقت الحالي.)

لنبدأ بـ TypeOf() :

 package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) } 

سيتم إخراج البرنامج
type: float64

يشبه البرنامج تمرير متغير بسيط float64 x reflect.TypeOf() . reflect.TypeOf() . هل ترى الواجهة؟ وهو - reflect.TypeOf() يقبل واجهة فارغة ، وفقًا لإعلان الوظيفة:

 // TypeOf()  reflect.Type    . func TypeOf(i interface{}) Type 

عند استدعاء reflect.TypeOf(x) ، x تخزين x أولاً في واجهة فارغة ، والتي يتم تمريرها بعد ذلك كوسيطة ؛ reflect.TypeOf() يفرغ هذه الواجهة الفارغة لاستعادة معلومات النوع.

تعمل وظيفة reflect.ValueOf() ، بالطبع ، على استعادة القيمة (فيما يلي سوف نتجاهل القالب ونركز على الكود):

 var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String()) 

ستطبع
value: <float64 Value>
(نطلق على طريقة String() صراحة لأنه ، بشكل افتراضي ، يتم reflect.Value الحزمة fmt reflect.Value القيمة reflect.Value لقيمة معينة.)
كلاهما reflect.Type reflect.Value لها العديد من الطرق ، مما يسمح لك باستكشافها وتعديلها. أحد الأمثلة المهمة هو أن reflect.Value لديها طريقة Type() التي تُرجع نوع القيمة. reflect.Type و reflect.Type و reflect.Value لها طريقة Kind() التي ترجع ثابتًا يشير إلى العنصر البدائي الذي يتم تخزينه: Uint, Float64, Slice ... يتم الإعلان عن هذه الثوابت في التعداد في حزمة الانعكاس. تسمح لنا طرق Value ذات الأسماء مثل Int() و Float() بسحب القيم (مثل int64 و float64) المضمنة في الداخل:

 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float()) 

ستطبع

 type: float64 kind is float64: true value: 3.4 

هناك أيضًا طرق مثل SetInt() و SetFloat() ، ولكن لاستخدامها نحتاج إلى فهم الاستقرار ، وهو موضوع القانون الثالث للتفكير.

تحتوي المكتبة المنعكسة على اثنين من الخصائص التي تحتاج إلى إبرازها. أولاً ، للحفاظ على واجهة برمجة التطبيقات بسيطة ، تعمل int64 القيمة "getter" و "setter" على النوع الأكبر الذي يمكن أن يحتوي على قيمة: int64 لجميع الأعداد الصحيحة int64 . أي ، الأسلوب Int() Value القيمة بإرجاع int64 ، SetInt() تأخذ int64 ؛ قد يتطلب التحويل إلى النوع الفعلي:

 var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) x = uint8(v.Uint()) // v.Uint  uint64. 

سيكون

 type: uint8 kind is uint8: true 

هنا v.Uint() uint64 ، هناك حاجة إلى بيان نوع صريح.

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

 type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) // v   Value. 

v.Kind() == reflect.Int ، على الرغم من أن النوع الثابت من x هو MyInt ، وليس int . بمعنى آخر ، لا يمكن لـ Kind() التمييز بين int MyInt ، MyInt Type() . Kind يمكنه فقط قبول قيم الأنواع المضمنة.

القانون الثاني للتفكير


  • يمتد الانعكاس من الكائن العاكس إلى الواجهة.

مثل الانعكاس المادي ، ينعكس الانعكاس في Go على عكس ذلك.

بعد أن reflect.Value ، يمكننا استعادة قيمة الواجهة باستخدام طريقة Interface() ؛ تقوم الطريقة بتجميع معلومات النوع والقيمة في الواجهة وإرجاع النتيجة:

 // Interface   v  interface{}. func (v Value) Interface() interface{} 
bvt
كمثال:

 y := v.Interface().(float64) // y   float64. fmt.Println(y) 

يطبع قيمة float64 ممثلة في كائن float64 v .
ومع ذلك ، يمكننا أن نفعل ما هو أفضل. يتم fmt.Println() الوسيطات الموجودة في fmt.Println() و fmt.Printf() كواجهة فارغة ، يتم تفكيكها بواسطة حزمة fmt داخليًا ، كما هو الحال في الأمثلة السابقة. لذلك ، كل ما هو مطلوب لطباعة محتويات reflect.Value بشكل صحيح هي تمرير نتيجة طريقة Interface() إلى وظيفة الإخراج المنسقة:

 fmt.Println(v.Interface()) 

(لماذا لا fmt.Println(v) ؟ لأن v من نوع reflect.Value . reflect.Value ؛ نريد الحصول على القيمة الموجودة في الداخل.) نظرًا لأن float64 هي float64 ، يمكننا حتى استخدام تنسيق النقطة العائمة إذا أردنا:

 fmt.Printf("value is %7.1e\n", v.Interface()) 

ستخرج في حالة معينة
3.4e+00

مرة أخرى ، ليست هناك حاجة v.Interface() نوع النتيجة v.Interface() في float64 ؛ تحتوي قيمة واجهة فارغة على معلومات حول قيمة معينة بالداخل ، fmt.Printf() .
وباختصار ، فإن طريقة Interface() هي معكوس دالة ValueOf() ، باستثناء أن نتيجتها تكون دائمًا interface{} النوع الثابت interface{} .

التكرار: يمتد الانعكاس من قيم الواجهة إلى كائنات الانعكاس والعكس صحيح.

القانون الثالث للتفكير التأملي


  • لتغيير كائن الانعكاس ، يجب أن تكون القيمة قابلة للتعيين.

القانون الثالث هو الأكثر دهاء ومربكا. نبدأ بالمبادئ الأولى.
هذا الرمز لا يعمل ، ولكنه يستحق الانتباه.

 var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) //  

إذا قمت بتشغيل هذا الرمز ، فسوف يتعطل من الذعر برسالة حرجة:
panic: reflect.Value.SetFloat
المشكلة ليست في أن الحرف 7.1 لم 7.1 تناوله ؛ هذا ما v غير قابل للتثبيت. reflect.Value هي خاصية reflect.Value ، reflect.Value ، وليس كل reflect.Value ، reflect.Value لها.
reflect.Value.CanSet() الأسلوب reflect.Value.CanSet() التي يتم تعيينها؛ في حالتنا:

 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet()) 

ستتم طباعة:
settability of v: false

حدث خطأ أثناء استدعاء الأسلوب Set() على قيمة غير مُدارة. ولكن ما هي قابلية التثبيت؟

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

 var x float64 = 3.4 v := reflect.ValueOf(x) 

نقوم بتمرير نسخة من x إلى reflect.ValueOf() ، لذلك يتم إنشاء الواجهة كوسيطة reflect.ValueOf() - هذه نسخة من x ، وليست x نفسها. وهكذا ، إذا كان البيان:

 v.SetFloat(7.1) 

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

هذا لا ينبغي أن يبدو غريبا. هذه حالة شائعة في الملابس غير العادية. ضع في اعتبارك تمرير x إلى دالة:
f(x)

لا نتوقع أن يتمكن f() من تغيير x ، لأننا مررنا نسخة من قيمة x ، وليس x نفسها. إذا أردنا f() يغير f() x مباشرة ، يجب أن نمرر المؤشر إلى x إلى وظيفتنا:
f(&x)

هذا أمر واضح ومألوف ، ويعمل التفكير بشكل مشابه. إذا أردنا تغيير x باستخدام الانعكاس ، فيجب علينا تزويد مكتبة الانعكاس بمؤشر للقيمة التي نريد تغييرها.

فلنفعل ذلك. أولاً ، نقوم بتهيئة x كالمعتاد ، ثم ننشئ reflect.Value p التي تشير إليها.

 var x float64 = 3.4 p := reflect.ValueOf(&x) //   x. fmt.Println("type of p:", p.Type()) fmt.Println("settability of p:", p.CanSet()) 

سيخرج
type of p: *float64
settability of p: false


لا يمكن تعيين كائن الانعكاس p ، ولكنه ليس p الذي نريد تعيينه ، بل هو المؤشر *p . للحصول على ما يشير إلى p ، نطلق على أسلوب Value.Elem() ، الذي يأخذ القيمة بشكل غير مباشر من خلال المؤشر ، ويخزن النتيجة في reflect.Value v :

 v := p.Elem() fmt.Println("settability of v:", v.CanSet()) 

الآن v هو كائن قابل للتثبيت ؛
settability of v: true
وبما أنها تمثل x ، يمكننا في النهاية استخدام v.SetFloat() لتغيير قيمة x :

 v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x) 

الاستنتاج كما هو متوقع
7.1
7.1

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

الهياكل


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

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

 type T struct { A int B string } t := T{23, "skidoo"} s := reflect.ValueOf(&t).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) } 

سيتم إخراج البرنامج
0: A int = 23
1: B string = skidoo

يتم عرض نقطة أخرى حول قابلية التثبيت هنا: أسماء الحقول T في الحالة العليا (المصدرة) ، لأن الحقول المصدرة فقط قابلة للتعيين.
بما أن s تحتوي على كائن انعكاس قابل للتثبيت ، يمكننا تغيير حقل البنية.

 s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t) 

النتيجة:
t is now {77 Sunset Strip}
إذا قمنا بتغيير البرنامج بحيث s إنشاء s من t بدلاً من &t ، فإن المكالمات إلى SetInt() و SetString() ستنتهي في حالة من الذعر ، لأن الحقول t لن تكون قابلة للتعيين.

الخلاصة


تذكر قوانين الانعكاس:

  • يمتد الانعكاس من الواجهة إلى انعكاس الكائن.
  • يمتد الانعكاس من انعكاس كائن إلى الواجهة.
  • لتغيير كائن الانعكاس ، يجب تعيين القيمة.

نشر من قبل روب بايك .

Source: https://habr.com/ru/post/ar415171/


All Articles