أنواع مرجعية .NET مقابل أنواع القيمة. الجزء 2


نوع الكائن الأساسي وتنفيذ الواجهات. الملاكمة


يبدو أننا وصلنا إلى الجحيم والماء العالي ويمكننا تظليل أي مقابلة ، حتى تلك المقابلة لفريق .NET CLR. ومع ذلك ، دعونا لا نتسرع في microsoft.com والبحث عن الوظائف الشاغرة. الآن ، نحن بحاجة إلى فهم كيف ترث أنواع القيم كائنًا إذا لم تتضمن أي إشارة إلى SyncBlockIndex ، وليس مؤشرًا إلى جدول الأساليب الافتراضية. سيوضح هذا تمامًا نظام أنواعنا وستجد جميع أجزاء الألغاز أماكنها. ومع ذلك ، سنحتاج إلى أكثر من جملة واحدة.


الآن ، دعونا نتذكر مرة أخرى كيف يتم تخصيص أنواع القيم في الذاكرة. يحصلون على مكان في الذاكرة في مكانهم الصحيح. تحصل أنواع المراجع على تخصيص على كومة الكائنات الصغيرة والكبيرة. هم دائما إعطاء إشارة إلى المكان على الكومة حيث الكائن. يحتوي كل نوع قيمة على أساليب مثل ToString و Equals و GetHashCode. إنها ظاهرية ويمكن تجاوزها ، ولكن لا تسمح بتوارث نوع قيمة عبر تجاوز الأساليب. إذا استخدمت أنواع القيم أساليب قابلة للاستبدال ، فستحتاج إلى جدول أساليب ظاهرية لتوجيه المكالمات. هذا من شأنه أن يؤدي إلى مشاكل نقل الهياكل إلى عالم غير مُدار: ستذهب الحقول الإضافية إلى هناك. نتيجة لذلك ، هناك أوصاف لأساليب أنواع القيمة في مكان ما ، لكن لا يمكنك الوصول إليها مباشرة عبر جدول الأساليب الافتراضية.


هذا قد يجلب فكرة أن قلة الميراث مصطنعة


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

وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع github / sidristij / dotnetbook .

هذا قد يجلب فكرة أن قلة الميراث مصطنعة:


  • هناك ميراث من كائن ، ولكن ليس مباشرة ؛
  • هناك ToString ، يساوي و GetHashCode داخل نوع أساسي. في أنواع القيم هذه الطرق لها سلوكها الخاص. هذا يعني أنه تم تجاوز الأساليب فيما يتعلق object ؛
  • علاوة على ذلك ، إذا قمت بإلقاء نوع على object ، فلديك الحق الكامل في الاتصال بـ ToString و Equals و GetHashCode ؛
  • عند استدعاء طريقة مثيل لنوع قيمة ، تحصل الطريقة على بنية أخرى هي نسخة من نسخة أصلية. هذا يعني أن استدعاء أسلوب مثيل يشبه استدعاء أسلوب ثابت: Method(ref structInstance, newInternalFieldValue) . في الواقع ، هذه الدعوة تتجاوز this ، مع استثناء واحد ، ولكن. يجب على JIT أن تقوم بتجميع نص أحد الأساليب ، لذلك لن يكون من الضروري إزاحة حقول البنية ، والقفز فوق المؤشر إلى جدول الأساليب الافتراضية ، والذي لا يوجد في البنية. إنه موجود لأنواع القيمة في مكان آخر .

تختلف الأنواع في السلوك ، لكن هذا الاختلاف ليس كبيرًا على مستوى التنفيذ في CLR. سوف نتحدث عن ذلك في وقت لاحق قليلا.


دعنا نكتب السطر التالي في برنامجنا:


 var obj = (object)10; 

سيسمح لنا بالتعامل مع الرقم 10 باستخدام فئة أساسية. وهذا ما يسمى الملاكمة. هذا يعني أن لدينا VMT لاستدعاء الأساليب الافتراضية مثل ToString () ، Equals و GetHashCode. في الواقع ، تقوم الملاكمة بإنشاء نسخة من نوع القيمة ، ولكن ليس مؤشرًا إلى الأصل. هذا لأنه يمكننا تخزين القيمة الأصلية في كل مكان: على المكدس أو كحقل لفصل دراسي. إذا وضعناها على نوع كائن ، فيمكننا تخزين مرجع لهذه القيمة طالما نريد. عندما يحدث الملاكمة:


  • يخصص CLR مساحة على الكومة لبنية + SyncBlockIndex + VMT لنوع القيمة (للاتصال بـ ToString و GetHashCode و Equals) ؛
  • نسخ مثيل نوع القيمة هناك.

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


انظر فقط إلى ما يحدث إذا كنت تستخدم بنية تنفذ واجهة باستخدام نفس الواجهة.


 struct Foo : IBoo { int x; void Boo() { x = 666; } } IBoo boo = new Foo(); boo.Boo(); 

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


 IBoo boo = (IBoo)(box_to_object)new Foo(); boo.Boo(); 

كتابة هذا الرمز غير فعال. سيكون عليك تغيير نسخة بدلاً من نسخة أصلية:


 void Main() { var foo = new Foo(); foo.a = 1; Console.WriteLite(foo.a); // -> 1 IBoo boo = foo; boo.Boo(); // looks like changing foo.a to 10 Console.WriteLite(foo.a); // -> 1 } struct Foo: IBoo { public int a; public void Boo() { a = 10; } } interface IBoo { void Boo(); } 

في المرة الأولى التي ننظر فيها إلى الكود ، لا يتعين علينا معرفة ما نتعامل معه في الكود بخلاف الكود الخاص بنا ورؤية واجهة مستخدمين لـ IBoo. هذا يجعلنا نعتقد أن فو هي طبقة وليست بنية. ثم لا يوجد تقسيم بصري في الهياكل والطبقات ، مما يجعلنا نفكر في
يجب أن تحصل نتائج تعديل الواجهة على foo ، وهو ما لا يحدث لأن boo هي نسخة من foo. هذا مضلل. في رأيي ، يجب أن يحصل هذا الرمز على تعليقات ، حتى يتمكن المطورون الآخرون من التعامل معه.


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


يمكنك الاعتراض على أنه إذا كان هذا صحيحًا ، فسيبدو كما يلي:


 var referenceToInteger = (IInt32)10; 

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


 public sealed class Boxed<T> { public T Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(object obj) { return Value.Equals(obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() { return Value.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { return Value.GetHashCode(); } } 

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


 var typedBoxing = new Boxed<int> { Value = 10 }; var pureBoxing = (object)10; 

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


بدلاً من ذلك ، يمكننا استدعاء بعض الأساليب لقيمة محاصر لدينا.


 struct Foo { public int x; public void ChangeTo(int newx) { x = newx; } } var boxed = new Boxed<Foo> { Value = new Foo { x = 5 } }; boxed.Value.ChangeTo(10); var unboxed = boxed.Value; 

لدينا أداة جديدة. دعونا نفكر فيما يمكننا القيام به.


  • يعمل نوع Boxed<T> على نفس النوع المعتاد: يخصص الذاكرة على الكومة ، ويمرر القيمة ويسمح بالحصول عليها ، من خلال القيام بنوع من unbox.
  • إذا فقدت إشارة إلى هيكل محاصر ، فسيجمعه GC ؛
  • ومع ذلك ، يمكننا الآن العمل مع نوع محاصر ، أي استدعاء أساليبها ؛
  • أيضًا ، يمكننا استبدال مثيل لنوع القيمة في SOH / LOH لأحد الأنواع الأخرى. لم نتمكن من القيام بذلك من قبل ، حيث سيتعين علينا القيام بإلغاء تثبيت علبته وتغيير هيكله إلى هيكل آخر وإعادة الملاكمة ، مع إعطاء إشارة جديدة للعملاء.

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


 var pool = new Pool<Boxed<Foo>>(maxCount:1000); var boxed = pool.Box(10); boxed.Value=70; // use boxed value here pool.Free(boxed); 

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


لنختتم:


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

في النهاية ، دعونا ننظر إلى رمز غير عملي تمامًا:


 static unsafe void Main() { // here we create boxed int object boxed = 10; // here we get the address of a pointer to a VMT var address = (void**)EntityPtr.ToPointerWithOffset(boxed); unsafe { // here we get a Virtual Methods Table address var structVmt = typeof(SimpleIntHolder).TypeHandle.Value.ToPointer(); // change the VMT address of the integer passed to Heap into a VMT SimpleIntHolder, turning Int into a structure *address = structVmt; } var structure = (IGetterByInterface)boxed; Console.WriteLine(structure.GetByInterface()); } interface IGetterByInterface { int GetByInterface(); } struct SimpleIntHolder : IGetterByInterface { public int value; int IGetterByInterface.GetByInterface() { return value; } } 

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


  1. هل الملاكمة لعدد صحيح.
  2. الحصول على عنوان كائن تم الحصول عليها (عنوان Int32 VMT)
  3. الحصول على VMT من SimpleIntHolder
  4. استبدال VMT عدد صحيح محاصر إلى VMT من بنية.
  5. جعل unboxing في نوع الهيكل
  6. عرض قيمة الحقل على الشاشة ، والحصول على Int32 ، كان
    محاصر.

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


لاغية \ <T>


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


 int? x = 5; int? y = null; var boxedX = (object)x; // -> 5 var boxedY = (object)y; // -> null 

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


 int? x = null; var pseudoBoxed = (object)x; double? y = (double?)pseudoBoxed; 

يعمل الرمز لمجرد أنه يمكنك إلقاء نوع على أي شيء تريده
مع فارغة.


الذهاب أعمق في الملاكمة


أخيرًا ، أود إخبارك بنوع System.Enum . من المنطقي أن يكون هذا نوع قيمة لأنه تعداد معتاد: تسمية الأرقام بالأسماء بلغة برمجة. ومع ذلك ، System.Enum هو نوع المرجع. يتم توارث كافة أنواع بيانات التعداد ، المعرفة في مجالك وكذلك في .NET Framework من System.Enum. إنه نوع بيانات الفصل. علاوة على ذلك ، إنها فئة مجردة ، موروثة من System.ValueType .


  [Serializable] [System.Runtime.InteropServices.ComVisible(true)] public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible { // ... } 

هل يعني ذلك أنه يتم تخصيص جميع التعدادات على SOH وعندما نستخدمها ، فإننا نزيد من كومة الذاكرة المؤقتة و GC؟ في الواقع لا ، كما نستخدمها فقط. بعد ذلك ، نفترض أن هناك مجموعة من التعدادات في مكان ما ونحصل على حالاتهم فقط. لا ، مرة أخرى. يمكنك استخدام التعدادات في الهياكل أثناء التنظيم. التعدادات هي الأرقام المعتادة.


الحقيقة هي أن CLR تقوم باختراق بنية نوع البيانات عند تكوينها إذا كان هناك تعداد يحول فئة إلى نوع قيمة :


 // Check to see if the class is a valuetype; but we don't want to mark System.Enum // as a ValueType. To accomplish this, the check takes advantage of the fact // that System.ValueType and System.Enum are loaded one immediately after the // other in that order, and so if the parent MethodTable is System.ValueType and // the System.Enum MethodTable is unset, then we must be building System.Enum and // so we don't mark it as a ValueType. if(HasParent() && ((g_pEnumClass != NULL && GetParentMethodTable() == g_pValueTypeClass) || GetParentMethodTable() == g_pEnumClass)) { bmtProp->fIsValueClass = true; HRESULT hr = GetMDImport()->GetCustomAttributeByName(bmtInternal->pType->GetTypeDefToken(), g_CompilerServicesUnsafeValueTypeAttribute, NULL, NULL); IfFailThrow(hr); if (hr == S_OK) { SetUnsafeValueClass(); } } 

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


ماذا لو كنت تريد أن ترى الملاكمة شخصيًا؟


لحسن الحظ ، ليس عليك استخدام أداة فك الشفرة والدخول إلى غابة الكود. لدينا نصوص .NET الأساسية بالكامل والعديد منها متطابق من حيث .NET Framework CLR و CoreCLR. يمكنك النقر على الروابط أدناه ورؤية تنفيذ الملاكمة على الفور:



هنا ، يتم استخدام الطريقة الوحيدة لفك العلبة:
JIT_Unbox (..) ، وهو عبارة عن مجمّع حول JIT_Unbox_Helper (..) .


أيضًا ، من المثير للاهتمام أنه ( https://stackoverflow.com/questions/3743762/unboxing-does-not-create-a-copy-of-the-value-is-this-right ) ، فإن إلغاء التخزين لا يعني النسخ البيانات إلى الكومة. الملاكمة تعني تمرير مؤشر إلى الكومة أثناء اختبار توافق الأنواع. سيحدد شفرة شفرة IL التي تلي إلغاء تحديد الإجراءات الإجراءات بهذا العنوان. قد يتم نسخ البيانات إلى متغير محلي أو مكدس لاستدعاء طريقة. خلاف ذلك ، سيكون لدينا نسخة مزدوجة. أولاً عند النسخ من الكومة إلى مكان ما ، ثم النسخ إلى المكان المقصود.


أسئلة


لماذا لا يمكن لـ .NET CLR القيام بتجميع ألعاب الملاكمة؟


إذا تحدثنا إلى أي مطور جافا ، فسوف نعرف شيئين:


  • جميع أنواع القيم في Java محاصر ، مما يعني أنها ليست أنواع قيمة بشكل أساسي. الأعداد الصحيحة هي أيضا محاصر.
  • لسبب التحسين يتم أخذ جميع الأعداد الصحيحة من -128 إلى 127 من مجموعة الكائنات.

فلماذا لا يحدث هذا في .NET CLR أثناء الملاكمة؟ انها بسيطة. نظرًا لأنه يمكننا تغيير محتوى نوع قيمة محاصر ، يمكننا القيام بما يلي:


 object x = 1; x.GetType().GetField("m_value", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(x, 138); Console.WriteLine(x); // -> 138 

أو مثل هذا (C ++ / CLI):


 void ChangeValue(Object^ obj) { Int32^ i = (Int32^)obj; *i = 138; } 

إذا تعاملنا مع التجميع ، فسنقوم بتغيير كل الطلبات في التطبيق إلى 138 ، وهذا ليس جيدًا.


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


لماذا لا يمكن عمل الملاكمة على المكدس بدلاً من الكومة ، عند استدعاء طريقة تأخذ نوع كائن ، وهو نوع قيمة في الواقع؟


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


لماذا لا يمكن استخدام نوع القيمة كحقل؟


في بعض الأحيان نريد استخدام الهيكل كحقل لهيكل آخر يستخدم الأول. أو أبسط: استخدام الهيكل كحقل هيكل. لا تسألني لماذا هذا يمكن أن يكون مفيدا. لا يمكن. إذا كنت تستخدم بنية كحقل لها أو من خلال التبعية مع بنية أخرى ، فإنك تنشئ تكرارا ، مما يعني بنية حجم لا حصر له. ومع ذلك ، يحتوي .NET Framework على بعض الأماكن حيث يمكنك القيام بذلك. مثال على ذلك System.Char ، والذي يحتوي على نفسه :


 public struct Char : IComparable, IConvertible { // Member Variables internal char m_value; //... } 

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


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

وأيضًا ، إذا كنت تريد أن تقول "شكرًا" ، فإن أفضل طريقة يمكنك اختيارها هي منحنا نجمًا على github أو مستودع forking https://github.com/sidristij/dotnetbook

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


All Articles