> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أخطاء شائعة وأسلوب C# الحديث Modern C#
- URL: https://arabroot.io/articles/اخطاء-شائعة-واسلوب-csharp-الحديث/
- Published: 2026-10-31T08:11:58.000Z
- Updated: 2026-10-31T09:49:52.000Z
- Description: أربعة عشر خطأ يحملها المطورون من Python وJavaScript وJava إلى C#، كل خطأ بتشغيل حقيقي فاشل ثم الإصلاح، وبعدها الأسلوب الحديث وخطة قراءة من المصادر الرسمية.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

بعد شهر من كتابة C# ستلاحظ أن الكود يعمل والاختبارات تنجح، ومع ذلك تعود طلبات الدمج Pull Requests بتعليقات من نوع واحد: «هذا JavaScript بصيغة C#»، أو «في Java كنا نكتبها هكذا، أما هنا فلا». والتعليقات ليست ذوقاً شخصياً للمراجع، فأغلبها عن أخطاء حقيقية لا يكشفها المترجم: سعر 9.90 ريال يصير 990 على سيرفر إعداداته ألمانية، وقائمة تبقى نصف معدلة بعد استثناء، وطلب بتوقيت السعودية يحفظ بساعة خاطئة، ودالة `async void` تسقط الخدمة كلها. وهذه أخطاء لا يرتكبها من يجهل البرمجة، وإنما من يعرف لغة أخرى جيداً، فيحمل معه عاداتها إلى لغة تتصرف بشكل مختلف.

وهذا هو المقال الخامس والأخير في مرحلة [لغة C# للمطورين](https://arabroot.io/start/#stage-lang-csharp)، بعد [الأدوات والأساسيات](https://arabroot.io/articles/csharp-%D9%84%D9%85%D9%86-%D9%8A%D8%B9%D8%B1%D9%81-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA/) و[الأنواع والذاكرة](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%88%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%AB%D9%86%D8%A7%D8%A1%D8%A7%D8%AA-%D9%81%D9%8A-csharp/) و[التزامن](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%81%D9%8A-csharp-async-await-%D9%88%D8%A7%D9%84%D9%85%D9%87%D8%A7%D9%85-tasks/) و[الخدمة الحقيقية](https://arabroot.io/articles/%D8%AE%D8%AF%D9%85%D8%A9-%D8%AD%D9%82%D9%8A%D9%82%D9%8A%D8%A9-%D8%A8-csharp-asp-net-core-%D9%88-postgresql/). وفيه أربعة عشر خطأ يقع فيها القادمون من Python وJavaScript وJava، وكل خطأ معه تشغيل حقيقي خاطئ بمخرجه كما ظهر، ثم الإصلاح بأسلوب C# الصحيح. وكل الأمثلة في برنامج واحد في مجلد `lab`، شغلناه بـ .NET 10.0.14 في Container بنواتين، ثم نقارن نفس الميزة مكتوبة بأسلوب 2012 وبأسلوب C# الحديث، ونختم بخطة قراءة من المصادر الرسمية لمن يريد أن يكمل.

وسوف نناقش في هذا المقال ما يلي:

- النصوص Strings والثقافة Culture: الجمع في حلقة، والتحويل على سيرفر بإعدادات مختلفة، والمقارنة دون حساسية للحالة.
- المجموعات Collections وLINQ: التنفيذ المؤجل، والتعديل أثناء المرور، و`First` على قائمة فارغة.
- الأنواع Types: نسخة struct من قائمة، وسجل فيه قائمة، وتعداد يقبل أي رقم.
- الدوال Functions والأخطاء: متغير الحلقة في Lambda، و`throw ex`، و`async void`.
- الوقت Time والأرقام: `DateTime` والمناطق الزمنية، والتقريب الافتراضي.
- أسلوب C# الحديث Modern C#: نفس الميزة بكود 2012 وبكود اليوم، وكيف تفرض الأسلوب على الفريق.
- قائمة مراجعة للكود، وخطة قراءة من المصادر الرسمية.

## لماذا يحمل المطور عاداته معه؟ Language Transfer

قبل أن ندخل في الأخطاء نفسها، من المفيد أن تعرف من أين يأتي كل واحد منها، لأن هذا يخبرك أين تنظر في كودك أنت. فكل لغة تعلمك افتراضات صامتة لا تكتبها في أي مكان: في JavaScript كل الأرقام من نوع واحد، والنص يجمع بـ `+` دون تفكير، و`let` في الحلقة تعطي كل دورة متغيرها. وفي Python المجموعات تقارن بالمحتوى، والتاريخ عادة بلا منطقة زمنية حتى تطلبها، والخطأ يلتقط بـ `try` في أي مكان. وفي Java التعداد مجموعة مغلقة من الكائنات، و`throw e` لا تمحو الـ Stack Trace، والنصوص تقارن بـ `equals` ولا يفكر أحد في الثقافة. وكل افتراض من هذه صحيح في لغته، فإذا انتقل معك إلى C# كما هو صار خطأ لا يراه المترجم، لأن الكود صحيح نحوياً ويفعل شيئاً آخر غير ما تقصده.

| تأتي من    | العادة التي تنقلها                                                                  | الأخطاء في هذا المقال |
| ---------- | ----------------------------------------------------------------------------------- | --------------------- |
| JavaScript | رقم واحد لكل شيء، + للنصوص، let في الحلقات، دوال async بلا قيمة                     | 1، 10، 12، 14         |
| Python     | مجموعات تقارن بالمحتوى، items\[0\]، الحذف من القائمة أثناء المرور، تواريخ بلا منطقة | 5، 6، 8، 13           |
| Java       | تعداد مغلق، throw e، النص كقيمة دون ثقافة، كل شيء كلاس                              | 2، 3، 9، 11           |
| Go أو C    | struct يعدل في مكانه عبر المؤشر                                                     | 7                     |
| أي لغة     | الاستعلام قائمة وليس وصفاً                                                          | 4                     |

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

## التجربة Lab: برنامج واحد بأربعة عشر قسماً

البرنامج في الملف `lab/Program.cs`، وكل خطأ قسم مستقل فيه الطريقة الخاطئة ثم الإصلاح، ويشغله السكربت `lab/run.sh` داخل Container من الـ SDK:

```bash
docker run --rm --cpus=2 --memory=1g -v "$PWD/lab":/lab \
  mcr.microsoft.com/dotnet/sdk:10.0 bash /lab/run.sh
```

والمشكلة الوحيدة التي واجهناها في التجربة كانت في البرنامج نفسه، وهي درس بحد ذاتها: في النسخة الأولى كان القسم الأخير، وهو قسم `async void`، جزءاً من التشغيل الكامل `all`، فأسقط العملية كلها في منتصف التشغيل، وظهر في آخر المخرج سطر `Aborted (core dumped)` بدل نهاية طبيعية، فنقلناه إلى عملية منفصلة. وسوف ترى في القسم الخاص به لماذا لا يمكن «التقاط» هذا الخطأ أصلاً.

## النصوص Strings والثقافة Culture

### الخطأ 1: جمع النصوص في حلقة String Concatenation

القادم من Python أو JavaScript يبني الإيصال سطراً سطراً بـ `+=`، لأن محركات تلك اللغات تحسن هذه الحالة أحياناً. أما في C# فالنص `string` غير قابل للتعديل Immutable، فكل `+=` تنشئ نصاً جديداً وتنسخ فيه كل ما قبله، فيصير بناء النص من عشرين ألف سطر عملاً تربيعياً. والمقارنة بين الطريقتين لعشرين ألف سطر:

```csharp
string s = "";
for (int i = 0; i < 20_000; i++) s += $"line {i}: 9.90 SAR\n";              // wrong

var sb = new StringBuilder();
for (int i = 0; i < 20_000; i++) sb.Append("line ").Append(i).Append(": 9.90 SAR\n");
string s2 = sb.ToString();                                                    // fix
```

والمخرج سوف يكون كما يلي:

```text
=== concat: 20,000 receipt lines ===
  string +=      10371 ms,  7686 MB allocated, length 408,890
  StringBuilder      1 ms,     1 MB allocated, same: True
```

نص نهائي طوله 408 آلاف حرف، احتاج مع `+=` أكثر من عشر ثوان وخصص 7.6 جيجابايت من الذاكرة على مراحل، لأن كل دورة نسخت النص كله، بينما [StringBuilder](https://learn.microsoft.com/en-us/dotnet/standard/base-types/stringbuilder?ref=arabroot.io) أخذ ملي ثانية واحدة وميجابايت واحداً، لأنه يكتب في مخزن Buffer يكبر عند الحاجة. والقاعدة: الجمع العادي أو النص المدمج `$"..."` لعدد ثابت من الأجزاء، و`StringBuilder` أو `string.Join` عندما يكون الجمع داخل حلقة.

وقد يتساءل البعض: لماذا لا يحول المترجم `+=` في الحلقة إلى `StringBuilder` تلقائياً؟ والإجابة أن المترجم يحسن ما يستطيع إثباته في سطر واحد، فالجمع `a + b + c + d` في سطر واحد يصير استدعاء واحداً لـ `string.Concat` بأربعة معاملات، والنص المدمج `$"line {i}: ..."` يبنى بأداة داخلية تكتب في مخزن واحد دون نصوص وسيطة. ولكن الحلقة شيء آخر، فقيمة `s` بعد كل دورة نص كامل قد يقرؤه أي كود آخر في نفس اللحظة، والمترجم لا يستطيع أن يفترض أن أحداً لن يقرأه في المنتصف، فيبقى النسخ في كل دورة كما كتبته.

### الخطأ 2: التحويل Parsing والتنسيق بثقافة السيرفر

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

```csharp
foreach (var name in new[] { "en-US", "de-DE", "ar-SA" })
{
    CultureInfo.CurrentCulture = new CultureInfo(name);
    bool ok = double.TryParse("9.90", out var price);
    Console.WriteLine($"{name}: ... ok={ok} value={price.ToString(CultureInfo.InvariantCulture)} ToString(\"N2\")={9.90.ToString("N2")} date={new DateTime(2026, 10, 31):d}");
}
```

والمخرج سوف يكون كما يلي:

```text
=== culture: the same price text on servers with different cultures ===
  en-US: double.Parse("9.90") ok=True value=9.9  ToString("N2")=9.90  date=10/31/2026
  de-DE: double.Parse("9.90") ok=True value=990  ToString("N2")=9,90  date=31.10.2026
  ar-SA: double.Parse("9.90") ok=False value=0  ToString("N2")=9٫90  date=20‏‏/5‏‏/1448 بعد الهجرة
  fix (de-DE + InvariantCulture): 9.9
```

في المخرج أعلاه لاحظ التالي:

- في الثقافة الألمانية النقطة فاصل الآلاف والفاصلة هي الفاصل العشري، فقرأ التحويل «9.90» على أنها تسعمئة وتسعون، ونجح دون أي خطأ، وهذا أسوأ احتمال: سعر أكبر بمئة مرة يمر إلى الفاتورة.
- في الثقافة العربية السعودية فشل التحويل كله، والتنسيق كتب الفاصل العشري العربي «٫»، والتاريخ ظهر بالتقويم الهجري، لأن التقويم الافتراضي لهذه الثقافة في .NET هو أم القرى. وهذا جميل في واجهة المستخدم، وكارثة في ملف CSV يقرؤه برنامج آخر.
- الإصلاح هو `CultureInfo.InvariantCulture`، وهي ثقافة ثابتة لا تتغير بإعدادات السيرفر، فأعطت 9.9 حتى مع الثقافة الألمانية.

ولماذا لا يظهر هذا الخطأ إلا في الإنتاج؟ لأن أجهزة المطورين وأجهزة CI تعمل في الغالب بثقافة en-US أو بالثقافة الثابتة، وفي الاثنتين أعطى نفس التحويل 9.9 في المخرج أعلاه، ولم يتغير شيء حتى غيرنا `CurrentCulture` بأنفسنا. فالاختبارات تنجح، والمراجعة لا ترى شيئاً، ثم ينقل أحدهم الخدمة إلى سيرفر إعداداته مختلفة، أو يضيف متغير البيئة `LANG` لسبب لا علاقة له بالكود، فتتغير الأسعار. ولاحظ من المخرج نفسه أن الثقافة الألمانية لم ترم أي خطأ، وإنما أعطت 990 بنجاح، فلا يوجد استثناء في السجلات يدلك على المشكلة، وإنما فاتورة أكبر بمئة مرة. والعلامة الوحيدة التي تراها هي البيانات نفسها، ولذلك أسرع طريقة لاكتشافه قبل العميل هي أن تشغل الاختبارات مرة بثقافة غير ثقافة جهازك، كما في قسم الاختبارات أدناه، أو أن تفعل القاعدة CA1305 التي أمسكت `ToString("N2")` في التجربة، مع العلم أنها لم تمسك `TryParse` نفسها كما سوف نرى.

والقاعدة التي تشرحها [صفحة العولمة Globalization](https://learn.microsoft.com/en-us/dotnet/core/extensions/globalization?ref=arabroot.io): كل ما يقرؤه أو يكتبه برنامج آخر، مثل JSON وCSV والسجلات ومعاملات الروابط، يستعمل `InvariantCulture` دائماً، وكل ما يقرؤه إنسان يستعمل ثقافة هذا الإنسان بالتحديد، وليس ثقافة السيرفر. ولهذا أيضاً تجد في كثير من خدمات الويب الخيار `InvariantGlobalization` في ملف المشروع، كما في الخدمة التي بنيناها في المقال الرابع، فيعمل البرنامج كله بالثقافة الثابتة ويستغني عن مكتبة ICU في الـ Image.

### الخطأ 3: المقارنة دون حساسية للحالة بـ ToUpper

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

```text
=== upper: comparing a file name, Turkish culture ===
  "invoice".ToUpper() = İNVOİCE  == "INVOICE"? False
  string.Equals(..., OrdinalIgnoreCase) = True
```

ففي اللغة التركية الحرف `i` الصغير يقابله `İ` كبير بنقطة، فصارت «invoice» بعد التحويل «İNVOİCE» ولم تساو «INVOICE». وهذه المشكلة معروفة باسم مشكلة الحرف التركي، وأصابت برامج كثيرة في فحوص الصلاحيات وأسماء الملفات. والإصلاح أن تقول للمقارنة صراحة كيف تقارن: `StringComparison.OrdinalIgnoreCase` للمعرفات وأسماء الملفات والمفاتيح، وهي مقارنة بالبايتات لا تتأثر بأي ثقافة وأسرع أيضاً، والتفاصيل في [أفضل ممارسات مقارنة النصوص](https://learn.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings?ref=arabroot.io). وفي القواميس تمرر نفس الخيار: `new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)`.

![أربعة صناديق لثقافات en-US وde-DE وar-SA وtr-TR: في en-US يعطي double.Parse لـ 9.90 القيمة 9.9 والتنسيق 9.90 والتاريخ 10/31/2026، وفي de-DE يعطي 990 لأن النقطة فاصل آلاف والتنسيق 9,90 والتاريخ 31.10.2026، وفي ar-SA يفشل TryParse والتنسيق 9٫90 والتاريخ 20/5/1448 بعد الهجرة، وفي tr-TR يصير invoice بعد ToUpper حروفاً بنقطة فلا تساوي INVOICE، وتحتها الإصلاحان InvariantCulture للبيانات وOrdinalIgnoreCase للمقارنة](https://arabroot.io/content/images/2026/10/csharp-idioms-02-culture.webp)

نفس السطر بأربع ثقافات: سعر بمئة ضعف، وتحويل فاشل، وتاريخ هجري، ومقارنة خاطئة

## المجموعات Collections مع LINQ

### الخطأ 4: نسيان أن LINQ مؤجلة التنفيذ Deferred Execution

ذكرنا في المقال الأول أن الاستعلام في LINQ وصف وليس نتيجة، وهنا نرى ثمنه. المتغير `withVat` في المثال التالي يحسب السعر مع الضريبة، وفيه عداد لعدد مرات تنفيذ الحساب:

```csharp
var withVat = prices.Select(p => { calls++; return (p * 115 + 50) / 100; });
Console.WriteLine($"Count={withVat.Count()}, Sum={withVat.Sum()}, Max={withVat.Max()} ...");
```

والمخرج سوف يكون كما يلي:

```text
=== deferred: LINQ runs every time you enumerate it ===
  Count=3, Sum=21149, Max=14835 -> selector ran 9 times for 3 items
  with ToList(): Count=3, Sum=21149, Max=14835 -> selector ran 3 times
```

ثلاثة عناصر وتسعة حسابات، لأن كل من `Count` و`Sum` و`Max` مر على الاستعلام من بدايته. وفي هذا المثال الثمن صغير، ولكن عندما يكون المصدر استعلام Entity Framework على قاعدة البيانات، أو قراءة ملف، أو استدعاء API في الدالة، يصير ثلاثة استعلامات أو ثلاث قراءات بدل واحدة، وقد تختلف نتائجها إذا تغيرت البيانات بينها. والإصلاح أن تحول الاستعلام إلى قائمة بـ `ToList()` أو `ToArray()` مرة واحدة عندما تريد أن تمر عليه أكثر من مرة، كما في السطر الثاني. والعكس أيضاً خطأ: لا تضع `ToList()` بعد كل سطر LINQ «احتياطاً»، فهذا ينشئ قوائم لا يحتاجها أحد، وإنما عندما تعرف أنك سوف تمر على النتيجة أكثر من مرة.

والحالة الأخطر هي `IQueryable` في Entity Framework Core، لأن الاستعلام هناك لا يعمل في الذاكرة أصلاً، وإنما يترجم إلى SQL ويرسل إلى قاعدة البيانات في كل مرة تمر عليه. فالمتغير الذي يبدو قائمة طلبات العميل، إذا مررت عليه مرة لتعدها ومرة لتعرضها، أرسل استعلامين، وفي صفحة فيها حلقة على العملاء يصير هذا مشكلة N+1 التي ذكرناها في مقال تصميم الأنظمة. ولهذا تحذر Microsoft من التعداد المتعدد بقاعدة تحليل خاصة هي CA1851، ولكنها غير مفعلة افتراضياً، ولم تظهر حتى مع `AnalysisMode=All` في تجربتنا كما سوف نرى.

### الخطأ 5: الحذف من قائمة أثناء المرور عليها Modifying While Iterating

المهمة: احذف المنتجات التي نفد مخزونها من القائمة. والطريقة البديهية من Python القديمة أو JavaScript هي المرور بـ `foreach` والحذف عند الشرط:

```csharp
var stock = new List<(string Name, int Qty)> { ("coffee", 40), ("dates", 0), ("dallah", 0) };
foreach (var p in stock) if (p.Qty == 0) stock.Remove(p);      // wrong
int removed = stock.RemoveAll(p => p.Qty == 0);                  // fix
```

والمخرج سوف يكون كما يلي:

```text
=== modify: remove sold-out products inside foreach ===
  InvalidOperationException: Collection was modified; enumeration operation may not execute.
  fix RemoveAll: removed 1, left coffee
```

الاستثناء متوقع، فـ .NET تكتشف أن القائمة تغيرت أثناء المرور وترفض أن تكمل. ولكن انظر للسطر الثاني: `RemoveAll` حذفت عنصراً واحداً فقط وليس اثنين، والسبب أن الحلقة الخاطئة حذفت «dates» فعلاً قبل أن يرمي الاستثناء في الدورة التالية. فالقائمة بقيت نصف معدلة، وإذا كان هذا الكود داخل `try` يبتلع الاستثناء، فسوف تستمر الخدمة ببيانات لا تطابق أي حالة متوقعة. والإصلاح [RemoveAll](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.removeall?ref=arabroot.io) بشرط، أو بناء قائمة جديدة بـ `Where`، أو المرور بحلقة `for` من الآخر إلى الأول.

### الخطأ 6: ‏First على مجموعة فارغة Empty Sequence

القادم من Python معتاد على `items[0]` التي ترمي استثناء على القائمة الفارغة، فيكتب `First()` في C# بنفس العقلية، والمخرج على قائمة فارغة:

```text
=== first: the cheapest product in an empty category ===
  InvalidOperationException: Sequence contains no elements
  FirstOrDefault on value tuples = (null, 0)  <- a default, not null
  FirstOrDefault on strings = null
```

رسالة الاستثناء لا تقول أي مجموعة كانت فارغة، وفي سجلات الإنتاج تراها بلا سياق. والبديل `FirstOrDefault`، ولكن انتبه للسطر الثاني: على نوع قيمة مثل الصف Tuple لا ترجع null، وإنما القيمة الافتراضية للنوع، أي اسم فارغ وسعر صفر، فإذا لم تفحص فقد يظهر في الصفحة «أرخص منتج: صفر ريال». والطريقة الصحيحة أن تقرر ماذا تعني المجموعة الفارغة في هذا المكان، ثم تكتبها: `FirstOrDefault()` مع فحص `is null` للمراجع، أو `FirstOrDefault(defaultValue)` بقيمة تختارها، أو `Any()` قبل أن تطلب العنصر.

![لوحتان: عشرون ألف سطر في إيصال حيث string += أخذ 10,371 ms وخصص 7,686 MB لنص طوله 408,890 حرفاً وStringBuilder أخذ 1 ms و1 MB، لأن النص لا يتغير وكل += تنسخ ما قبلها، واستعلام LINQ بدون ToList نفذ الحساب 9 مرات لثلاثة عناصر ومع ToList ثلاث مرات](https://arabroot.io/content/images/2026/10/csharp-idioms-01-hidden-cost.webp)

ثمن لا يظهر في الكود: نسخ النص في كل دورة، واستعلام يعاد في كل مرور

## الأنواع Types: القيم والسجلات والتعدادات

### الخطأ 7: تعديل struct أخذته من قائمة

رأينا في مقال الأنواع والذاكرة أن الـ struct ينسخ في كل إسناد، وهذا يظهر في أكثر مكان مفاجئ: عنصر من قائمة. المثال سلة فيها سطر واحد من نوع struct، ونريد أن نطبق خصماً عليه:

```text
=== struct: discount on an item from List<struct> ===
  after first.Price = 500: cart[0].Price = 990
  after cart[0] = first:   cart[0].Price = 500
  record with: original 990, new 500
```

السطر `var first = cart[0];` نسخ العنصر، والتعديل `first.Price = 500` عدل النسخة، فبقي السعر في السلة 990، ولم يظهر أي خطأ أو تحذير. ولو كتبت `cart[0].Price = 500` مباشرة لرفضه المترجم بالخطأ CS1612، ولكن الشكل الذي فيه متغير وسيط يمر. والإصلاح الأول أن تكتب النسخة المعدلة في القائمة، كما في السطر الثاني، والإصلاح الأفضل ألا يكون الـ struct قابلاً للتعديل أصلاً، وأن تستعمل `record` مع `with`، فيكون واضحاً من الكود أنك أنشأت قيمة جديدة ولم تعدل القديمة، كما في السطر الثالث. وإذا كان النوع يجب أن يبقى struct لأسباب الأداء، فاجعله `readonly struct`، فيرفض المترجم أي تعديل على حقوله بعد الإنشاء، ويصير السطر الخاطئ `first.Price = 500` خطأ بناء بدل خطأ صامت، وتصير الطريقة الوحيدة للتغيير أن تنشئ قيمة جديدة وتضعها في مكانها بنفسك.

### الخطأ 8: سجل record فيه قائمة

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

```text
=== record: equality with a List inside ===
  same content, a == b: False
  with SequenceEqual in Equals: c == d: True
```

طلبان بنفس الرقم ونفس المنتجات غير متساويين، لأن السجل يقارن حقوله بمقارنة كل حقل الافتراضية، والحقل هنا `List<int>`، وهو كلاس يقارن بالمرجع، والقائمتان كائنان مختلفان. فالمقارنة بالقيمة في السجلات سطحية Shallow، وليست عميقة كما في Kotlin data class مع القوائم مثلاً. والإصلاح إما أن تكتب `Equals` بنفسك كما في السجل الثاني في التجربة، مع `SequenceEqual` و`GetHashCode` متوافقة، أو أن تتجنب المقارنة على هذه السجلات وتقارن بمفتاح واضح مثل رقم الطلب.

### الخطأ 9: التعداد Enum يقبل أي رقم

القادم من Java يظن أن `enum` في C# مجموعة مغلقة من القيم كما في Java، ولكنه في C# اسم لعدد صحيح فقط، والمخرج التالي يبين ما يعنيه ذلك عندما يرسل عميل قديم حالة طلب غير معروفة:

```text
=== enum: a status from an old client ===
  (OrderStatus)42 = 42, defined? False
  Enum.TryParse("Shipped") = True Shipped
  Enum.TryParse("42")      = True 42  <- also true
```

التحويل `(OrderStatus)42` نجح دون أي خطأ، مع أن التعداد فيه ثلاث قيم فقط هي 1 و2 و3، والأغرب أن `Enum.TryParse` نفسها قبلت النص «42» ورجعت `true`، لأنها تقبل الأسماء والأرقام معاً. وهذه القيمة سوف تمر إلى `switch` لا يغطيها، أو تحفظ في قاعدة البيانات. والإصلاح أن تفحص كل قيمة تأتي من خارج البرنامج بـ [Enum.IsDefined](https://learn.microsoft.com/en-us/dotnet/api/system.enum.isdefined?ref=arabroot.io) قبل أن تقبلها، وأن تضع في كل `switch` على تعداد فرعاً أخيراً يرمي استثناء للقيمة غير المعروفة، بدل أن يفترض أن الفروع تغطي كل شيء.

والمترجم يساعدك هنا قليلاً: إذا كتبت تعبير `switch` على تعداد وغطيت كل الأسماء المعرفة دون فرع `_` أخير، فإنه يحذرك بالتحذير CS8524 بأن القيم غير المسماة، مثل 42، لا يغطيها أي فرع، وأن التعبير سوف يرمي `SwitchExpressionException` عند وصولها. وهذا التحذير بالذات يستحق أن يكون خطأ في ملف المشروع، لأنه يجبر كل من يكتب `switch` على تعداد أن يقرر صراحة ماذا يحدث للقيمة غير المعروفة.

![أربعة صناديق بمخرج التجربة: الحذف داخل foreach رمى InvalidOperationException بعد أن حذف dates فوجد RemoveAll عنصراً واحداً، وتعديل struct من قائمة غير نسخة فبقي السعر 990 حتى كتبناها في القائمة، وLambda داخل for طبعت 3,3,3 وforeach طبعت 0,1,2، وthrow ex أظهر Wrapper وRethrow فقط بينما throw أظهر LoadPrice أيضاً](https://arabroot.io/content/images/2026/10/csharp-idioms-03-traps.webp)

أربعة فخاخ طبع البرنامج نتيجتها الخاطئة دون أن يعترض المترجم

## الدوال Functions والأخطاء Errors

### الخطأ 10: التقاط متغير حلقة for في Lambda

عندما تنشئ دوال Lambda داخل حلقة، كأن تسجل ثلاث مهام لثلاثة منتجات، فالسؤال: أي قيمة للمتغير سوف تراها كل دالة؟ والمخرج:

```text
=== closure: three callbacks created in a for loop ===
  for:     3,3,3
  foreach: 0,1,2
  for with a copy: 0,1,2
```

في حلقة `for` يوجد متغير واحد `i` للحلقة كلها، والدوال الثلاث التقطت هذا المتغير نفسه وليس قيمته، فعندما استدعيناها بعد الحلقة كانت قيمته 3، فطبعت الثلاث 3\. أما `foreach` فتنشئ متغيراً جديداً في كل دورة منذ C# 5، كما تذكر [صفحة جمل التكرار](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/iteration-statements?ref=arabroot.io)، فالتقطت كل دالة متغيرها. وهذا نفس سلوك `var` مقابل `let` في JavaScript القديمة، ولكن هنا الحلقتان في نفس اللغة تتصرفان بشكل مختلف. والإصلاح نسخ المتغير داخل الحلقة `int copy = i;`، أو استعمال `foreach`.

### الخطأ 11: ‏throw ex بدل throw

عندما تلتقط استثناء لتسجله ثم تعيد رميه، فالكلمة الصحيحة `throw;` وحدها، أما `throw ex;` التي تبدو منطقية للقادم من Java فتمحو مكان الخطأ الأصلي. والتجربة التالية ترمي استثناء من `LoadPrice`، وتلتقطه `Wrapper` وتعيد رميه بالطريقتين، ثم تطبع الإطارات في الـ Stack Trace:

```text
=== rethrow: where did the error happen? ===
  throw ex -> <<Main>$>g__Wrapper|0_11:144 <- <<Main>$>g__Rethrow|0_10:131
  throw    -> <<Main>$>g__LoadPrice|0_12:148 <- <<Main>$>g__Wrapper|0_11:141 <- <<Main>$>g__Rethrow|0_10:131
```

مع `throw ex` يبدأ الـ Stack Trace من السطر 144، وهو سطر إعادة الرمي، والدالة `LoadPrice` التي حدث فيها الخطأ فعلاً اختفت. ومع `throw` بقي كل شيء، بدءاً من السطر 148 في `LoadPrice`. وفي حادثة حقيقية الفرق بين الحالتين هو الفرق بين أن تعرف السبب من السجل في دقيقة، أو أن تعيد إنتاج الخطأ ساعة. وإذا أردت أن تضيف سياقاً فارم استثناء جديداً وضع القديم داخله: `throw new PriceException("...", ex);`، فيبقى الأصل كاملاً في `InnerException`. وفي الحالات النادرة التي تحتاج فيها أن تحفظ الاستثناء ثم ترميه لاحقاً من مكان آخر، مثل خيط ينقل خطأ إلى خيط آخر، فالأداة هي `ExceptionDispatchInfo.Capture(ex).Throw()`، وهي تحفظ الـ Stack Trace الأصلي وتضيف إليه مكان الرمي الجديد، وهذا ما تفعله `await` نفسها خلف الكواليس عندما تعيد رمي استثناء المهمة في مكان الاستدعاء.

### الخطأ 12: ‏async void يسقط العملية Process Crash

القادم من JavaScript يكتب دالة غير متزامنة لا ترجع شيئاً، فيضع لها `async void` لأن «void» تعني لا شيء. والتجربة التالية تقارن دالتين متطابقتين ترمي كل منهما استثناء بعد الانتظار، واحدة `async Task` والثانية `async void`، وكل منهما داخل `try` و`catch`:

```csharp
try { await SaveTask(); } catch (Exception e) { Console.WriteLine($"  async Task: caught {e.Message}"); }
try { SaveVoid(); }       catch (Exception e) { Console.WriteLine($"  async void: caught {e.Message}"); }
await Task.Delay(200);
Console.WriteLine("  still running?");
```

والمخرج سوف يكون كما يلي:

```text
=== asyncvoid: an exception in async Task vs async void ===
  async Task: caught disk full (Task)
Unhandled exception. System.IO.IOException: disk full (void)
   at Program.<<Main>$>g__SaveVoid|0_20() in /w/Program.cs:line 215
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__124_1(Object state)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
   at System.Threading.Thread.StartCallback()
  still running?
/lab/run.sh: line 11:    70 Aborted                 (core dumped) dotnet /out/Idioms.dll asyncvoid
  exit code: 134
```

استثناء `async Task` التقطته `catch` كما يجب، لأن `await` أعادت رميه في مكان الاستدعاء. أما `async void` فلا يوجد مهمة يرجع فيها الاستثناء، فرماه الـ Runtime على خيط من المجمع لا يوجد فوقه أي `catch`، فصار استثناء غير معالج أسقط العملية كلها بالرمز 134\. ولاحظ أن «still running?» طبعت لأن الخيط الرئيسي كان ما زال يعمل في لحظة السقوط، ثم انتهى كل شيء. وفي خدمة ويب يعني هذا أن خطأ واحداً في دالة `async void` يسقط الخدمة لكل المستخدمين. والقاعدة كما في [صفحة سيناريوهات async](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios?ref=arabroot.io): الدالة غير المتزامنة ترجع `Task` دائماً، و`async void` فقط لمعالجات الأحداث في تطبيقات الواجهات حيث يفرضها التوقيع.

## الوقت Time والأرقام Numbers

### الخطأ 13: ‏DateTime والمناطق الزمنية Time Zones

عميل في السعودية أرسل طلباً الساعة العاشرة صباحاً بتوقيته، والتطبيق أرسل الوقت بصيغة ISO 8601 مع الفرق `+03:00`، والسيرفر في Container يعمل بتوقيت UTC كأغلب السيرفرات:

```text
=== dates: an order placed at 10:00 Saudi time (+03:00), server in UTC ===
  TimeZoneInfo.Local = UTC
  DateTime.Parse       -> 2026-10-31 07:00 Kind=Local
  DateTimeOffset.Parse -> 2026-10-31 10:00 +03:00, UTC 07:00
  DateTime.Now.Kind=Local, DateTime.UtcNow.Kind=Utc
```

`DateTime.Parse` قرأ الفرق الزمني، ثم حول الوقت إلى توقيت السيرفر المحلي وتخلص من الفرق، فصار الطلب «الساعة 7:00» بنوع `Local`، أي «محلي» بمعنى توقيت السيرفر الذي لا يعرف عنه العميل شيئاً. وإذا نقلت الخدمة إلى سيرفر آخر بتوقيت مختلف تغير معنى كل التواريخ المحفوظة. أما `DateTimeOffset` فاحتفظ بالوقت والفرق معاً، 10:00 بفرق +03:00، ويعطيك UTC متى طلبته. والقاعدة كما في [صفحة الاختيار بين أنواع التاريخ](https://learn.microsoft.com/en-us/dotnet/standard/datetime/choosing-between-datetime?ref=arabroot.io): `DateTimeOffset` لكل لحظة حدثت فعلاً، مثل وقت الطلب، ويقابلها `timestamptz` في PostgreSQL، و`DateTime.UtcNow` إذا احتجت `DateTime`، ولا تستعمل `DateTime.Now` في كود سيرفر، وحول إلى توقيت المستخدم عند العرض فقط. وفي .NET 8 وما بعده توجد أيضاً `TimeProvider` التي تجعل الوقت قابلاً للاختبار بدل استدعاء الساعة مباشرة.

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

### الخطأ 14: ‏Math.Round يقرب إلى الزوجي

رأينا في مقال الأنواع أن `Math.Round` دون تحديد الطريقة أعطت النتيجة الصحيحة في مثال الضريبة بالصدفة، وهنا أربع قيم تبين الصدفة:

```text
=== round: Math.Round default (ToEven) vs AwayFromZero ===
   0.125: Round= 0.12  AwayFromZero= 0.13
   0.135: Round= 0.14  AwayFromZero= 0.14
     2.5: Round=    2  AwayFromZero=    3
     3.5: Round=    4  AwayFromZero=    4
```

الطريقة الافتراضية هي التقريب إلى الزوجي ToEven، ويسميها البعض تقريب البنوك Banker's Rounding، فالنصف يذهب إلى الرقم الزوجي: 0.125 صارت 0.12 لأن 2 زوجي، و0.135 صارت 0.14 لأن 4 زوجي، و2.5 صارت 2\. وهذه الطريقة لها منطق إحصائي لأنها لا تنحاز للأعلى في المتوسط، ولكنها ليست ما يتوقعه المحاسب ولا ما تقوله قاعدة الضريبة في خدمتنا. والإصلاح أن تكتب الطريقة في كل استدعاء دائماً، أو أن تحسب بالأعداد الصحيحة كما فعلنا بالهللات. ولاحظ أن الافتراضي نفسه مختلف بين اللغات: `round` في Python 3 تقرب إلى الزوجي أيضاً، بينما `Math.round` في JavaScript و`Math.round` في Java تقربان النصف للأعلى، فالقادم من JavaScript أو Java هو من يفاجأ هنا أكثر من غيره.

![أربعة صناديق: طلب الساعة 10:00 بتوقيت السعودية حيث حول DateTime.Parse الوقت إلى 07:00 بنوع Local بينما احتفظ DateTimeOffset بـ 10:00 +03:00، وحالة طلب 42 قبلها التعداد وEnum.TryParse أيضاً، وFirst على قائمة فارغة رمى InvalidOperationException وFirstOrDefault أعطت قيماً افتراضية، وMath.Round الافتراضي قرب 0.125 إلى 0.12 و2.5 إلى 2 مقابل AwayFromZero](https://arabroot.io/content/images/2026/10/csharp-idioms-04-dates-enums.webp)

قيم تمر بلا أي خطأ وهي خاطئة: الوقت والتعداد والقيمة الافتراضية والتقريب

وثلاثة أخطاء أخرى رأيناها بالأرقام في مقالات المرحلة السابقة، فلن نعيد تجربتها هنا: `double` للمال الذي أخطأ في 1,302 من 5,000 مبلغ حدي في مقال الأنواع، وعلامة `!` التي أسكتت تحذير null حتى سقطت المهمة الليلية، و`.Result` الذي جمد مجمع الخيوط 23 ثانية في مقال التزامن.

## ماذا تمسك المحللات Analyzers من هذه الأخطاء؟

ذكرنا في المقال الأول أن C# تحلل الكود داخل البناء نفسه، فالسؤال الطبيعي: لماذا لا نترك المحللات تمسك هذه الأخطاء بدل أن نحفظها؟ ولكي نجيب بالأرقام بنينا نفس البرنامج مرتين بالسكربت `lab/run-analyzers.sh`: مرة بالإعداد الافتراضي، ومرة بتفعيل كل قواعد الجودة بالخيار `AnalysisMode=All`:

```bash
dotnet build -c Release --no-incremental
dotnet build -c Release --no-incremental -p:AnalysisMode=All
```

والمخرج سوف يكون كما يلي:

```text
=== default build
      2 warning CA2200
=== AnalysisMode=All
      5 warning CA2007
      5 warning CA1515
      4 warning CA1031
      3 warning CA1852
      3 warning CA1305
      3 warning CA1002
      2 warning CA1311
      2 warning CA1304
      2 warning CA1303
      1 warning CA2200
      1 warning CA1862
      1 warning CA1510
      1 warning CA1507
      1 warning CA1050
--- lines
Program.cs(144,33): warning CA2200: Re-throwing caught exception changes stack information
Program.cs(176,14): warning CA1305: The behavior of 'DateTime.Parse(string)' could vary based on the current user's locale settings. ...
Program.cs(177,15): warning CA1305: The behavior of 'DateTimeOffset.Parse(string)' could vary based on the current user's locale settings. ...
Program.cs(53,142): warning CA1305: The behavior of 'double.ToString(string)' could vary based on the current user's locale settings. ...
Program.cs(67,52): warning CA1304: The behavior of 'string.ToUpper()' could vary based on the current user's locale settings. ...
Program.cs(67,56): warning CA1311: Specify a culture or use an invariant version to avoid implicit dependency on current culture
Program.cs(67,85): warning CA1862: Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, ...
```

في المخرج أعلاه لاحظ التالي:

- البناء الافتراضي أمسك خطأ واحداً فقط من الأربعة عشر، وهو `throw ex` بالقاعدة CA2200، وهي مفعلة افتراضياً لأن ضررها واضح ولا يوجد سبب مشروع لها تقريباً. ولاحظ أن هذا التحذير لم يظهر في مخرج `run.sh` الأول، لأن السكربت كان يبحث عن تحذيرات CS فقط، وهذا درس صغير آخر: إذا كان سكربت CI يرشح المخرج، فتأكد أن المرشح لا يخفي ما تريد رؤيته.
- مع `AnalysisMode=All` ظهرت قواعد الثقافة: CA1305 أمسكت `DateTime.Parse` و`DateTimeOffset.Parse` و`ToString("N2")` بدون ثقافة، وCA1304 وCA1311 وCA1862 أمسكت `ToUpper` في المقارنة، أي الخطأين 2 و3 والجزء الأول من 13.
- ولكن انظر لما لم تمسكه: `double.TryParse("9.90", out ...)` نفسها، التي أعطت 990، لم يظهر عليها أي تحذير، ولا على `+=` في الحلقة، ولا على التنفيذ المؤجل، ولا على الحذف أثناء المرور، ولا على نسخة الـ struct، ولا على Lambda الحلقة، ولا على التعداد، ولا على `First`، ولا على `Math.Round`، ولا على `async void`. أي أن قرابة عشرة أخطاء من أربعة عشر مرت من كل المحللات المدمجة.
- وظهرت معها عشرات التحذيرات التي لا علاقة لها بأخطائنا، مثل CA2007 التي تطلب `ConfigureAwait` في كل `await`، وCA1515 التي تطلب جعل الأنواع `internal`، وهذا سبب أن أحداً لا يفعل `AnalysisMode=All` كما هو، وإنما يختار القواعد.

والنتيجة العملية أمران. الأول أن تفعل في ملف `.editorconfig` قواعد الثقافة والنصوص بمستوى تحذير على الأقل في أي خدمة تقرأ أو تكتب أرقاماً وتواريخ، مثل CA1305 وCA1304 وCA1309 وCA1310 وCA1311، فهي رخيصة وتمسك أخطاء حقيقية كما رأينا. والثاني أن المحللات شبكة أمان وليست بديلاً عن معرفة اللغة، فأغلب ما في هذا المقال يمر منها، ولا يمسكه إلا مراجع يعرفه أو اختبار يشغله. ويوجد محللات إضافية من حزم NuGet تغطي جزءاً من الباقي، مثل حزمة `Microsoft.VisualStudio.Threading.Analyzers` التي فيها قاعدة ضد `async void`، فإذا كان فريقك يكتب كوداً غير متزامن كثيراً فهي تستحق التجربة.

## كيف تمسك هذه الأخطاء بالاختبارات Tests؟

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

- **الثقافة:** شغل اختبارات التحويل والتنسيق مرة بثقافة ألمانية ومرة بثقافة عربية ومرة بتركية، بتغيير `CultureInfo.CurrentCulture` في بداية الاختبار كما فعلنا في التجربة، أو بتشغيل الاختبارات كلها في CI مرة إضافية بمتغير البيئة `LANG=de_DE.UTF-8`. فلو كان في خدمة المتجر اختبار واحد بثقافة de-DE لظهر سعر 990 قبل أن يصل لأي عميل.
- **الوقت:** لا تستدعي `DateTime.Now` ولا `DateTimeOffset.UtcNow` مباشرة في منطق الأعمال، وإنما خذ `TimeProvider` من حقن الاعتماديات، فتستطيع في الاختبار أن تثبت الوقت على لحظة تعرفها، مثل الساعة 23:59 بتوقيت السعودية في آخر يوم من الشهر، وهي اللحظة التي تظهر فيها أخطاء المناطق الزمنية عادة، لأن اليوم بتوقيت UTC ما زال اليوم السابق.
- **الحدود:** لكل دالة تأخذ مجموعة اختبار بمجموعة فارغة، ولكل دالة تأخذ تعداداً من الخارج اختبار بقيمة غير معرفة، ولكل حساب مالي اختبار على قيمة حدية تنتهي بنصف وحدة، كما فعلنا مع الضريبة في مقال الأنواع.
- **الأداء:** خطأ `+=` لا يظهر في اختبار بعشرة أسطر، وإنما في اختبار بحجم بيانات الإنتاج، لذلك ضع للعمليات التي تبني نصوصاً كبيرة أو تمر على مجموعات كبيرة اختباراً واحداً على الأقل بحجم حقيقي، مع حد للزمن أو الذاكرة.

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

## ماذا تفعل في أسبوعك الأول في فريق C#؟ Onboarding

إذا كنت انضممت لفريق يكتب C# كما في قصة هذه المرحلة، فهذه خطوات عملية تختصر عليك أغلب تعليقات المراجعة قبل أن تأتي:

- اقرأ ملف `.editorconfig` و`Directory.Build.props` في المستودع قبل أي كود، فهما يقولان لك ما يفرضه الفريق، وما يعتبره تحذيراً أو خطأ.
- ابن المشروع بـ `dotnet build --no-incremental` مرة واحدة، واقرأ كل التحذيرات الموجودة حتى لو كانت كثيرة، فهي خريطة للأخطاء المتكررة في هذا الكود بالذات.
- ابحث في المستودع عن الكلمات التي في قائمة المراجعة أدناه: `async void` و`.Result` و`DateTime.Now` و`ToUpper()` و`throw ex`، وانظر كيف تعامل الفريق معها، فقد تجد أن بعضها مقصود وله تعليق، وبعضها دين تقني Technical Debt ينتظر من يصلحه.
- في أول طلب دمج لك اكتب في الوصف أنك قادم من لغة أخرى واطلب من المراجع أن يشير إلى ما لا يبدو C#، فهذا يختصر أسابيع من التخمين.
- شغل اختبارات المشروع محلياً بثقافة غير ثقافة جهازك مرة واحدة، فإذا فشل شيء فقد وجدت خطأ حقيقياً في أسبوعك الأول.

## أسلوب C# الحديث Modern C#: نفس الميزة بكود 2012 وكود اليوم

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

```csharp
public class Product
{
    private readonly string _name; private readonly int _price; private readonly int _stock;
    public Product(string name, int price, int stock) { _name = name; _price = price; _stock = stock; }
    public string Name { get { return _name; } }
    public int Price { get { return _price; } }
    public int Stock { get { return _stock; } }
}

public static List<string> Lines(List<Product> products)
{
    if (products == null) throw new ArgumentNullException("products");
    var result = new List<string>();
    foreach (var p in products)
    {
        string label;
        if (p.Stock == 0) label = "sold out";
        else if (p.Stock < 15) label = "low";
        else label = "ok";
        if (p.Price > 1000) result.Add(p.Name + ": " + label);
    }
    result.Sort();
    return result;
}
```

والثاني بأسلوب C# الحديث:

```csharp
public record Product(string Name, int Price, int Stock);

public static List<string> Lines(IEnumerable<Product> products)
{
    ArgumentNullException.ThrowIfNull(products);
    return [.. products
        .Where(p => p.Price > 1000)
        .Select(p => $"{p.Name}: {Label(p)}")
        .Order()];
}

static string Label(Product p) => p.Stock switch
{
    0 => "sold out",
    < 15 => "low",
    _ => "ok",
};
```

والمخرج يبين أن النتيجة واحدة:

```text
=== modern: the same report, 2012 style vs modern C# ===
  legacy: dallah: sold out | dates: low
  modern: dallah: sold out | dates: low
  same output: True
  raw string literal: {"name": "dallah", "price_halalas": 12900}
```

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

- الكلاس بثلاثة حقول وثلاث خصائص ومشيد Constructor صار سطراً واحداً بالسجل، ومعه المقارنة بالقيمة و`ToString` مجاناً.
- `ArgumentNullException.ThrowIfNull` تكتب اسم المعامل تلقائياً، فلا يوجد نص `"products"` ينسى أحدهم تغييره عند إعادة التسمية.
- المعامل صار `IEnumerable<Product>` بدل `List`، فالدالة تقبل أي مجموعة، وهذه من إرشادات تصميم المكتبات: اقبل أعم نوع تحتاجه، وأرجع أدق نوع عندك.
- الشروط المتداخلة صارت تعبير `switch` بأنماط، وكل فرع سطر، والمترجم يحذرك إذا نسيت حالة.
- `[.. query]` تعبير مجموعات Collection Expression يحول الاستعلام إلى النوع الذي ترجعه الدالة، و`Order()` ترتب دون أن تكتب `x => x`.
- السطر الأخير في المخرج نص خام [Raw String Literal](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/raw-string?ref=arabroot.io) بثلاث علامات تنصيص، يكتب JSON كما هو دون أي هروب للعلامات.

وهناك ميزات حديثة أخرى ستراها في كل مستودع جديد: المشيدات الأساسية [Primary Constructors](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors?ref=arabroot.io) للكلاسات، مثل `OrderService(NpgsqlDataSource db)` في خدمة المقال الرابع، فتأخذ الاعتماديات دون حقول ومشيد، ومساحة الأسماء على مستوى الملف، والخصائص المطلوبة `required`، و`is null` و`is not null` بدل `== null`. ولا تحتاج أن تحفظ كل هذا، فالمحررات تقترح التحويل بنفسها، والأهم أن يفرض الفريق أسلوباً واحداً عن طريق ملف `.editorconfig` فيه [قواعد الأسلوب](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options?ref=arabroot.io) مع الخيار `EnforceCodeStyleInBuild`، فيصير مخالف الأسلوب تحذيراً في البناء، ومع `TreatWarningsAsErrors` خطأ، فلا يبقى في المراجعة نقاش عن الشكل.

![أربعة أعمدة لأربعة أسابيع: الأسبوع الأول اللغة بجولة C# والأساسيات ومطابقة الأنماط وLINQ والأنواع القابلة للفراغ، والثاني المنصة بمقدمة .NET والاستثناءات وasync وGC والعولمة، والثالث الويب بـ Minimal APIs وحقن الاعتماديات والإعدادات والسجلات والاختبار بـ WebApplicationFactory وأفضل الممارسات، والرابع العمق بإرشادات تصميم المكتبات وما الجديد في C# 14 و.NET 10 ومقال تحسينات الأداء ومواصفة اللغة وكود dotnet/runtime](https://arabroot.io/content/images/2026/10/csharp-idioms-05-reading-plan.webp)

أربعة أسابيع من المصادر الرسمية المجانية، بالترتيب الذي يناسب من يعرف البرمجة

## قائمة مراجعة الكود Code Review Checklist

| ابحث في الكود عن                                  | لماذا                          | البديل                             |
| ------------------------------------------------- | ------------------------------ | ---------------------------------- |
| += على string داخل حلقة                           | 10 ثوان و7.6 GB لعشرين ألف سطر | StringBuilder أو string.Join       |
| Parse أو ToString بدون ثقافة لبيانات تقرؤها برامج | 9.90 صارت 990 على de-DE        | CultureInfo.InvariantCulture       |
| ToUpper() أو ToLower() للمقارنة                   | «İNVOİCE» على tr-TR            | StringComparison.OrdinalIgnoreCase |
| متغير LINQ يمر عليه أكثر من مرة                   | 9 حسابات لثلاثة عناصر          | ToList() مرة واحدة                 |
| Remove داخل foreach                               | استثناء وقائمة نصف معدلة       | RemoveAll أو Where                 |
| First() على مجموعة قد تكون فارغة                  | استثناء بلا سياق               | FirstOrDefault مع فحص              |
| struct قابل للتعديل                               | تعديل نسخة دون أن تعرف         | readonly struct أو record مع with  |
| record فيه List أو مصفوفة                         | المقارنة بالمرجع               | Equals مخصصة أو مفتاح              |
| تحويل عدد إلى enum من الخارج                      | 42 قيمة صالحة                  | Enum.IsDefined                     |
| Lambda داخل for                                   | 3,3,3                          | نسخة محلية أو foreach              |
| throw ex;                                         | يضيع مكان الخطأ                | throw;                             |
| async void                                        | يسقط العملية بالرمز 134        | async Task                         |
| DateTime.Now وDateTime.Parse                      | الوقت بتوقيت السيرفر           | DateTimeOffset وUtcNow             |
| Math.Round بدون طريقة                             | 2.5 تصير 2                     | MidpointRounding.AwayFromZero      |
| double للمال، !، .Result                          | من المقالات السابقة            | هللات int، فحص null، await         |

## خطة القراءة Reading Plan من المصادر الرسمية

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

**الأسبوع الأول، اللغة:** ابدأ بـ [جولة في C#](https://learn.microsoft.com/en-us/dotnet/csharp/tour-of-csharp/?ref=arabroot.io)، فهي مكتوبة لمن يعرف لغة أخرى، ثم [أساسيات نظام الأنواع](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/?ref=arabroot.io) لتثبيت ما رأيناه عن القيم والمراجع، ثم [مطابقة الأنماط](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching?ref=arabroot.io)، ثم [مقدمة LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/get-started/introduction-to-linq-queries?ref=arabroot.io)، وأخيراً [الأنواع القابلة للفراغ](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references?ref=arabroot.io). وفي نهاية الأسبوع افتح [تاريخ إصدارات C#](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history?ref=arabroot.io) وتصفحه سريعاً، لتعرف عندما ترى كوداً قديماً من أي سنة هو، وما البديل الحديث له.

**الأسبوع الثاني، المنصة:** [مقدمة .NET](https://learn.microsoft.com/en-us/dotnet/core/introduction?ref=arabroot.io) و[أداة dotnet](https://learn.microsoft.com/en-us/dotnet/core/tools/?ref=arabroot.io)، ثم [الاستثناءات](https://learn.microsoft.com/en-us/dotnet/standard/exceptions/?ref=arabroot.io) مع [أفضل ممارساتها](https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions?ref=arabroot.io)، ثم [البرمجة غير المتزامنة](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/?ref=arabroot.io)، ثم [جامع القمامة](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/?ref=arabroot.io)، ثم [أفضل ممارسات النصوص](https://learn.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings?ref=arabroot.io) و[العولمة](https://learn.microsoft.com/en-us/dotnet/core/extensions/globalization?ref=arabroot.io)، لأنهما مصدر نصف الأخطاء في هذا المقال.

**الأسبوع الثالث، الويب:** [الـ Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview?ref=arabroot.io) في ASP.NET Core، ثم [حقن الاعتماديات Dependency Injection](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?ref=arabroot.io) لأنه أساس كل خدمة، ثم [اختبارات التكامل](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?ref=arabroot.io) مع WebApplicationFactory كما في خدمة المقال الرابع، ثم [أفضل ممارسات ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices?ref=arabroot.io)، وهي صفحة واحدة تستحق أن تقرأها مرتين.

**الأسبوع الرابع، العمق:** [إرشادات تصميم المكتبات Framework Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/?ref=arabroot.io)، وهي الأسلوب الذي كتبت به مكتبة .NET نفسها، ثم [ما الجديد في .NET 10](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/overview?ref=arabroot.io) و[C# 14](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14?ref=arabroot.io)، ثم المقال السنوي الطويل [Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/?ref=arabroot.io) الذي يشرح كيف تعمل المنصة من الداخل، وعندما تريد إجابة دقيقة عن سلوك اللغة فالمرجع [مواصفة C#](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/readme?ref=arabroot.io)، وعن سلوك المكتبة فالكود نفسه في [مستودع dotnet/runtime](https://github.com/dotnet/runtime?ref=arabroot.io).

## الخلاصة

- أغلب أخطاء القادمين من لغات أخرى لا يكشفها المترجم، لأنها عادات صحيحة في لغتهم وخاطئة في C#.
- النص لا يتغير، فـ `+=` في حلقة أخذ عشر ثوان و7.6 جيجابايت، و`StringBuilder` ملي ثانية واحدة.
- التحويل والتنسيق يتبعان ثقافة السيرفر: 9.90 صارت 990 على de-DE وفشلت على ar-SA، و«invoice» لم تساو «INVOICE» على tr-TR، والإصلاح InvariantCulture وOrdinalIgnoreCase.
- LINQ مؤجلة فنفذت الحساب تسع مرات لثلاثة عناصر، والحذف أثناء المرور ترك القائمة نصف معدلة، و`FirstOrDefault` ترجع قيمة افتراضية وليس null لأنواع القيمة.
- نسخة struct من قائمة لا تعدل القائمة، والسجل يقارن القوائم بالمرجع، والتعداد قبل 42 حتى عبر `TryParse`.
- Lambda داخل `for` طبعت 3,3,3، و`throw ex` محت مكان الخطأ، و`async void` أسقطت العملية بالرمز 134 ولم تلتقطها `catch`.
- `DateTime.Parse` حول 10:00 بتوقيت السعودية إلى 07:00 بتوقيت السيرفر ونسي الفرق، و`Math.Round` يقرب إلى الزوجي.
- الأسلوب الحديث بالسجلات والأنماط وتعبيرات المجموعات يعطي نفس النتيجة بنصف الكود، ويفرضه على الفريق `.editorconfig` مع البناء.

وبهذا تكتمل مرحلة [لغة C# للمطورين](https://arabroot.io/start/#stage-lang-csharp)، وتجد مقالاتها الخمسة ومقالات تكملها هنا:

[![](https://arabroot.io/content/images/2026/10/csharp-for-devs-cover-cover.webp)دليلC# لمن يعرف البرمجة: الأدوات Toolchain والأساسياتتثبيت .NET 10 وجولة في C# مقارنة بـ Python وJavaScript، ثم حل كامل بالأمر dotnet من البناء والاختبار إلى أربعة أشكال للنشر، مع فخ تحذيرات حقيقي واجهناه.](https://arabroot.io/articles/csharp-%D9%84%D9%85%D9%86-%D9%8A%D8%B9%D8%B1%D9%81-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/csharp-types-memory-cover-cover.webp)دليلالأنواع والذاكرة والاستثناءات Exceptions في C#قسنا ما تكلفه أنواع القيمة والمرجع وجامع القمامة داخل Container محدود، ثم شغلنا كل خطأ شائع حتى يسقط، وكشفنا أن double يخطئ في ربع مبالغ الضريبة الحدية.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%88%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%AB%D9%86%D8%A7%D8%A1%D8%A7%D8%AA-%D9%81%D9%8A-csharp/)[![](https://arabroot.io/content/images/2026/10/csharp-concurrency-cover-cover.webp)دليلالتزامن في C#: ‏async/await والمهام Tasks بالأرقامقسنا نموذج التزامن في .NET بمئة طلب HTTP ومهمة SHA-256 على نواة ونواتين وأربع، ورأينا ‏.Result يجمد مجمع الخيوط 23 ثانية، ثم سباق بيانات وقفلاً مميتاً وإصلاح كل منهما.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%81%D9%8A-csharp-async-await-%D9%88%D8%A7%D9%84%D9%85%D9%87%D8%A7%D9%85-tasks/)[![](https://arabroot.io/content/images/2026/10/csharp-real-service-cover-cover.webp)دليلخدمة حقيقية بـ C#: ‏ASP.NET Core مع PostgreSQL والاختباراتبنينا واجهة طلبات بـ ASP.NET Core وNpgsql بقفل الصفوف وضريبة بالهللات، واختبرناها بعشرين طلباً متوازياً على PostgreSQL حقيقي، ثم قسنا الـ Image والبدء البارد والذاكرة وwrk بأمانة.](https://arabroot.io/articles/%D8%AE%D8%AF%D9%85%D8%A9-%D8%AD%D9%82%D9%8A%D9%82%D9%8A%D8%A9-%D8%A8-csharp-asp-net-core-%D9%88-postgresql/)[![](https://arabroot.io/content/images/2026/10/polyglot-programming-cover-cover.webp)دليلالبرمجة بعدة لغات Polyglot: متى تفيد ومتى تعقد مشروعك؟نستدعي نفس الدالة من Python بخمس طرق، عبر FFI إلى C وGo وعبر HTTP وgRPC، ونقيس كلفة كل حد بين لغتين وتعقيد البناء وما يحدث عندما يتعطل الطرف الآخر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A8%D8%B9%D8%AF%D8%A9-%D9%84%D8%BA%D8%A7%D8%AA-polyglot-%D9%85%D8%AA%D9%89-%D8%AA%D9%81%D9%8A%D8%AF-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%B9%D9%82%D8%AF-%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)[![](https://arabroot.io/content/images/2026/10/debugging-basics-cover-cover.webp)دليلقراءة ال Stack Trace وتصحيح الأخطاء Debugging للمبتدئينكيف تقرأ رسالة الخطأ في Python وJavaScript سطراً بسطر، ومتى تستخدم الطباعة أو نقاط التوقف في VS Code، وكيف يجد git bisect التعديل الذي أدخل الخطأ.](https://arabroot.io/articles/%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-%D8%B1%D8%B3%D8%A7%D9%84%D8%A9-%D8%A7%D9%84%D8%AE%D8%B7%D8%A7-stack-trace-%D9%88%D8%A7%D9%84%D8%AA%D8%B5%D8%AD%D9%8A%D8%AD-debugging/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ الأقسام الأربعة عشر بـ .NET SDK 10.0.403 وRuntime 10.0.14 في Container بنواتين.