اذهب إلى المحتوى

لغة #C إحدى أبرز لغات البرمجة التي تطورها شركة مايكروسوفت ضمن بيئة التطوير المتكاملة Visual Studio. تعمل هذه اللغة ضمن بيئة تنفيذ اللغة المشتركة Common Language Runtime أو CLR اختصارًا والتي توفر ميزات عديدة مثل التكامل بين اللغات المختلفة ومعالجة الاستثناءات وتعزيز مستويات الأمان.

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

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

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

الخطأ 1: استخدام المرجع كقيمة أو العكس

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

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

 Point point1 = new Point(20, 30);
      Point point2 = point1;
      point2.X = 50;
      Console.WriteLine(point1.X);       // 20 (هل تفاجأت من هذه النتيجة)
      Console.WriteLine(point2.X);       // 50

      Pen pen1 = new Pen(Color.Black);
      Pen pen2 = pen1;
      pen2.Color = Color.Blue;
      Console.WriteLine(pen1.Color);     // Blue (أم تفاجأت من هذه النتيجة)
      Console.WriteLine(pen2.Color);     // Blue

لاحظ أن كلًا من الكائنين point وpen أنشئا بنفس الطريقة تمامًا، لكن قيمة point1 بقيت على حالها دون تغيير عند تعيين قيمة جديدة للإحداثي X فيpoint2.

أما في الحالة الثانية فقد تعدلت قيمة pen1 عند إسناد قيمة لون جديدة إلى pen2، وبالتالي يمكننا استنتاج أن كلًا من point1 وpoint2 يحتويان على نسخة من الكائن الأصلي point أما pen1 وpen2 فيحتويان على مراجع references تشير للكائن الأساسي pen وهذا ما يمكنه أن نتعلمه من خلال التجربة.

لمعرفة الفرق بين الحالتين يمكننا العودة إلى التعريف الخاص بكل كائن وهو ما يمكننا القيام به بسهولة في بيئة التطوير Visual Studio من خلال وضع المؤشر على اسم الكائن والضغط على المفتاح F12.

public struct Point { ... }     // نوع قيمة value
public class Pen { ... }        // نوع مرجع reference

تستخدم الكلمة المفتاحية struct في لغة البرمجة #C لتعريف النوع كقيمة value في حين تستخدم الكلمة المفتاحية class لتعريف النوع كمرجع reference.

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

الخطأ 2: عدم فهم القيم الافتراضية للمتغيرات غير المهيأة

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

لذا عليك التحقق دومًا من وجود قيمة افتراضية للمتغير غير المهيَّأ قبل استخدامه، كما في المثال التالي:

class Program {
          static Point point1;
          static Pen pen1;
          static void Main(string[] args) {
              Console.WriteLine(pen1 == null);      // True
              Console.WriteLine(point1 == null);    // False (لماذا؟)
          }
      }

لاحظ أنه لا يمكن إسناد null إلى point1 لأن point من النوع قيمة والقيمة الافتراضية لها هي (0,0) وليست null، إن التعرف على هذا الخطأ في #C أمر بسيط وسهل للغاية لأن معظم الأنواع (وليس كلها) من النوع قيمة تمتلك الخاصية IsEmpty التي تمكنك من التحقق فيما إذا كان الكائن يمتلك قيمة افتراضية أم لا.

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

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

الخطأ 3: استخدام توابع مقارنة النصوص بشكل خاطئ

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

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

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

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

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

  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));

بالتالي الطريقة العملية الآمنة للمقارنة بين النصوص هي تزويد التابع Equals بالمعامل comparisonType دائمًا، وفيما يلي بعض الإرشادات الأساسية:

  • استخدم المقارنة الحساسة للاختلافات بين اللغات عند مقارنة النصوص المدخلة من المستخدم أو التي ستعرض على المستخدم (CurrentCulture أو CurrentCultureIgnoreCase).

  • استخدم المقارنة الترتيبية عند مقارنة النصوص المسندة برمجيًا (Ordinal أو OrdinalIgnoreCase).

  • لا تستخدم المعاملين InvariantCulture و InvariantCultureIgnoreCase إلا في حالات محدودة جدًا لأن المقارنات الترتيبية في هذه الحالة أكثر فعالية.

ملاحظة: هناك تابع آخر لمقارنة النصوص بالإضافة إلى التابع Equals وهو التابع Compare الذي يقدم لك معلومات جيدة حول الترتيب النسبي للسلاسل النصية، وهو مناسب عند استخدام عوامل المقارنة < و=> و=<.

الخطأ 4: استخدام العبارات التكرارية بدل التصريحية لمعالجة المجموعات

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

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

على سبيل المثال إذا كان لدينا مصفوفة تخزن حسابات مستخدمين فيمكن استعراضها من خلال التعليمة foreach كما يلي:

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 أو استخدام العبارات التكرارية، هذا يعتمد على أداء البرنامج لذا يمكنك إجراء مقارنة لأداء الطريقتين واختيار الأنسب.

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

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

لاحظ الكود التالي:

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

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

قد يتبادر لذهنك سؤال آخر: ذكرنا أن العامل == يجري مقارنة حساسة لحالة الأحرف في السلاسل النصية، فلماذا قام هنا بمقارنة غير حساسة للأحرف؟ الجواب عندما تكون الكائنات التي تتعامل معها عبارات LINQ مرتبطة بجدول SQL مثل كائن DbSet في إطار عمل الكيانات Entity Framework، سيحوَّل الاستعلام إلى عبارة T-SQL وفي هذه الحالة يتبع العامل == قواعد لغة SQL وهي غير حساسة لحالة الأحرف، ولن يتبع قواعد #C. لذلك تصبح المقارنة غير حساسة لحالة الأحرف.

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

الخطأ 6: الغموض عند استخدام توابع التمديد

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

على سبيل المثال ستعمل الدالة البسيطة Sum في الكود التالي على إضافة الأرصدة إلى مجموعة من الحسابات كما يلي:

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

وسيط الدالة myAccounts في هذا الكود من النوع <IEnumerable<Account، وبما أن myAccounts يشير للتابع ()Sum فقد تتوقع أن التابع معرف ضمن الواجهة <IEnumerable<T، لكن تعريف الواجهة <IEnumerable<T لا يتضمن التابع Sum ويبدو كما يلي:

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

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

الجواب هو أن التابع ()Sum غير معرف ضمن الواجهة <IEnumerable<T وإنما هو تابع ثابت يدعى تابع التمديد Extension Method وهو معرّف ضمن الصنف System.Linq.Enumerable كما هو موضح تاليًا:

 namespace System.Linq {
        public static class Enumerable {
          ...
          // الإشارة إلى this IEnumerable<TSource> source
          //  هي التي توفر الوصول إلى تابع التمديد 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 لكونها تساعد في كتابة كود أسهل وأوضح وتحقق جمالية الصياغة syntactic sugar وتجعل الكود أسهل في القراءة والفهم. ولكن، إذا لم تكن معتادًا على استخدامها قد تشعر بالارتباك في البداية. لكن مع الوقت والخبرة ستعتاد استخدامها والتعامل معها فقد أصبح استخدام توابع التمديد شائعًا في مكتبات #C البرمجية، مثل مكتبة LINQ، ومحرك الألعاب Unity، وإطار Web API، وغيرها من المكتبات. وكلما كان الإطار البرمجي حديثًا، زادت احتمالية استخدامه لهذه التوابع.

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

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

الخطأ 7: اختيار نوع المجموعة غير المناسب للمهمة المطلوبة

توفر لغة البرمجة #C الكثير من المجموعات collection وفيما يلي بعض منها:

  • المصفوفة Array
  • المصفوفة الديناميكية ArrayList
  • جدول التعمية HashTable
  • القاموس <Dictionary<K,V
  • القائمة <List<T
  • المكدس Stack
  • الرتل Queue
  • القائمة المرتبة SortedList
  • القاموس النصي StringDictionary

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

ولتجنب الأخطاء وضمان أمان الكود، استخدم المجموعات المعممة Generic Interfaces في #C التي تستلزم تعيين نوع العناصر التي ستتعامل معها المجموعة عند التصريح عنها وتتعامل مع نوع محدّد بدلاً من المجموعات غير المعممة Non-generic Interfaces التي  لا تحدد نوع العناصر وتجعل عملية التحقق من صحة الأنواع صعبة على مصرف #C.

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

ولا تحاول كتابة كود خاص لإنشاء كائنات المجموعات بنفسك، فإطار العمل .NET يوفر بالفعل العديد من المجموعات الجاهزة التي توفر الكثير من الوقت والجهد كما تقدم مكتبة C5 في #C مجموعة من الأدوات وهياكل البيانات المتقدمة التي تسهل التعامل مع مجموعات البيانات مثل الأشجار الدائمة persistent trees وارتال الأولوية priority queues والقوائم المرتبطة linked lists وغيرها.. فهذه المجموعات تساعد في تنظيم البيانات وتسهل التعامل مع إضافتها وحذفها واسترجاعها.

الخطأ 8: إهمال تحرير الموارد غير المستخدمة

تستخدم بيئة تنفيذ CLR أداة كنس المهملات Garbage Collector لإدارة الذاكرة تلقائيًا، فلست بحاجة لتحرير الذاكرة التي حجزتها عند إنشاء الكائنات. وعلى عكس لغات البرمجة مثل لغة ++C التي تحتوي على العامل delete أو لغة C التي تستخدم التابع free لتحرير الذاكرة، لا تتضمن لغة #C شيئًا مشابهًا.

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

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

يحتوي إطار العمل .NET على واجهة باسم IDisposable تتضمن تابعًا واحدًا فقط يسمى Dispose()‎. وأي كائن ينفذ هذه الواجهة سينفذ تابع Dispose()‎ الذي يساعد في تحرير الموارد بفعالية. فعندما تريد إنشاء وتحرير كائن object في كتلة الكود نفسها استخدم التابع ()Dispose بشكل أساسي، ويمكنك استخدام العبارة using للتأكد من استدعاء ()Dispose بغض النظر عن الطريقة التي ستخرج فيها من الكود سواء بسبب وقوع استثناء أو بسبب عبارة return أو إغلاق كتلة الكود، والعبارة using المذكورة هنا هي نفس تلك المستخدمة لتضمين فضاء الأسماء namespace أعلى ملف #C، وتجدر الإشارة هنا بأن للعبارة using استخدامًا آخر لا يعرفه الكثير من المطورين وهو ضمان استدعاء التابع ()Dispose للكائن عند الخروج من كتلة الكود كما في المثال التالي:

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

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

الخطأ 9: تجنب الاستثناءات

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

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

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

// طريقة1:
      // سيرمى استثناء إذا لم نتمكن من تحويل account إلى  SavingsAccount
      SavingsAccount savingsAccount = (SavingsAccount)account;

// طريقة2:
      // لن يرمى استثناء في حال لم نتمكن من التحويل
      // سيتم تعيين savingsAccount إلى null بدلاً من ذلك
      SavingsAccount savingsAccount = account as SavingsAccount;

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

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

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

لاحظ المثال التالي حيث ستصدر الطريقة الأولى استثناء أما الطريقة الثانية فلا تصدره:

int.Parse();     // يرمي استثناء إذا لم يكن بالإمكان تحويل الوسيط
int.TryParse();  // يعيد قيمة من نوع bool للدلالة على ما إذا كان التحويل قد نجح

IEnumerable.First();     // يرمي استثناء إذا كانت السلسلة فارغة

IEnumerable.FirstOrDefault();     // يعيد null أو القيمة الافتراضية إذا كانت السلسلة فارغة

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

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

if (int.TryParse(myString, out myInt)) {
        // استعمل myInt
      } else {
        // استعمل القيمة الافتراضية
      }

بدلًا من الكود التالي الذي يطلق استثناء:

 try {
        myInt = int.Parse(myString);
        // استعمل myInt
      } catch (FormatException) {
        // استعمل القيمة الافتراضية
      }

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

الخطأ 10: السماح بتراكم تحذيرات المصرّف

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

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

class Account {

          int myId;
          int Id;   // يطلق المصرف تحذير هنا لكنك لم تنتبه له

          // الباني
          Account(int id) {
          this.myId = Id;     //  خطأ
          }

      }

يوجد خطأ كبير في الكود السابق بالرغم من أن مصرف اللغة وضعه كتحذير فقط وهو تعيين قيمة المتغير Id للمتغير myId باستخدام الأمر this.myId=Id والذي يجب أن يكون this.myId=id، إذا تجاهلت التحذير ستضيع الكثير من الوقت في تتبع مثل هذا الخطأ حسب درجة تعقيد البرنامج، أما إذا عالجته من البداية فستكتشف الخطأ بوقت قصير.

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

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

الخلاصة

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

ترجمة وبتصرف للمقال Buggy C# Code: The 10 Most Common Mistakes in C# Programming لكاتبه Patrick Ryder.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...