Buggy C # Code: الأخطاء العشرة الأكثر شيوعًا في البرمجة بلغة C #

نشرت: 2022-03-11

حول سي شارب

C # هي واحدة من عدة لغات تستهدف Microsoft Common Language Runtime (CLR). تستفيد اللغات التي تستهدف CLR من ميزات مثل التكامل عبر اللغات ومعالجة الاستثناءات ، والأمان المحسّن ، والنموذج المبسط لتفاعل المكونات ، وخدمات التصحيح والتنميط. من بين لغات CLR الحالية ، تعد C # هي الأكثر استخدامًا على نطاق واسع لمشاريع التطوير المهنية المعقدة التي تستهدف سطح مكتب Windows أو الهاتف المحمول أو بيئات الخادم.

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

حول هذا البرنامج التعليمي للبرمجة سي شارب

يصف هذا البرنامج التعليمي 10 من أكثر أخطاء البرمجة C # شيوعًا أو المشكلات التي يجب تجنبها بواسطة مبرمجي C # وتزويدهم بالمساعدة.

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

خطأ البرمجة C # الشائع # 1: استخدام مرجع مثل القيمة أو العكس

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

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

 Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue

كما ترى ، تم إنشاء كائنات Point و Pen بالطريقة نفسها تمامًا ، لكن قيمة point1 ظلت دون تغيير عندما تم تعيين قيمة إحداثي X جديدة إلى point2 ، بينما تم تعديل قيمة pen1 عند تعيين لون جديد pen2 . لذلك يمكننا أن نستنتج أن point1 والنقطة 2 تحتوي كل منهما على point2 الخاصة من كائن Point ، بينما يحتوي كل من pen1 و pen2 على إشارات إلى نفس كائن Pen . لكن كيف يمكننا معرفة ذلك بدون إجراء هذه التجربة؟

الإجابة هي إلقاء نظرة على تعريفات أنواع الكائنات (والتي يمكنك القيام بها بسهولة في Visual Studio عن طريق وضع المؤشر فوق اسم نوع الكائن والضغط على F12):

 public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type

كما هو موضح أعلاه ، في برمجة C # ، يتم استخدام الكلمة الأساسية struct لتحديد نوع القيمة ، بينما يتم استخدام الكلمة الأساسية class لتحديد نوع المرجع. بالنسبة لأولئك الذين لديهم خلفية C ++ ، الذين هدأوا إلى شعور زائف بالأمان من خلال العديد من أوجه التشابه بين الكلمات الرئيسية C ++ و C # ، من المحتمل أن يكون هذا السلوك بمثابة مفاجأة قد تجعلك تطلب المساعدة من برنامج تعليمي C #.

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

خطأ البرمجة C # الشائع # 2: سوء فهم القيم الافتراضية للمتغيرات غير المهيأة

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

 class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }

لماذا لا تعتبر point1 فارغة؟ الإجابة هي أن Point هي نوع قيمة ، والقيمة الافتراضية Point هي (0،0) وليست خالية. يعد الفشل في التعرف على هذا خطأ سهلًا (وشائعًا) يتم ارتكابه في C #.

تحتوي العديد من أنواع القيم (وليس جميعها) على خاصية IsEmpty والتي يمكنك التحقق منها لمعرفة ما إذا كانت تساوي قيمتها الافتراضية:

 Console.WriteLine(point1.IsEmpty); // True

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

خطأ البرمجة C # الشائع # 3: استخدام طرق مقارنة سلسلة غير مناسبة أو غير محددة

هناك العديد من الطرق المختلفة لمقارنة السلاسل في C #.

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

بدلاً من ذلك ، فإن الطريقة المفضلة لاختبار تكافؤ السلسلة في برمجة C # هي باستخدام طريقة Equals :

 public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);

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

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

 string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));

الممارسة الأكثر أمانًا هي توفير معلمة comparisonType دائمًا للأسلوب Equals . فيما يلي بعض الإرشادات الأساسية:

  • عند مقارنة السلاسل التي تم إدخالها بواسطة المستخدم ، أو التي سيتم عرضها للمستخدم ، استخدم مقارنة تراعي الثقافة ( CurrentCulture أو CurrentCultureIgnoreCase ).
  • عند مقارنة السلاسل البرمجية ، استخدم المقارنة الترتيبية ( Ordinal أو OrdinalIgnoreCase ).
  • لا يتم استخدام InvariantCulture و InvariantCultureIgnoreCase إلا في ظروف محدودة للغاية ، لأن المقارنات الترتيبية أكثر كفاءة. إذا كانت المقارنة المدركة للثقافة ضرورية ، فيجب عادةً إجراؤها مقابل الثقافة الحالية أو ثقافة معينة أخرى.

بالإضافة إلى طريقة Equals ، توفر السلاسل أيضًا طريقة Compare ، والتي تمنحك معلومات حول الترتيب النسبي للسلاسل بدلاً من مجرد اختبار للمساواة. هذه الطريقة مفضلة على عوامل التشغيل < ، <= ، > و >= ، لنفس الأسباب المذكورة أعلاه - لتجنب مشاكل C #.

الموضوعات ذات الصلة: 12 سؤالًا أساسيًا للمقابلة على شبكة الإنترنت

خطأ البرمجة C # الشائع # 4: استخدام العبارات التكرارية (بدلاً من التصريحية) للتعامل مع المجموعات

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

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

بينما يعد الاستعلام عن قاعدة البيانات استخدامًا شائعًا جدًا لعبارات LINQ ، إلا أنها تعمل في الواقع على أي مجموعة يمكن عدها (على سبيل المثال ، أي كائن يقوم بتنفيذ واجهة IEnumerable). على سبيل المثال ، إذا كان لديك مجموعة من الحسابات ، فبدلاً من كتابة قائمة C # للكل:

 decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }

يمكنك فقط أن تكتب:

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

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

خطأ البرمجة C # الشائع رقم 5: الفشل في مراعاة الكائنات الأساسية في جملة LINQ

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

على سبيل المثال ، ضع في اعتبارك العبارة التالية:

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

ماذا يحدث إذا كان أحد حسابات الكائن account.Status الحالة يساوي "نشط" (لاحظ رأس المال أ)؟ حسنًا ، إذا كان myAccounts عبارة عن كائن DbSet (تم إعداده باستخدام التكوين الافتراضي غير الحساس لحالة الأحرف) ، فسيظل التعبير where يطابق هذا العنصر. ومع ذلك ، إذا كانت myAccounts موجودة في مصفوفة في الذاكرة ، فلن تتطابق ، وبالتالي ستؤدي إلى نتيجة مختلفة للإجمالي.

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

الإجابة هي أنه عندما تكون الكائنات الأساسية في عبارة LINQ مراجع لبيانات جدول SQL (كما هو الحال مع كائن Entity Framework DbSet في هذا المثال) ، يتم تحويل العبارة إلى جملة T-SQL. يتبع المشغلون بعد ذلك قواعد برمجة T-SQL ، وليس قواعد برمجة C # ، لذا فإن المقارنة في الحالة أعلاه ينتهي بها الأمر إلى أن تكون غير حساسة لحالة الأحرف.

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

خطأ البرمجة # 6 الشائع: الخلط بين طرق التمديد أو التزييف

كما ذكرنا سابقًا ، تعمل عبارات LINQ على أي كائن يقوم بتنفيذ IEnumerable. على سبيل المثال ، ستضيف الوظيفة البسيطة التالية الأرصدة في أي مجموعة حسابات:

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }

في الكود أعلاه ، تم التصريح عن نوع معلمة myAccounts على أنها IEnumerable<Account> . نظرًا لأن myAccounts تشير إلى طريقة Sum (تستخدم C # "تدوين النقطة" المألوف للإشارة إلى طريقة في فئة أو واجهة) ، نتوقع رؤية طريقة تسمى Sum() في تعريف واجهة IEnumerable<T> . ومع ذلك ، فإن تعريف IEnumerable<T> لا يشير إلى أي طريقة Sum ويبدو ببساطة كما يلي:

 public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }

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

الإجابة هي أن Sum() ليس طريقة محددة في واجهة IEnumerable . بدلاً من ذلك ، إنها طريقة ثابتة (تسمى "طريقة الامتداد") يتم تحديدها في فئة System.Linq.Enumerable :

 namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }

إذن ما الذي يجعل طريقة الامتداد مختلفة عن أي طريقة ثابتة أخرى وما الذي يمكننا من الوصول إليها في الفئات الأخرى؟

السمة المميزة لطريقة التمديد هي this المُعدِّل في معاملها الأول. هذا هو "السحر" الذي يعرّف المترجم بأنه طريقة تمديد. يشير نوع المعلمة التي يعدلها (في هذه الحالة IEnumerable<TSource> ) إلى الفئة أو الواجهة التي ستظهر بعد ذلك لتنفيذ هذه الطريقة.

(كنقطة جانبية ، لا يوجد شيء سحري حول التشابه بين اسم واجهة IEnumerable واسم فئة Enumerable التي يتم تعريف طريقة الامتداد على أساسها. هذا التشابه هو مجرد اختيار أسلوبي عشوائي.)

من خلال هذا الفهم ، يمكننا أيضًا أن نرى أن وظيفة sumAccounts التي قدمناها أعلاه كان من الممكن بدلاً من ذلك تنفيذها على النحو التالي:

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }

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

يتم وضع طرق الامتداد في النطاق عن طريق تضمين using [namespace]; بيان في أعلى الملف. تحتاج إلى معرفة مساحة الاسم C # التي تتضمن طرق الامتداد التي تبحث عنها ، ولكن من السهل جدًا تحديد ذلك بمجرد معرفة ما تبحث عنه.

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

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

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

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

أصبح استخدام طرق الامتداد في مكتبات C # سائدًا بشكل متزايد. بالإضافة إلى LINQ ، فإن Unity Application Block وإطار Web API هما مثالان على مكتبتين حديثتين مستخدمتين بكثرة بواسطة Microsoft والتي تستخدم طرق الامتداد أيضًا ، وهناك العديد من المكتبات الأخرى. كلما كان إطار العمل أكثر حداثة ، زادت احتمالية تضمينه لطرق التمديد.

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

خطأ البرمجة # 7 الشائع: استخدام النوع الخاطئ من المجموعة للمهمة قيد البحث

يوفر C # مجموعة كبيرة ومتنوعة من كائنات المجموعة ، مع كون القائمة التالية مجرد قائمة جزئية:
Array ، ArrayList ، BitArray ، BitVector32 ، Dictionary<K,V> ، HashTable ، HybridDictionary ، List<T> ، NameValueCollection ، OrderedDictionary ، Queue, Queue<T> ، SortedList ، Stack, Stack<T> ، StringCollection ، StringDictionary .

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

إذا كان هناك نوع مجموعة يستهدف نوع العنصر لديك (مثل سلسلة أو بت) فعليك باستخدامه أولاً. يكون التنفيذ أكثر كفاءة بشكل عام عندما يستهدف نوعًا معينًا من العناصر.

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

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

خطأ البرمجة C # الشائع # 8: إهمال تحرير الموارد

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

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

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

يعد تسرب الموارد مصدر قلق في أي بيئة تقريبًا. ومع ذلك ، يوفر C # آلية قوية وسهلة الاستخدام والتي ، إذا تم استخدامها ، يمكن أن تجعل التسريبات نادرة الحدوث. يحدد إطار عمل .NET واجهة IDisposable ، والتي تتكون فقط من طريقة Dispose() . يتوقع أي كائن يقوم بتطبيق IDisposable أن يتم استدعاء هذه الطريقة كلما انتهى مستهلك الكائن من معالجته. ينتج عن هذا تحرير صريح وحتمي للموارد.

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

 using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }

من خلال إنشاء كتلة using في المثال أعلاه ، فأنت تعلم على وجه اليقين أن myFile.Dispose() سيتم استدعاءه بمجرد الانتهاء من الملف ، سواء أكان Read() يطرح استثناءًا أم لا.

خطأ البرمجة رقم 9 الشائع: الابتعاد عن الاستثناءات

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

على سبيل المثال ، إليك طريقتان مختلفتان لأداء نوع صريح في C #:

 // METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;

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

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

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

 int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty

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

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

 if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }

بدلا من:

 try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }

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

خطأ البرمجة # 10 الشائع: السماح بتراكم تحذيرات المترجم

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

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

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

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

 class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }

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

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

تذكر أن مترجم C Sharp يمنحك الكثير من المعلومات المفيدة حول قوة التعليمات البرمجية الخاصة بك ... إذا كنت تستمع. لا تتجاهل التحذيرات. عادةً ما يستغرق إصلاحها بضع ثوانٍ فقط ، ويمكن أن يوفر لك إصلاح أخرى جديدة عند حدوثها ساعات. درب نفسك على توقع أن تعرض نافذة Visual Studio "Error List" "0 أخطاء ، 0 تحذيرات" ، بحيث تجعلك أي تحذيرات على الإطلاق غير مرتاح بما يكفي لمعالجتها على الفور.

بالطبع، هناك استثناءات لكل قاعدة. وفقًا لذلك ، قد تكون هناك أوقات تبدو فيها التعليمات البرمجية مريبة إلى حد ما بالنسبة للمترجم ، على الرغم من أن هذا هو بالضبط ما كنت تريده أن يكون. في تلك الحالات النادرة جدًا ، استخدم #pragma warning disable [warning id] فقط حول الكود الذي يطلق التحذير ، وفقط لمعرّف التحذير الذي يطلقه. سيؤدي هذا إلى منع هذا التحذير ، وهذا التحذير فقط ، بحيث يظل بإمكانك البقاء في حالة تأهب للتحذير الجديد.

يتم إحتوائه

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

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


مزيد من القراءة على مدونة Toptal Engineering:

  • أسئلة المقابلة الأساسية C #
  • C # مقابل C ++: ماذا يوجد في الجوهر؟