7 أخطاء خطيرة يسهل ارتكابها في C # /. NET

تم إعداد ترجمة للمقال قبل بدء الدورة "C # ASP.NET Core Developer" .




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

1. لا أفهم تأخير التنفيذ (الكسول)


أعتقد أن المطورين ذوي الخبرة على دراية بآلية .NET هذه ، ولكنها قد تفاجئ الزملاء الأقل دراية. باختصار ، لا يتم تنفيذ الأساليب / العوامل التي ترجع IEnumerable<T>وتستخدم yieldلإرجاع كل نتيجة في سطر الكود الذي يستدعيها بالفعل - يتم تنفيذها عند الوصول إلى المجموعة الناتجة بطريقة ما *. لاحظ أن معظم عبارات LINQ تُرجع نتائجها في النهاية مع الإنتاجية .

كمثال ، ضع في اعتبارك اختبار الوحدة الفظيع أدناه.

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Ensure_Null_Exception_Is_Thrown()
{
   var result = RepeatString5Times(null);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Ensure_Invalid_Operation_Exception_Is_Thrown()
{
   var result = RepeatString5Times("test");
   var firstItem = result.First();
}
private IEnumerable<string> RepeatString5Times(string toRepeat)
{
   if (toRepeat == null)
       throw new ArgumentNullException(nameof(toRepeat));
   for (int i = 0; i < 5; i++)
   {   
       if (i == 3)
            throw new InvalidOperationException("3 is a horrible number");
       yield return $"{toRepeat} - {i}";
   }
}

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

يعد التنفيذ المؤجل في الواقع آلية مثيرة للاهتمام ، خاصة لأنه يجعل من السهل ربط استعلامات LINQ ، واسترداد البيانات فقط عندما يكون الاستعلام الخاص بك جاهزًا للاستخدام.

2. , Dictionary ,


هذا مزعج بشكل خاص ، وأنا متأكد من أنه في مكان ما لدي كود يعتمد على هذا الافتراض. عند إضافة عناصر إلى القائمة List<T>، يتم تخزينها بنفس الترتيب الذي أضفتها به - منطقياً. في بعض الأحيان تحتاج إلى وجود كائن آخر مرتبط بعنصر في القائمة ، والحل الواضح هو استخدام قاموس Dictionary<TKey,TValue>يسمح لك بتحديد قيمة ذات صلة للمفتاح.

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

لتوضيح ذلك ، في المثال أدناه ، سيكون الناتج كما يلي:

الثالثة
الثانية


var dict = new Dictionary<string, object>();       
dict.Add("first", new object());
dict.Add("second", new object());
dict.Remove("first");
dict.Add("third", new object());
foreach (var entry in dict)
{
    Console.WriteLine(entry.Key);
}

لا تصدقني؟ تحقق هنا عبر الإنترنت بنفسك .

3. لا تأخذ في الاعتبار سلامة التدفق


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

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

على سبيل المثال ، ضع في اعتبارك كتلة التعليمات البرمجية هذه التي تستخدم مؤشر ترابط بسيط ولكنه غير آمن List<T>.

var items = new List<int>();
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
   tasks.Add(Task.Run(() => {
       for (int k = 0; k < 10000; k++)
       {
           items.Add(i);
       }
   }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(items.Count);

وبالتالي ، نضيف أرقامًا من 0 إلى 4 إلى القائمة 10000 مرة لكل منها ، مما يعني أن القائمة يجب أن تحتوي في النهاية على 50000 عنصر. هل علي أن؟ حسنًا ، هناك فرصة ضئيلة في النهاية ، ولكن فيما يلي نتائج 5 من عمليات الإطلاق المختلفة:

28191
23536
44346
40007
40476

يمكنك التحقق من ذلك بنفسك عبر الإنترنت هنا .

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

4. الإساءة كسول (مؤجل) التحميل في LINQ


يعد التحميل البطيء ميزة رائعة لكل من LINQ to SQL و LINQ to Entities (Entity Framework) ، والذي يسمح لك بتحميل صفوف الجدول ذات الصلة حسب الحاجة. في أحد مشروعاتي الأخرى ، لدي جدول "الوحدات النمطية" وجدول "النتائج" بعلاقة رأس بأطراف (يمكن أن يكون للوحدة نتائج عديدة).



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

using (var db = new DBEntities())
{
   var modules = db.Modules;
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

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

using (var db = new DBEntities())
{
   var modules = db.Modules.Include(b => b.Results);
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

5. لا تفهم كيف تترجم LINQ إلى SQL / Entity Framework الاستعلامات


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

لن تعمل معظم استدعاءات الطريقة.

لذا ، تخيل أن لديك الاستعلام أدناه لفصل اسم جميع الوحدات بنقطتين والتقاط الجزء الثاني.

var modules = from m in db.Modules
              select m.Name.Split(':')[1];

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

قد ينتج عن تلك النتائج نتائج غير متوقعة ...

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

int modules = db.Modules.Sum(a => a.ID);

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

int modules = db.Modules.ToList().Sum(a => a.ID);

صدمة ، رعب - سوف تفعل نفس الشيء بالضبط! ومع ذلك ، ماذا لو لم يكن لديك صفوف في جدول الوحدة؟ إرجاع LINQ إلى كائنات 0 ، وإصدار Entity Framework / LINQ إلى SQL إصدار InvalidOperationException ، الذي يقول أنه لا يمكن تحويل "int؟" في "int" ... مثل. هذا لأنه عند تنفيذ SUM في SQL لمجموعة فارغة ، يتم إرجاع NULL بدلاً من 0 - لذلك ، بدلاً من ذلك ، يحاول إرجاع int nullable. إليك بعض النصائح لحل هذه المشكلة إذا واجهت مثل هذه المشكلة .

اعرف متى تحتاج فقط إلى استخدام SQL القديم الجيد.

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

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

6. التقريب خاطئ


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

يتضمن .NET Framework طريقة ثابتة ممتازة في فئة الرياضيات تسمى Round ، والتي تأخذ قيمة رقمية وتقربها إلى المكان العشري المحدد. إنه يعمل بشكل مثالي في معظم الأوقات ، ولكن ماذا تفعل عندما تحاول تقريب 2.25 إلى أول علامة عشرية؟ أفترض أنك ربما تتوقع أن تصل إلى 2.3 - هذا ما اعتدنا عليه جميعًا ، أليس كذلك؟ حسنًا ، من الناحية العملية ، اتضح أن .NET يستخدم تقريب المصرفيينالذي تقريب المثال المعطى إلى 2.2! ويرجع ذلك إلى حقيقة أن المصرفيين يتم تقريبهم إلى أقرب رقم زوجي إذا كان الرقم في "نقطة المنتصف". لحسن الحظ ، يمكن تجاوز هذا بسهولة في طريقة Math.Round.

Math.Round(2.25,1, MidpointRounding.AwayFromZero)

7. الطبقة الرهيبة "DBNull"


يمكن أن يسبب هذا ذكريات غير سارة للبعض - يخفي ORM هذه القذارة منا ، ولكن إذا كنت تتعمق في عالم ADO.NET (SqlDataReader وما شابه) ستقابل DBNull.Value.

أنا لست متأكدا 100٪ من السبب لماذاتتم معالجة قيم NULL من قاعدة البيانات على النحو التالي (يرجى التعليق أدناه إذا كنت تعرف!) ، لكن Microsoft قررت أن تقدم لهم نوعًا خاصًا من DBNull (مع قيمة حقل ثابتة). يمكنني إعطاء إحدى مزايا هذا - لن تحصل على أي NullReferenceException غير سارة عند الوصول إلى حقل قاعدة بيانات NULL. ومع ذلك ، يجب ألا تدعم فقط الطريقة الثانوية للتحقق من قيم NULL (التي يسهل نسيانها ، والتي يمكن أن تؤدي إلى أخطاء خطيرة) ، ولكنك تفقد أيًا من الميزات الرائعة لـ C # التي تساعدك على العمل مع القيم الخالية. ما يمكن أن يكون بهذه البساطة

reader.GetString(0) ?? "NULL";

ما سيصبح في النهاية ...

reader.GetString(0) != DBNull.Value ? reader.GetString(0) : "NULL";

قرف.

ملحوظة


هذه ليست سوى بعض "المجرفات" غير العادية التي واجهتها في .NET - إذا كنت تعرف المزيد ، أود أن أسمع منك أدناه.



ASP.NET Core:



All Articles