المفاهيم الخاطئة للمطورين المبتدئين C #. محاولة للإجابة على الأسئلة القياسية

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



# 1 تعويذة حوالي 3 أجيال في أي حالة


هذا هو عدم دقة أكثر من خطأ. أصبح السؤال حول "أداة تجميع البيانات المهملة في C #" للمطور أمرًا كلاسيكيًا وسيبدأ عدد قليل من الأشخاص في الإجابة بذكاء حول مفهوم الأجيال. ومع ذلك ، لسبب ما ، قلة من الناس تولي اهتماما لحقيقة أن جامع القمامة الكبير والرهيب هو جزء من وقت التشغيل. وفقًا لذلك ، كنت قد أوضحت أنها لم تكن إصبعًا ، وسألت عن نوع بيئة التشغيل. للاستعلام "أداة تجميع البيانات المهملة في c #" ، يمكن العثور على أكثر من الكثير من المعلومات المماثلة على الإنترنت. ومع ذلك ، يذكر عدد قليل من الناس أن هذه المعلومات تشير إلى CLR / CoreCLR (كقاعدة عامة). لكن لا تنسى أن مونو ، وقت التشغيل الخفيف والمرن والمضمّن الذي احتل مكانته في تطوير الأجهزة المحمولة (Unity، Xamarin) ويستخدم في Blazor. وبالنسبة للمطورين المعنيين ، أنصحك بالاستعلام عن تفاصيل جهاز التجميع في Mono. على سبيل المثال ، عند الاستعلام "أجيال جامع القمامة الأحادية" ، يمكنك أن ترى أن هناك جيلين فقط - الحضانة والجيل القديم (في جامع القمامة الجديد والعصري - SGen ).

# 2 تعويذة حوالي 2 مراحل لجمع القمامة في أي حالة


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

ومع ذلك ، فقد تغير كل شيء عندما أطلق رجال النار حربًا مع ظهور CoreCLR والشفرة المصدرية للمجمع . تم أخذ رمز برنامج التحويل البرمجي لـ CoreCLR بالكامل من إصدار CLR. لم يكتبها أحد من نقطة الصفر ، على التوالي ، وكل ما يمكن تعلمه تقريبًا من كود مصدر CoreCLR سيكون صحيحًا بالنسبة إلى CLR أيضًا. الآن ، لفهم كيفية عمل شيء ما ، ما عليك سوى الانتقال إلى github والعثور عليه في الكود المصدري أو قراءة الملف التمهيدي . هناك يمكنك أن ترى أن هناك 5 مراحل: وضع العلامات ، والتخطيط ، وتحديث الروابط ، والضغط (الحذف مع إعادة التوطين) والحذف دون نقل (من الصعب الترجمة). لكن بشكل رسمي يمكن تقسيمها إلى 3 مراحل - التعليم والتخطيط والتنظيف.

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

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

# 3 تخصيص الذاكرة على كومة الذاكرة المؤقتة يكون بنفس سرعة المكدس


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

# 4 تعريف المرجع ، وأنواع ذات مغزى والتعبئة من خلال مفاهيم المكدس والكومة


الكلاسيكية الصحيحة ، والتي ، لحسن الحظ ، ليست شائعة جدا.

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

قد يكون هناك نوع كبير:

  1. في الذاكرة الديناميكية (الكومة) ، إذا كان جزءًا من كائن موجود في الكومة ، أو في حالة التعبئة ؛
  2. على المكدس ، إذا كانت قيمة متغير / وسيطة / إرجاع محلية للطريقة ؛
  3. في السجلات ، إذا كان يسمح حجم نوع كبير وشروط أخرى.

نوع المرجع ، أي القيمة التي يشير إليها الارتباط ، موجود حاليًا على الكومة.

الرابط نفسه يمكن أن يكون موجودا في نفس المكان الذي يوجد فيه النوع الهام.

كما لا يتم تحديد التغليف من خلال مواقع التخزين. النظر في مثال موجز.

رمز C #
public struct MyStruct { public int justField; } public class MyClass { public MyStruct justStruct; } public static void Main() { MyClass instance = new MyClass(); object boxed = instance.justStruct; } 


ورمز IL المقابل للطريقة الرئيسية

كود IL
  1: nop 2: newobj instance void C/MyClass::.ctor() 3: stloc.0 4: ldloc.0 5: ldfld valuetype C/MyStruct C/MyClass::justStruct 6: box C/MyStruct 7: stloc.1 8: ret 


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

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

# 4 الأحداث - آلية منفصلة


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

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

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

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

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

# 5 الموارد المدارة وغير المدارة. النهائيون و idisposable


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

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

  1. واجهة IDisposable (وإصدارها غير المتزامن من IAsyncDisposable). تتم مراقبته بواسطة جميع أجهزة تحليل الشفرات ، لذلك يصعب نسيان مكالمته. يوفر طريقة واحدة - التخلص. ودعم مترجم هو استخدام البيان. مرشح ممتاز لنص أسلوب التخلص هو استدعاء طريقة مماثلة لأحد الحقول في الفصل الدراسي أو لتحرير مورد غير مُدار. دعا صراحة من قبل مستخدم الصف. يعني وجود هذه الواجهة في الفصل أنه عند الانتهاء من العمل مع المثيل ، تحتاج إلى استدعاء هذه الطريقة.
  2. Finalizer. في جوهرها هو التأمين. تم الاحتجاج به ضمنيًا ، في وقت غير محدد ، أثناء جمع القمامة. يبطئ تخصيص الذاكرة ، وهو عمل جامع البيانات المهملة ، ويمتد عمر الكائنات على الأقل حتى التجميع التالي ، أو حتى لفترة أطول ، ولكن يطلق عليه في حد ذاته ، حتى لو لم يطلق عليه أحد. نظرًا لطبيعتها غير الحتمية ، يجب تحرير الموارد غير المدارة فقط. يمكنك أيضًا العثور على أمثلة تم فيها استخدام أداة الإنهاء النهائي لإحياء الكائن وتنظيم تجمع الكائنات بهذه الطريقة. ومع ذلك ، مثل هذا التطبيق لمجموعة من الكائنات هو بالتأكيد فكرة سيئة. مثل محاولة تسجيل الدخول ، ورمي استثناءات ، والوصول إلى قاعدة البيانات وآلاف من الإجراءات المماثلة.

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

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

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

# 6 دفق المكدس ، استدعاء المكدس ، مكدس الحوسبة و
  المكدس <T> 


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

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

كان يعتبر بالتفصيل في المادة السابقة .
أيضًا ، يتم استخدام تعريف مكدس الاستدعاء للإشارة إلى سلسلة استدعاءات أساليب معينة بلغة معينة.

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

# 7 المزيد من المواضيع - رمز أسرع


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

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

لجميع التعاريف ، أوصي بالاتصال هنا:

C # مواصفات اللغة - ECMA-334
مصادر جيدة فقط:
كونراد كوكوسا - برو. إدارة الذاكرة
مواصفات CLI - ECMA-335
المطورين CoreCLR حول وقت التشغيل - كتاب وقت التشغيل
من Stanislav Sidristy حول الانتهاء والمزيد - .NET Platform Architecture

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


All Articles