مسلية C #. خمسة أمثلة لاستراحات القهوة

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

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

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

الصورة

مثال 1


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

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

النظر في الكود:

public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } static void Main(string[] args) { var d = new SDummy(); using (d) { Console.WriteLine(d.GetDispose()); } Console.WriteLine(d.GetDispose()); } } 

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

وفقًا لمواصفات لغة C # ، فإن استخدام جملة للأنواع المهمة في وقت الترجمة يتوسع إلى كتلة try-وأخيراً:

  try { Console.WriteLine(d.GetDispose()); } finally { ((IDisposable)d).Dispose(); } 

لذلك ، في التعليمة البرمجية الخاصة بنا ، يتم استدعاء الأسلوب GetDispose () داخل كتلة الاستخدام ، والتي تُرجع حقل Boolean _dispose ، والتي لم يتم تعيين قيمتها بعد للكائن d (تم تعيينها فقط في طريقة Dispose () ، والتي لم يتم استدعاؤها بعد) وبالتالي يتم إرجاع القيمة الافتراضي هو خطأ. ما التالي؟

ثم الأكثر إثارة للاهتمام.
تشغيل خط في كتلة أخيرا
  ((IDisposable)d).Dispose(); 

عادة ما يؤدي إلى الملاكمة. هذا ليس من الصعب رؤيته ، على سبيل المثال ، هنا (في الجزء العلوي الأيمن في النتائج ، حدد أولاً C # ، ثم IL):

الصورة

في هذه الحالة ، يتم استدعاء الأسلوب Dispose بالفعل لكائن آخر ، وليس للكائن d على الإطلاق.
قم بتشغيل برنامجنا وشاهد أن البرنامج يعرض "False False" بالفعل على وحدة التحكم. ولكن هل هذا بسيط؟ :)

في الواقع ، لا التعبئة يحدث. ما ، وفقًا لإريك ليبرت ، يتم من أجل التحسين (انظر هنا وهنا ).
ولكن ، إذا لم يكن هناك عبوة (والتي قد تبدو في حد ذاتها مفاجئة) ، فلماذا "False False" وليس "False True" على الشاشة ، لأن التخلص يجب الآن تطبيقه على نفس الكائن؟!؟

وهنا ليس لذلك!
ألقِ نظرة على ما يوسع برنامج التحويل البرمجي C # برنامجنا إلى:

 public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } private static void Main(string[] args) { SDummy sDummy = default(SDummy); SDummy sDummy2 = sDummy; try { Console.WriteLine(sDummy.GetDispose()); } finally { ((IDisposable)sDummy2).Dispose(); } Console.WriteLine(sDummy.GetDispose()); } } 


يوجد sDummy2 متغير جديد ، يتم تطبيق طريقة التخلص منه ()!
من أين جاء هذا المتغير المخفي؟
دعنا ننتقل إلى المواصفات مرة أخرى :
تحتوي العبارة باستخدام النموذج 'using (expression) statement' على التوسعات الثلاثة المحتملة نفسها. في هذه الحالة ، يكون ResourceType ضمنيًا هو نوع وقت الترجمة للتعبير ... لا يمكن الوصول إلى متغير "مورد" في العبارة المضمنة وغير مرئية لها.

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

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

الاستنتاج العام هو هذا: أنواع القيم القابلة للتغيير هي الشر الذي من الأفضل تجنبه.

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

أود أن أتوجه بالشكر الخاص إلى SergeyT على تعليقاتك القيمة على هذا المثال.



مثال 2


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

لذا ، ضع في اعتبارك فئة MyLogger:

 class MyLogger { static MyLogger innerInstance = new MyLogger(); static MyLogger() { Console.WriteLine("Static Logger Constructor"); } private MyLogger() { Console.WriteLine("Instance Logger Constructor"); } public static MyLogger Instance { get { return innerInstance; } } } 

افترض أن هذه الفئة بها بعض منطق الأعمال الذي نحتاجه لدعم التسجيل (الوظيفة ليست مهمة في الوقت الحالي).

دعونا نرى ما هو في صفنا MyLogger:

  1. ثابت البناء المحدد
  2. هناك منشئ خاص دون معلمات
  3. ثابت ثابت متغير innerInstance المحددة
  4. وهناك خاصية ثابتة مفتوحة لـ مثيل للتواصل مع العالم الخارجي

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

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

 class Program { public static void Main() { var logger = MyLogger.Instance; } } 

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

في حالتنا ، ننتقل إلى عضو الفئة - الخاصية مثيل ، والتي يجب أن تتسبب في بدء المنشئ الثابت أولاً ، ثم سيتم استدعاء مُنشئ مثيل الفئة. أي سينتج البرنامج:

ثابت المسجل البناء
مثيل المسجل منشئ


ومع ذلك ، بعد بدء البرنامج ، نحصل على وحدة التحكم:

مثيل المسجل منشئ
ثابت المسجل البناء


كيف ذلك؟ عملت منشئ مثيل قبل المنشئ ثابت؟!؟
الجواب: نعم!

وهنا السبب.

ينص المعيار C # ECMA-334 على ما يلي للفئات الثابتة:

17.4.5.1: "في حالة وجود مُنشئ ثابت (الفقرة 17.11) في الفصل ، يحدث تنفيذ مُهيئات الحقل الثابت مباشرةً قبل تنفيذ ذلك المنشئ الثابت.
...
17.11: ... إذا كان الفصل يحتوي على أي حقول ثابتة مع مُهيئات ، يتم تنفيذ تلك المُهيئات بالترتيب النصي مباشرةً قبل تنفيذ المُنشئ الثابت

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

في حالتنا ، يتم الإعلان عن الحقل الثابت innerInstance مع المُهيئ ، وهو مُنشئ مثيل الفئة. وفقًا لمعيار ECMA ، يجب أن يطلق على أداة التهيئة قبل استدعاء المنشئ الثابت. ما يحدث في برنامجنا: يُسمى مُنشئ المثيل ، كونه مُهيئ الحقل الثابت ، قبل المُنشئ الثابت. توافق ، بشكل غير متوقع تماما.

لاحظ أن هذا صحيح فقط بالنسبة لمهيئات الحقل الثابت. بشكل عام ، يُطلق على المُنشئ الثابت قبل استدعاء مُنشئ مثيل الفئة.

مثل ، على سبيل المثال ، هنا:

 class MyLogger { static MyLogger() { Console.WriteLine("Static Logger Constructor"); } public MyLogger() { Console.WriteLine("Instance Logger Constructor"); } } class Program { public static void Main() { var logger = new MyLogger(); } } 

ومن المتوقع أن يخرج البرنامج إلى وحدة التحكم:

ثابت المسجل البناء
مثيل المسجل منشئ


الصورة

مثال 3


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

لنفترض أننا نحتاج إلى تنفيذ دالة تتحقق من الرقم بسبب الغرابة (أي أن الرقم غير قابل للقسمة على 2 بدون الباقي).

قد يبدو التنفيذ كالتالي:

 static bool isOddNumber(int i) { return (i % 2 == 1); } 

للوهلة الأولى ، كل شيء على ما يرام ، وعلى سبيل المثال للأرقام 5.7 و 11 ، نتوقع أن نحصل على True.

ما سوف ترجع الدالة isOddNumber (-5)؟
-5 هو رقم فردي ، ولكن كرد على وظيفتنا نحصل على False!
دعونا نتعرف على السبب.

وفقًا لـ MSDN ، يتم كتابة ما يلي حول باقي عامل٪ division:
"بالنسبة لمعاملات الأعداد الصحيحة ، تكون نتيجة٪ b هي القيمة الناتجة عن a - (a / b) * b"
في حالتنا ، للحصول على = -5 ، ب = 2 نحصل على:
-5٪ 2 = (-5) - ((-5) / 2) * 2 = -5 + 4 = -1
لكن -1 دائمًا لا تساوي 1 ، وهو ما يفسر نتيجة خطأ لدينا.

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

 static bool isOddNumber(int i) { return (i % 2 != 0); } 

أو احصل على وظيفة منفصلة للتحقق من التكافؤ وتنفيذ المنطق من خلالها:

 static bool isEvenNumber(int i) { return (i % 2 == 0); } static bool isOddNumber(int i) { return !isEvenNumber(i); } 


مثال 4


كل شخص قام بالبرمجة في C # ، ربما التقى بـ LINQ ، وهو ملائم للغاية للعمل مع المجموعات ، وإنشاء استعلامات ، وتصفية وتجميع البيانات ...

لن ننظر تحت غطاء محرك السيارة LINQ. ربما سنفعل ذلك مرة أخرى.

في غضون ذلك ، فكر في مثال صغير:

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; var selectedData = dataArray.Select( x => { summResult += x; return x; }); Console.WriteLine(summResult); 

ما سوف يخرج هذا الرمز؟
نظهر على الشاشة قيمة المتغير summResult ، الذي يساوي القيمة الأولية ، أي 0

لماذا حدث هذا؟

ولأن تعريف استعلام LINQ وإطلاق هذا الاستعلام هما عمليتان يتم إجراؤهما بشكل منفصل. وبالتالي ، فإن تعريف الطلب لا يعني إطلاقه / تنفيذه.

يتم استخدام متغير summResult داخل مفوض مجهول في طريقة التحديد: يتم فرز عناصر صفيف dataArray بالتسلسل وإضافتها إلى متغير summResult.

يمكننا أن نفترض أن الكود الخاص بنا سوف يطبع مجموع عناصر صفيف dataArray. لكن LINQ لا يعمل بهذه الطريقة.

النظر في متغير المحدد البيانات. الكلمة الرئيسية var هي "السكر النحوي" ، مما يقلل في كثير من الحالات من حجم رمز البرنامج ويحسن قابليته للقراءة. ويقوم النوع الحقيقي لمتغير البيانات المحدد بتنفيذ واجهة IEnumerable. أي رمزنا يشبه هذا:

  IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; }); 

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

هذا هو ، حتى الآن وضعنا طلبًا فقط ، لكننا لم نطلقه. هذا هو السبب في أن قيمة المتغير summResult لم تتغير. يمكن تشغيل استعلام ، على سبيل المثال ، باستخدام أساليب ToArray أو ToList أو ToDictionary:

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; //        selectedData IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; }); //   selectedData selectedData.ToArray(); //    summResult Console.WriteLine(summResult); 

سيعرض هذا الرمز بالفعل قيمة المتغير summResult ، مساويًا لكل عناصر صفيف dataArray ، يساوي 15.

نحن برزت بها. ثم ماذا سيعرض هذا البرنامج على الشاشة؟

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; //1 var summResult = dataArray.Sum() + dataArray.Skip(3).Take(2).Sum(); //2 var groupedData = dataArray.GroupBy(x => x).Select( //3 x => { summResult += x.Key; return x.Key; }); Console.WriteLine(summResult); //4 

يقوم المتغير groupedData (السطر 3) بتنفيذ واجهة IEnumerable بالفعل ويقوم بتعريف الطلب إلى مصدر بيانات dataArray. هذا يعني أنه لكي يعمل مفوض مجهول ، والذي يغير قيمة متغير summResult ، يجب تشغيل هذا الطلب بشكل صريح. ولكن لا يوجد مثل هذا الإطلاق في برنامجنا. لذلك ، سيتم تغيير قيمة المتغير summResult فقط في السطر 2 ، ولا يمكننا مراعاة كل شيء آخر في حساباتنا.

بعد ذلك ، يكون من السهل حساب قيمة المتغير summResult ، أي ، على التوالي ، 15 + 7 ، أي 22.

مثال 5


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

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

لذلك يجب أن يكون هناك سطر من التعليمات البرمجية:

 int i = (int)+(char)-(int)+(long)-1; 

ماذا ستكون قيمة المتغير؟
الجواب: 1

قد تعتقد أن الحساب العددي يستخدم هنا على أحجام كل نوع بالبايت ، حيث أن العلامات "+" و "-" تصادف هنا بشكل غير متوقع هنا لتحويل النوع.

في C # ، من المعروف أن عدد الأعداد الصحيحة يبلغ طوله 4 بايت و 8 طول و char 2.

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

 int i = (4)+(2)-(4)+(8)-1; 

ومع ذلك ، هذا ليس كذلك. ومن أجل الخلط والتوجيه من خلال هذا المنطق الخاطئ ، يمكن تغيير المثال ، على سبيل المثال ، مثل هذا:

 int i = (int)+(char)-(int)+(long)-sizeof(int); 

لا يتم استخدام العلامات "+" و "-" في هذا المثال كعمليات حسابية ثنائية ، ولكن كمشغلين أحاديين. بعد ذلك ، يمثل سطر الشفرة لدينا مجرد سلسلة من تحويلات الكتابة الصريحة المختلطة مع مكالمات لعمليات أحادية ، والتي يمكن كتابتها على النحو التالي:

  int i = (int)( // call explicit operator int(char), ie char to int +( // call unary operator + (char)( // call explicit operator char(int), ie int to char -( // call unary operator - (int)( // call explicit operator int(long), ie long to int +( // call unary operator + (long)( // call explicit operator long(int), ie int to long -1 ) ) ) ) ) ) ); 


الصورة

هل أنت مهتم بالتعلم في أكاديمية فييم؟


الآن هناك مجموعة لفصل الربيع المكثف على C # في سان بطرسبرغ ، ونحن ندعو الجميع للخضوع للاختبار عبر الإنترنت على موقع Veeam Academy.

تبدأ الدورة في 18 شباط (فبراير) 2019 ، وتستمر حتى منتصف مايو وستكون ، كما هو الحال دائمًا ، مجانية تمامًا. التسجيل لأي شخص يريد الخضوع لاختبار القبول متاح بالفعل على موقع الأكاديمية: academy.veeam.ru

الصورة

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


All Articles