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

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


لنناقش الاختلافات الحقيقية:


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

ومع ذلك ، هناك ميزات مشتركة:


  • يمكن لكلتا الفئتين الفرعيتين ترث نوع الكائن وتصبح ممثلين له.

دعونا ننظر عن كثب في كل ميزة.


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

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


دعونا ننظر عن كثب في كل ميزة.


نسخ


الفرق الرئيسي بين النوعين هو كما يلي:


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

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


DateTime dt = DateTime.Now; // Here, we allocate space for DateTime variable when calling a method, // but it will contain zeros. Next, let's copy all // values of the Now property to dt variable DateTime dt2 = dt; // Here, we copy the value once again object obj = new object(); // Here, we create an object by allocating memory on the Small Object Heap, // and put a pointer to the object in obj variable object obj2 = obj; // Here, we copy a reference to this object. Finally, // we have one object and two references. 

يبدو أن هذه الخاصية تنتج تصميمات غامضة مثل
تغيير الكود في المجموعات:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create an array of such structures and initialize the Data field = 5 var array = new [] { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field array[0].Data = 4; // Let's check the value Console.WriteLine(array[0].Data); 

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


دعونا نرى ما سيحدث إذا قمنا بتغيير هذا الرمز:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field list[0].Data = 4; // Let's check the value Console.WriteLine(list[0].Data); 

ستفشل عملية تجميع هذا الرمز ، لأنه عندما تكتب list[0].Data = 4 تحصل على نسخة من الهيكل أولاً. في الواقع ، تقوم باستدعاء أسلوب مثيل من نوع List<T> الذي يقوم عليه الوصول بواسطة فهرس. يستغرق نسخة من بنية من صفيف داخلي ( List<T> بتخزين البيانات في صفائف) وإرجاع هذه النسخة إليك من طريقة الوصول باستخدام فهرس. بعد ذلك ، تحاول تعديل النسخة ، والتي لا يتم استخدامها بشكل أكبر. هذا الرمز لا طائل منه. يحظر المترجم مثل هذا السلوك ، مع العلم أن الناس يسيئون استخدام أنواع القيم. يجب أن نعيد كتابة هذا المثال بالطريقة التالية:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field. Then, let's save it again. var copy = list[0]; copy.Data = 4; list[0] = copy; // Let's check the value Console.WriteLine(list[0].Data); 

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


يوضح المثال التالي ما أعنيه بـ "قيمة البنية هي
الهيكل بأكمله "


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } int x = 5; PersonInfo person; int y = 6; // Variant 2 int x = 5; int Height; int Width; int HairColor; int y = 6; 

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


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } class Employee { public int x; public PersonInfo person; public int y; } // Variant 2 class Employee { public int x; public int Height; public int Width; public int HairColor; public int y; } 

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


بالطبع ، ليست هذه هي حالة أنواع المراجع. المثيل نفسه موجود على كومة كائن صغير غير قابلة للوصول (SOH) أو كومة كائن كبير (LOH). يحتوي حقل فئة فقط على قيمة مؤشر إلى مثيل: رقم 32 أو 64 بت.


لنلقِ نظرة على المثال الأخير لإغلاق المشكلة.


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } void Method(int x, PersonInfo person, int y); // Variant 2 void Method(int x, int HairColor, int Width, int Height, int y); 

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


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


طرق الغالبة والميراث


الفرق الكبير التالي بين النوعين هو عدم وجود افتراضية
جدول الطرق في الهياكل. هذا يعني أن:


  1. لا يمكنك وصف وتجاوز الأساليب الافتراضية في الهياكل.
  2. هيكل لا يمكن أن يرث واحد آخر. الطريقة الوحيدة لمحاكاة الميراث هي وضع بنية نوع أساسي في الحقل الأول. ستتبع حقول البنية "الموروثة" الحقول الخاصة بالهيكل "الأساسي" وستخلق الميراث المنطقي. سوف تتزامن حقول كلا الهيكلين بناءً على الإزاحة.
  3. يمكنك تمرير بنيات إلى رمز غير مدار. ومع ذلك ، سوف تفقد المعلومات حول الأساليب. هذا لأن البنية هي مجرد مساحة في الذاكرة ، مملوءة بالبيانات دون معلومات حول نوع. يمكنك تمريرها إلى أساليب غير مُدارة ، على سبيل المثال ، مكتوبة في C ++ ، دون تغييرات.

إن الافتقار إلى جدول الأساليب الافتراضية يطرح جزءًا معينًا من الميراث "السحري" من الهياكل ولكنه يمنحهم مزايا أخرى. الأول هو أنه يمكننا نقل مثيلات مثل هذا الهيكل إلى بيئات خارجية (خارج .NET Framework). تذكر ، هذه مجرد ذاكرة
مجموعة! يمكننا أيضًا أن نأخذ نطاقًا من الذاكرة من كود غير مُدار ونُقل نوعًا إلى هيكلنا لجعل حقوله أكثر سهولة. لا يمكنك القيام بذلك باستخدام الفصول الدراسية حيث يوجد حقلان لا يمكن الوصول إليهما. هذه هي SyncBlockIndex وعنوان جدول أساليب افتراضية. إذا مر هذان الحقلان برمز غير مدار ، فسيكون ذلك خطيرًا. باستخدام جدول الأساليب الافتراضية ، يمكن للمرء الوصول إلى أي نوع وتغييره لمهاجمة تطبيق ما.


دعنا نظهر أنه مجرد نطاق ذاكرة دون منطق إضافي.


 unsafe void Main() { int secret = 666; HeightHolder hh; hh.Height = 5; WidthHolder wh; unsafe { // This cast wouldn't work if structures had the information about a type. // The CLR would check a hierarchy before casting a type and if it didn't find WidthHolder, // it would output an InvalidCastException exception. But since a structure is a memory range, // you can interpret it as any kind of structure. wh = *(WidthHolder*)&hh; } Console.WriteLine("Width: " + wh.Width); Console.WriteLine("Secret:" + wh.Secret); } struct WidthHolder { public int Width; public int Secret; } struct HeightHolder { public int Height; } 

هنا ، نقوم بتنفيذ العملية المستحيلة في الكتابة القوية. نحن نلقي نوعًا واحدًا على نوع آخر غير متوافق يحتوي على حقل إضافي واحد. نقدم متغير إضافي داخل الطريقة الرئيسية. من الناحية النظرية ، فإن قيمتها سرية. ومع ذلك ، فإن رمز المثال سوف يخرج قيمة المتغير ، غير موجود في أي من الهياكل داخل الأسلوب Main() . قد تعتبره خرقًا للأمان ، لكن الأمور ليست بهذه البساطة. لا يمكنك التخلص من التعليمات البرمجية غير المدارة في البرنامج. السبب الرئيسي هو بنية مكدس مؤشر الترابط. يمكن للمرء استخدامها للوصول إلى التعليمات البرمجية غير المدارة واللعب مع المتغيرات المحلية. يمكنك الدفاع عن التعليمات البرمجية الخاصة بك من هذه الهجمات عن طريق عشوائية حجم إطار مكدس. أو يمكنك حذف المعلومات حول سجل EBP لتعقيد عودة إطار مكدس. ومع ذلك ، هذا لا يهمنا الآن. ما يهمنا في هذا المثال هو ما يلي. يذهب المتغير "السري" قبل تعريف المتغير hh وبعده في بنية WidthHolder (في أماكن مختلفة ، فعليًا). إذن لماذا حصلنا على قيمتها بسهولة؟ الجواب هو أن المكدس ينمو من اليمين إلى اليسار. سيكون للمتغيرات المعلنة أولاً عناوين أعلى من ذلك بكثير ، والمتغيرات التي تم الإعلان عنها لاحقًا سيكون لها عناوين أقل.


السلوك عند استدعاء أساليب المثيل


كلا النوعين من البيانات لهما ميزة أخرى ليس من السهل رؤيتها ويمكنهما شرح هيكل كلا النوعين. وهو يتعامل مع استدعاء أساليب المثال.


 // The example with a reference type class FooClass { private int x; public void ChangeTo(int val) { x = val; } } // The example with a value type struct FooStruct { private int x; public void ChangeTo(int val) { x = val; } } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); klass.ChangeTo(10); strukt.ChangeTo(10); 

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


 // An example with a reference type class FooClass { public int x; } // An example with a value type struct FooStruct { public int x; } public void ChangeTo(FooClass klass, int val) { klass.x = val; } public void ChangeTo(ref FooStruct strukt, int val) { strukt.x = val; } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); ChangeTo(klass, 10); ChangeTo(ref strukt, 10); 

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


القدرة على الإشارة إلى موقف العناصر.


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


  • للعمل مع واجهات برمجة التطبيقات الخارجية في العالم غير المُدار دون الحاجة إلى إدراج حقول غير مستخدمة قبل حقل ضروري ؛
  • لتوجيه برنامج التحويل البرمجي لتحديد موقع حق في بداية نوع ( [FieldOffset(0)] ). وستجعل العمل مع هذا النوع أسرع. إذا كان هذا الحقل شائع الاستخدام ، فيمكننا زيادة أداء التطبيق. ومع ذلك ، هذا صحيح فقط لأنواع القيمة. في أنواع المراجع ، يحتوي الحقل الذي يحتوي على إزاحة صفرية على عنوان جدول الأساليب الافتراضية ، والذي يأخذ 1 كلمة آلة. حتى لو قمت بمعالجة الحقل الأول من الفصل ، فسيستخدم العنوان المركب (عنوان + إزاحة). هذا لأن حقل الفئة الأكثر استخدامًا هو عنوان جدول الأساليب الافتراضية. الجدول ضروري لاستدعاء جميع الأساليب الافتراضية ؛
  • للإشارة إلى عدة حقول باستخدام عنوان واحد. في هذه الحالة ، يتم تفسير نفس القيمة على أنها أنواع بيانات مختلفة. في C ++ يسمى هذا النوع من البيانات اتحاد؛
  • لا تهتم بتصريح أي شيء: سيقوم المترجم بتخصيص الحقول على النحو الأمثل. وبالتالي ، قد يكون الترتيب النهائي للحقول مختلفًا.

ملاحظات عامة


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

استخدام FieldOffset لتخطي حقول البنية غير المستخدمة


يمكن أن تحتوي الهياكل القادمة من العالم غير المُدار على حقول محفوظة. يمكن للمرء استخدامها في إصدار مستقبلي من المكتبة. في C / C ++ ، نملأ هذه الثغرات عن طريق إضافة حقول ، على سبيل المثال Reserved1 ، Reserved2 ، ... ومع ذلك ، في .NET ، قمنا فقط بالتعويض إلى بداية حقل باستخدام سمة FieldOffsetAttribute و [StructLayout(LayoutKind.Explicit)] .


 [StructLayout(LayoutKind.Explicit)] public struct SYSTEM_INFO { [FieldOffset(0)] public ulong OemId; // 92 bytes reserved [FieldOffset(100)] public ulong PageSize; [FieldOffset(108)] public ulong ActiveProcessorMask; [FieldOffset(116)] public ulong NumberOfProcessors; [FieldOffset(124)] public ulong ProcessorType; } 

الفجوة مشغولة ولكن المساحة غير المستخدمة. سيكون للهيكل حجم يساوي 132 وليس 40 بايت كما قد يبدو من البداية.


الاتحاد


باستخدام FieldOffsetAttribute ، يمكنك محاكاة نوع C / C ++ الذي يسمى الاتحاد. يسمح بالوصول إلى نفس البيانات ككيانات
أنواع مختلفة. لنلقِ نظرة على المثال التالي:


 // If we read the RGBA.Value, we will get an Int32 value accumulating all // other fields. // However, if we try to read the RGBA.R, RGBA.G, RGBA.B, RGBA.Alpha, we // will get separate components of Int32. [StructLayout(LayoutKind.Explicit)] public struct RGBA { [FieldOffset(0)] public uint Value; [FieldOffset(0)] public byte R; [FieldOffset(1)] public byte G; [FieldOffset(2)] public byte B; [FieldOffset(3)] public byte Alpha; } 

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


 class Program { public static void Main() { Union x = new Union(); x.Reference.Value = "Hello!"; Console.WriteLine(x.Value.Value); } [StructLayout(LayoutKind.Explicit)] public class Union { public Union() { Value = new Holder<IntPtr>(); Reference = new Holder<object>(); } [FieldOffset(0)] public Holder<IntPtr> Value; [FieldOffset(0)] public Holder<object> Reference; } public class Holder<T> { public T Value; } } 

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


الفرق في التخصيص


ميزة أخرى تميز كلا النوعين هي تخصيص الذاكرة للكائنات أو الهياكل. يجب أن يقرر CLR عدة أشياء قبل تخصيص الذاكرة لكائن ما. ما هو حجم كائن؟ هل هو أكثر أو أقل من 85K؟ إذا كان أقل ، فهل هناك مساحة كافية على SOH لتخصيص هذا الكائن؟ إذا كان أكثر من ذلك ، يقوم CLR بتنشيط أداة تجميع البيانات المهملة. يمر عبر الرسم البياني للكائن ، ويضغط الكائنات عن طريق تحريكها إلى مساحة خالية. في حالة عدم وجود مساحة على SOH ، سيبدأ تخصيص صفحات الذاكرة الظاهرية الإضافية. عندها فقط يتم تخصيص مساحة لكائن ما ، يتم مسحه من القمامة. بعد ذلك ، يحدد CLR SyncBlockIndex و VirtualMethodsTable. أخيرًا ، تعود الإشارة إلى كائن إلى مستخدم.


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


هناك العديد من السيناريوهات المحتملة لـ RefTypes:


  • RefType <85K ، هناك مساحة على SOH: تخصيص ذاكرة سريع ؛
  • RefType <85K ، المساحة على SOH تنفد: تخصيص ذاكرة بطيئة جدًا؛
  • RefType> 85K ، تخصيص ذاكرة بطيئة.

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


ومع ذلك ، لا يستغرق ذلك وقتًا أطول من نسخ متغير واحد إلى آخر.


الاختيار بين فئة أو هيكل


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


  • بناءً على بنية نظام الكتابة ، والتي سيتفاعل فيها النوع الخاص بك ؛
  • بناءً على النهج الذي تتبعه كمبرمج نظام لاختيار نوع ذي أداء مثالي ؛
  • عندما لا يكون هناك خيار آخر.

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


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


    • إن DateTime عبارة عن بنية تتضمن مفهوم لحظة في الوقت. إنه يخزن هذه البيانات على أنها uint ولكنه يتيح الوصول إلى خصائص منفصلة للحظة من الزمن: السنة ، والشهر ، واليوم ، والساعة ، والدقائق ، بالميللي ثانية وحتى علامات المعالج. ومع ذلك ، فإنه غير قابل للتغيير ، مستندة إلى ما تغليفه. لا يمكننا تغيير لحظة في الوقت المناسب. لا أستطيع العيش في اللحظة التالية كما لو كان أفضل عيد ميلادي في الطفولة. وبالتالي ، إذا اخترنا نوع البيانات ، فيمكننا اختيار فئة ذات واجهة للقراءة فقط ، والتي تنتج مثيلًا جديدًا لكل تغيير في الخصائص. أو ، يمكننا اختيار هيكل ، يمكنه ولكن لا ينبغي تغيير حقول مثيلاته: قيمته هي وصف لحظة في الوقت ، مثل الرقم. لا يمكنك الوصول إلى بنية الرقم وتغييره. إذا كنت ترغب في الحصول على لحظة أخرى في الوقت المناسب ، والتي تختلف عن يوم واحد عن الأصلي ، فستحصل فقط على مثيل جديد للبنية.
    • KeyValuePair<TKey, TValue> هو هيكل يحتوي على مفهوم لزوج القيمة - المفتاح المتصل. هذه البنية هي فقط لإخراج محتوى القاموس أثناء التعداد. من وجهة النظر المعمارية ، فإن المفتاح والقيمة هما مفاهيم غير قابلة للتجزئة في Dictionary<T> . ومع ذلك ، في الداخل لدينا بنية معقدة ، حيث يكمن المفتاح بشكل منفصل عن القيمة. بالنسبة للمستخدم ، يعتبر زوج القيمة الرئيسية مفهومًا لا ينفصل عن الواجهة وواجهة بنية البيانات. إنها قيمة كاملة بحد ذاتها. إذا قام أحدهم بتعيين قيمة أخرى لأحد المفاتيح ، فسيتغير الزوج بالكامل. وبالتالي ، فإنها تمثل كيان واحد. هذا يجعل هيكل البديل المثالي في هذه الحالة.

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


    • على سبيل المثال ، إذا أخذنا بنية رأس ملف ، فسيكون من غير المناسب تمرير مرجع من ملف إلى آخر ، على سبيل المثال ، بعض ملفات header.txt. سيكون هذا مناسبًا عند إدراج مستند في آخر ، وليس عن طريق تضمين ملف ولكن باستخدام مرجع في نظام الملفات. مثال جيد على ذلك هو اختصار الملفات في نظام التشغيل ويندوز. ومع ذلك ، إذا تحدثنا عن رأس ملف (على سبيل المثال رأس ملف JPEG الذي يحتوي على بيانات التعريف حول حجم الصورة وطرق الضغط ومعلمات التصوير وإحداثيات GPS وغيرها) ، فينبغي أن نستخدم هياكل لتصميم أنواع لتحليل الرأس. إذا كنت تصف جميع الرؤوس في الهياكل ، فستحصل على نفس موضع الحقول في الذاكرة كما في الملف. باستخدام تحويل بسيط غير آمن *(Header *)readedBuffer دون إلغاء التسلسل ، ستحصل على بنيات بيانات ممتلئة بالكامل.


  1. لا يوجد مثال يوضح ميراث السلوك. أنها تظهر أنه ليست هناك حاجة إلى وراثة سلوك هذه الكيانات. فهي مكتفية ذاتيا. ومع ذلك ، إذا أخذنا فعالية الشفرة في الاعتبار ، فسنرى الاختيار من جانب آخر:
  2. إذا احتجنا إلى أخذ بعض البيانات المنظمة من التعليمات البرمجية غير المُدارة ، فيجب علينا اختيار الهياكل. يمكننا أيضًا تمرير بنية البيانات إلى طريقة غير آمنة. نوع المرجع غير مناسب لهذا على الإطلاق.
  3. الهيكل هو اختيارك إذا مر نوع ما بالبيانات في استدعاءات الطريقة (كقيم مُرجعة أو كمعلمة أسلوب) ولا داعي للإشارة إلى نفس القيمة من أماكن مختلفة. المثال المثالي هو tuples. إذا أرجعت إحدى الطرق عدة قيم باستخدام tuples ، فسوف تُرجع ValueTuple ، المُعلنة كهيكل. لن تقوم الطريقة بتخصيص مساحة على الكومة ، ولكنها ستستخدم مكدس مؤشر الترابط ، حيث لا يكلف تخصيص الذاكرة أي شيء.
  4. إذا قمت بتصميم نظام ينشئ حركة مرور كبيرة من المثيلات ذات الحجم الصغير وعمرها ، فإن استخدام أنواع المراجع سيؤدي إما إلى تجمع من الكائنات أو ، إذا لم يكن هناك تجمع للكائنات ، إلى تراكم غير مهيأ للقمامة على الكومة. ستتحول بعض الكائنات إلى أجيال أقدم ، مما يزيد من الحمل على GC. سيؤدي استخدام أنواع القيم في مثل هذه الأماكن (إذا كان ذلك ممكنًا) إلى زيادة الأداء نظرًا لأنه لن يتم تمرير أي شيء إلى SOH. هذا سوف يقلل من الحمل على GC والخوارزمية ستعمل بشكل أسرع ؛

بناءً على ما قلته ، إليك بعض النصائح حول استخدام الهياكل:


  1. عند اختيار المجموعات ، يجب عليك تجنب الصفائف الكبيرة التي تقوم بتخزين الهياكل الكبيرة. وهذا يشمل هياكل البيانات على أساس صفائف. يمكن أن يؤدي هذا إلى الانتقال إلى كومة الكائنات الكبيرة وتفتيتها. من الخطأ الاعتقاد أنه إذا كان هيكلنا يحتوي على 4 حقول من نوع البايت ، فسوف يستغرق الأمر 4 بايت. يجب أن نفهم أنه في أنظمة 32 بت يتم محاذاة كل حقل بنية على حدود 4 بايت (يجب تقسيم كل حقل عنوان تمامًا على 4) وفي أنظمة 64 بت - على حدود 8 بايت. يجب أن يعتمد حجم المصفوفة على حجم البنية والنظام الأساسي ، وتشغيل البرنامج. في مثالنا الذي يحتوي على 4 بايتات - 85 كيلو بايت / (من 4 إلى 8 بايت لكل حقل * عدد الحقول = 4) ناقص حجم رأس الصفيف يساوي حوالي 600 عنصر لكل صفيف وفقًا للنظام الأساسي (يجب تقريب هذا لأسفل ) هذا ليس كثيرا جدا. ربما يبدو أننا يمكن أن نصل بسهولة إلى ثابت سحري يتكون من 20000 عنصر
  2. في بعض الأحيان تستخدم بنية ذات حجم كبير كمصدر للبيانات وتضعها كحقل في الفصل الدراسي ، بينما يتم نسخ نسخة واحدة لإنتاج ألف من الحالات. ثم تقوم بتوسيع كل مثيل لفئة لحجم بنية. سيؤدي ذلك إلى تورم الجيل صفر والانتقال إلى الجيل الأول والثاني. إذا كانت مثيلات الفصل الدراسي لها فترة حياة قصيرة وكنت تعتقد أن GC ستجمعها عند الجيل صفر - لمدة 1 مللي ثانية ، ستصاب بخيبة أمل. هم بالفعل في جيل واحد وحتى اثنين. هذا يجعل الفرق. إذا قام GC بجمع الجيل صفر لمدة 1 مللي ثانية ، فسيتم جمع الأجيال الأولى والثانية ببطء شديد مما يؤدي إلى انخفاض الكفاءة ؛
  3. للسبب نفسه ، يجب تجنب تمرير بنيات كبيرة عبر سلسلة من مكالمات الطريقة. إذا اتصلت كل العناصر ببعضها البعض ، فسوف تشغل هذه المكالمات مساحة أكبر على المكدس وستؤدي إلى توقف التطبيق الخاص بك عن طريق StackOverflowException. السبب التالي هو الأداء. لمزيد من النسخ هناك أكثر ببطء يعمل كل شيء.

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


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

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

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


All Articles